excel_utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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. purchase_date = data.get('purchase_date', '').strip() or ''
  150. warranty_expire = data.get('warranty_expire', '').strip() or ''
  151. # 处理IP
  152. bmc_address = data.get('bmc_address', '').strip() or ''
  153. ip_address = data.get('ip_address', '').strip() or ''
  154. # 资产面值 - 直接存字符串
  155. asset_value = data.get('asset_value', '').strip() or ''
  156. # 处理ID - 如果提供了ID且已存在,则更新该记录
  157. import_id = data.get('id', '').strip()
  158. asset = None
  159. is_update = False
  160. if import_id and import_id.isdigit():
  161. try:
  162. id_val = int(import_id) if str(import_id).strip().isdigit() else None
  163. if id_val:
  164. asset = Asset.objects.get(id=id_val)
  165. is_update = True
  166. except (Asset.DoesNotExist, ValueError):
  167. asset = None
  168. if asset:
  169. # 更新已有记录
  170. asset.asset_number = asset_number
  171. asset.name = data.get('name', '')
  172. asset.category = category
  173. asset.brand = data.get('brand', '')
  174. asset.model = data.get('model', '')
  175. asset.asset_value = asset_value
  176. asset.serial_number = data.get('serial_number', '')
  177. asset.location = data.get('location', '')
  178. asset.cabinet = data.get('cabinet', '')
  179. asset.cabinet_position = data.get('cabinet_position', '')
  180. asset.bmc_address = bmc_address
  181. asset.ip_address = ip_address
  182. asset.gpu_type = data.get('gpu_type', '')
  183. gpu_count_str = data.get('gpu_count', '').strip()
  184. asset.gpu_count = gpu_count_str if gpu_count_str else ''
  185. asset.purchase_date = purchase_date
  186. asset.warranty_expire = warranty_expire
  187. asset.supplier = data.get('supplier', '')
  188. asset.responsible_person = data.get('responsible_person', '')
  189. asset.department = data.get('department', '')
  190. asset.user = data.get('user', '')
  191. asset.business_type = data.get('business_type', '')
  192. asset.status = status
  193. asset.remark = data.get('remark', '')
  194. asset.save()
  195. AssetChangeLog.objects.create(
  196. asset=asset,
  197. asset_number=asset.asset_number,
  198. action='import',
  199. description=f'通过Excel导入更新(ID:{asset.id})',
  200. operator=operator,
  201. )
  202. else:
  203. # 创建新记录
  204. gpu_count_str = data.get('gpu_count', '').strip()
  205. gpu_count = gpu_count_str if gpu_count_str else ''
  206. # 创建参数
  207. create_kwargs = {
  208. 'asset_number': asset_number,
  209. 'name': data.get('name', ''),
  210. 'category': category,
  211. 'brand': data.get('brand', ''),
  212. 'model': data.get('model', ''),
  213. 'asset_value': asset_value,
  214. 'serial_number': data.get('serial_number', ''),
  215. 'location': data.get('location', ''),
  216. 'cabinet': data.get('cabinet', ''),
  217. 'cabinet_position': data.get('cabinet_position', ''),
  218. 'bmc_address': bmc_address,
  219. 'ip_address': ip_address,
  220. 'gpu_type': data.get('gpu_type', ''),
  221. 'gpu_count': gpu_count,
  222. 'purchase_date': purchase_date,
  223. 'warranty_expire': warranty_expire,
  224. 'supplier': data.get('supplier', ''),
  225. 'responsible_person': data.get('responsible_person', ''),
  226. 'department': data.get('department', ''),
  227. 'user': data.get('user', ''),
  228. 'business_type': data.get('business_type', ''),
  229. 'status': status,
  230. 'remark': data.get('remark', ''),
  231. 'created_by': operator,
  232. }
  233. # 如果Excel提供了ID,使用该ID创建
  234. if import_id and import_id.isdigit():
  235. try:
  236. if str(import_id).strip().isdigit():
  237. create_kwargs['id'] = int(import_id)
  238. except ValueError:
  239. pass
  240. asset = Asset.objects.create(**create_kwargs)
  241. AssetChangeLog.objects.create(
  242. asset=asset,
  243. asset_number=asset.asset_number,
  244. action='import',
  245. description=f'通过Excel导入创建',
  246. operator=operator,
  247. )
  248. results['success'] += 1
  249. except Exception as e:
  250. results['errors'].append(f'第{row_idx}行: {str(e)}')
  251. return results