excel_utils.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """Excel导入导出工具"""
  2. from openpyxl import Workbook
  3. from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
  4. from openpyxl.utils import get_column_letter
  5. from django.utils import timezone
  6. from .models import Category
  7. # Excel列定义(顺序与资产列表一致)
  8. EXPORT_COLUMNS = [
  9. ('id', 'ID', 8),
  10. ('location', '机房', 20),
  11. ('cabinet', '机柜', 10),
  12. ('cabinet_position', '机柜位置', 10),
  13. ('asset_number', '资产编号', 18),
  14. ('name', '设备名称', 20),
  15. ('category', '设备分类', 12),
  16. ('brand', '品牌', 12),
  17. ('model', '型号', 20),
  18. ('asset_value', '资产面值', 12),
  19. ('serial_number', '序列号', 25),
  20. ('bmc_address', 'BMC地址', 16),
  21. ('ip_address', 'IP地址', 16),
  22. ('gpu_type', '显卡类型', 15),
  23. ('gpu_count', '卡数', 6),
  24. ('responsible_person', '负责人', 10),
  25. ('department', '使用部门', 15),
  26. ('user', '维护人', 10),
  27. ('business_type', '业务类型', 15),
  28. ('status', '状态', 8),
  29. ('purchase_date', '采购日期', 12),
  30. ('warranty_expire', '质保到期', 12),
  31. ('supplier', '供应商', 15),
  32. ('remark', '备注', 30),
  33. ]
  34. STATUS_MAP = {
  35. 'in_use': '在用', 'idle': '闲置', 'maintenance': '维修中', 'scrapped': '已报废',
  36. }
  37. STATUS_MAP_REVERSE = {v: k for k, v in STATUS_MAP.items()}
  38. # 深色主题样式
  39. HEADER_FILL = PatternFill(start_color='1B2838', end_color='1B2838', fill_type='solid')
  40. HEADER_FONT = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
  41. CELL_FONT = Font(name='微软雅黑', size=10)
  42. THIN_BORDER = Border(
  43. left=Side(style='thin', color='3A4A5C'),
  44. right=Side(style='thin', color='3A4A5C'),
  45. top=Side(style='thin', color='3A4A5C'),
  46. bottom=Side(style='thin', color='3A4A5C'),
  47. )
  48. def export_assets_to_excel(queryset):
  49. """导出资产到Excel"""
  50. wb = Workbook()
  51. ws = wb.active
  52. ws.title = '硬件资产'
  53. # 写表头
  54. for col_idx, (field, header, width) in enumerate(EXPORT_COLUMNS, 1):
  55. cell = ws.cell(row=1, column=col_idx, value=header)
  56. cell.fill = HEADER_FILL
  57. cell.font = HEADER_FONT
  58. cell.alignment = Alignment(horizontal='center', vertical='center')
  59. cell.border = THIN_BORDER
  60. ws.column_dimensions[get_column_letter(col_idx)].width = width
  61. # 写数据
  62. for row_idx, asset in enumerate(queryset, 2):
  63. for col_idx, (field, _, _) in enumerate(EXPORT_COLUMNS, 1):
  64. if field == 'id':
  65. value = asset.id
  66. elif field == 'category':
  67. value = str(asset.category) if asset.category else ''
  68. elif field == 'status':
  69. value = STATUS_MAP.get(asset.status, asset.status)
  70. elif field in ('purchase_date', 'warranty_expire'):
  71. value = str(getattr(asset, field, '')) or ''
  72. else:
  73. value = getattr(asset, field, '') or ''
  74. cell = ws.cell(row=row_idx, column=col_idx, value=value)
  75. cell.font = CELL_FONT
  76. cell.border = THIN_BORDER
  77. cell.alignment = Alignment(vertical='center')
  78. # 冻结首行
  79. ws.freeze_panes = 'A2'
  80. return wb
  81. def generate_import_template():
  82. """生成导入模板"""
  83. wb = Workbook()
  84. ws = wb.active
  85. ws.title = '资产导入模板'
  86. headers = [header for _, header, width in EXPORT_COLUMNS]
  87. for col_idx, (field, header, width) in enumerate(EXPORT_COLUMNS, 1):
  88. cell = ws.cell(row=1, column=col_idx, value=header)
  89. cell.fill = HEADER_FILL
  90. cell.font = HEADER_FONT
  91. cell.alignment = Alignment(horizontal='center', vertical='center')
  92. cell.border = THIN_BORDER
  93. ws.column_dimensions[get_column_letter(col_idx)].width = width
  94. # 示例数据行(顺序与EXPORT_COLUMNS一致)
  95. example_data = [
  96. '1', '3楼机房A区', 'A01', 'U10-U15', 'IT-2024-0001', '测试服务器',
  97. '服务器', 'Dell', 'PowerEdge R740', '50000.00', 'ABC123456',
  98. '192.168.1.200', '192.168.1.100', 'NVIDIA A100', '8', '张三',
  99. '研发部', '李四', 'AI训练', '在用', '2024-01-15', '2027-01-15',
  100. '戴尔科技', '测试备注'
  101. ]
  102. for col_idx, value in enumerate(example_data, 1):
  103. cell = ws.cell(row=2, column=col_idx, value=value)
  104. cell.font = Font(name='微软雅黑', size=10, color='666666')
  105. cell.border = THIN_BORDER
  106. ws.freeze_panes = 'A2'
  107. return wb
  108. def import_assets_from_excel(ws, category_map, operator=None):
  109. """从Excel导入资产,返回结果统计"""
  110. from .models import Asset, AssetChangeLog
  111. header_row = [cell.value for cell in ws[1]]
  112. field_map = {header: field for field, header, _ in EXPORT_COLUMNS}
  113. results = {
  114. 'success': 0,
  115. 'skipped': 0,
  116. 'errors': [],
  117. 'total': 0,
  118. }
  119. for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), 2):
  120. if not row or not row[0]:
  121. continue
  122. results['total'] += 1
  123. try:
  124. data = {}
  125. for col_idx, value in enumerate(row):
  126. if col_idx < len(header_row):
  127. field = field_map.get(header_row[col_idx])
  128. if field:
  129. data[field] = str(value).strip() if value else ''
  130. # 资产编号必填
  131. asset_number = data.get('asset_number', '').strip()
  132. if not asset_number:
  133. results['errors'].append(f'第{row_idx}行: 缺少资产编号')
  134. continue
  135. # 处理分类 - 不存在则自动创建
  136. category_name = data.get('category', '').strip()
  137. category = category_map.get(category_name)
  138. if not category and category_name:
  139. category = Category.objects.create(name=category_name)
  140. category_map[category_name] = category
  141. elif not category_name:
  142. category = category_map.get('未分类')
  143. if not category:
  144. category = Category.objects.create(name='未分类')
  145. category_map['未分类'] = category
  146. # 处理状态
  147. status = STATUS_MAP_REVERSE.get(data.get('status', '在用'), 'in_use')
  148. # 处理日期
  149. from datetime import datetime as dt
  150. purchase_date = None
  151. warranty_expire = None
  152. if data.get('purchase_date'):
  153. try:
  154. purchase_date = dt.strptime(data['purchase_date'], '%Y-%m-%d').date()
  155. except ValueError:
  156. pass
  157. if data.get('warranty_expire'):
  158. try:
  159. warranty_expire = dt.strptime(data['warranty_expire'], '%Y-%m-%d').date()
  160. except ValueError:
  161. pass
  162. # 处理IP
  163. bmc_address = data.get('bmc_address') or None
  164. ip_address = data.get('ip_address') or None
  165. # 处理资产面值
  166. from decimal import Decimal, InvalidOperation
  167. asset_value = None
  168. if data.get('asset_value'):
  169. try:
  170. asset_value = Decimal(str(data['asset_value']).replace(',', ''))
  171. except (InvalidOperation, ValueError):
  172. pass
  173. # 处理ID - 如果提供了ID且已存在,则更新该记录
  174. import_id = data.get('id', '').strip()
  175. asset = None
  176. is_update = False
  177. if import_id:
  178. try:
  179. asset = Asset.objects.get(id=int(import_id))
  180. is_update = True
  181. except (Asset.DoesNotExist, ValueError):
  182. asset = None
  183. if asset:
  184. # 更新已有记录
  185. asset.asset_number = asset_number
  186. asset.name = data.get('name', '')
  187. asset.category = category
  188. asset.brand = data.get('brand', '')
  189. asset.model = data.get('model', '')
  190. asset.asset_value = asset_value
  191. asset.serial_number = data.get('serial_number', '')
  192. asset.location = data.get('location', '')
  193. asset.cabinet = data.get('cabinet', '')
  194. asset.cabinet_position = data.get('cabinet_position', '')
  195. asset.bmc_address = bmc_address
  196. asset.ip_address = ip_address
  197. asset.gpu_type = data.get('gpu_type', '')
  198. gpu_count_str = data.get('gpu_count', '').strip()
  199. asset.gpu_count = int(gpu_count_str) if gpu_count_str else None
  200. asset.purchase_date = purchase_date
  201. asset.warranty_expire = warranty_expire
  202. asset.supplier = data.get('supplier', '')
  203. asset.responsible_person = data.get('responsible_person', '')
  204. asset.department = data.get('department', '')
  205. asset.user = data.get('user', '')
  206. asset.business_type = data.get('business_type', '')
  207. asset.status = status
  208. asset.remark = data.get('remark', '')
  209. asset.save()
  210. AssetChangeLog.objects.create(
  211. asset=asset,
  212. asset_number=asset.asset_number,
  213. action='import',
  214. description=f'通过Excel导入更新(ID:{asset.id})',
  215. operator=operator,
  216. )
  217. else:
  218. # 创建新记录
  219. gpu_count_str = data.get('gpu_count', '').strip()
  220. gpu_count = int(gpu_count_str) if gpu_count_str else None
  221. # 创建参数
  222. create_kwargs = {
  223. 'asset_number': asset_number,
  224. 'name': data.get('name', ''),
  225. 'category': category,
  226. 'brand': data.get('brand', ''),
  227. 'model': data.get('model', ''),
  228. 'asset_value': asset_value,
  229. 'serial_number': data.get('serial_number', ''),
  230. 'location': data.get('location', ''),
  231. 'cabinet': data.get('cabinet', ''),
  232. 'cabinet_position': data.get('cabinet_position', ''),
  233. 'bmc_address': bmc_address,
  234. 'ip_address': ip_address,
  235. 'gpu_type': data.get('gpu_type', ''),
  236. 'gpu_count': gpu_count,
  237. 'purchase_date': purchase_date,
  238. 'warranty_expire': warranty_expire,
  239. 'supplier': data.get('supplier', ''),
  240. 'responsible_person': data.get('responsible_person', ''),
  241. 'department': data.get('department', ''),
  242. 'user': data.get('user', ''),
  243. 'business_type': data.get('business_type', ''),
  244. 'status': status,
  245. 'remark': data.get('remark', ''),
  246. 'created_by': operator,
  247. }
  248. # 如果Excel提供了ID,使用该ID创建
  249. if import_id:
  250. try:
  251. create_kwargs['id'] = int(import_id)
  252. except ValueError:
  253. pass
  254. asset = Asset.objects.create(**create_kwargs)
  255. AssetChangeLog.objects.create(
  256. asset=asset,
  257. asset_number=asset.asset_number,
  258. action='import',
  259. description=f'通过Excel导入创建',
  260. operator=operator,
  261. )
  262. results['success'] += 1
  263. except Exception as e:
  264. results['errors'].append(f'第{row_idx}行: {str(e)}')
  265. return results