excel_utils.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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. ('asset_number', '资产编号', 18),
  11. ('name', '设备名称', 20),
  12. ('category', '设备分类', 12),
  13. ('brand', '品牌', 12),
  14. ('model', '型号', 20),
  15. ('asset_value', '资产面值', 12),
  16. ('serial_number', '序列号', 25),
  17. ('location', '机房', 20),
  18. ('cabinet', '机柜', 10),
  19. ('cabinet_position', '机柜位置', 10),
  20. ('bmc_address', 'BMC地址', 16),
  21. ('ip_address', 'IP地址', 16),
  22. ('gpu_type', '显卡类型', 15),
  23. ('gpu_count', '卡数', 6),
  24. ('purchase_date', '采购日期', 12),
  25. ('warranty_expire', '质保到期', 12),
  26. ('supplier', '供应商', 15),
  27. ('responsible_person', '负责人', 10),
  28. ('department', '使用部门', 15),
  29. ('user', '使用人', 10),
  30. ('business_type', '业务类型', 15),
  31. ('status', '状态', 8),
  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. # 示例数据行
  95. example_data = [
  96. '1', 'IT-2024-0001', '测试服务器', '服务器', 'Dell', 'PowerEdge R740',
  97. '50000.00', 'ABC123456', '3楼机房A区', 'A01', 'U10-U15', '192.168.1.200',
  98. '192.168.1.100', 'NVIDIA A100', '8', '2024-01-15', '2027-01-15', '戴尔科技',
  99. '张三', '研发部', '李四', 'AI训练', '在用', '测试备注'
  100. ]
  101. for col_idx, value in enumerate(example_data, 1):
  102. cell = ws.cell(row=2, column=col_idx, value=value)
  103. cell.font = Font(name='微软雅黑', size=10, color='666666')
  104. cell.border = THIN_BORDER
  105. ws.freeze_panes = 'A2'
  106. return wb
  107. def import_assets_from_excel(ws, category_map, operator=None):
  108. """从Excel导入资产,返回结果统计"""
  109. from .models import Asset, AssetChangeLog
  110. header_row = [cell.value for cell in ws[1]]
  111. field_map = {header: field for field, header, _ in EXPORT_COLUMNS}
  112. results = {
  113. 'success': 0,
  114. 'skipped': 0,
  115. 'errors': [],
  116. 'total': 0,
  117. }
  118. for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=True), 2):
  119. if not row or not row[0]:
  120. continue
  121. results['total'] += 1
  122. try:
  123. data = {}
  124. for col_idx, value in enumerate(row):
  125. if col_idx < len(header_row):
  126. field = field_map.get(header_row[col_idx])
  127. if field:
  128. data[field] = str(value).strip() if value else ''
  129. # 资产编号必填
  130. asset_number = data.get('asset_number', '').strip()
  131. if not asset_number:
  132. results['errors'].append(f'第{row_idx}行: 缺少资产编号')
  133. continue
  134. # 处理分类 - 不存在则自动创建
  135. category_name = data.get('category', '').strip()
  136. category = category_map.get(category_name)
  137. if not category and category_name:
  138. category = Category.objects.create(name=category_name)
  139. category_map[category_name] = category
  140. elif not category_name:
  141. category = category_map.get('未分类')
  142. if not category:
  143. category = Category.objects.create(name='未分类')
  144. category_map['未分类'] = category
  145. # 处理状态
  146. status = STATUS_MAP_REVERSE.get(data.get('status', '在用'), 'in_use')
  147. # 处理日期
  148. from datetime import datetime as dt
  149. purchase_date = None
  150. warranty_expire = None
  151. if data.get('purchase_date'):
  152. try:
  153. purchase_date = dt.strptime(data['purchase_date'], '%Y-%m-%d').date()
  154. except ValueError:
  155. pass
  156. if data.get('warranty_expire'):
  157. try:
  158. warranty_expire = dt.strptime(data['warranty_expire'], '%Y-%m-%d').date()
  159. except ValueError:
  160. pass
  161. # 处理IP
  162. bmc_address = data.get('bmc_address') or None
  163. ip_address = data.get('ip_address') or None
  164. # 处理资产面值
  165. from decimal import Decimal, InvalidOperation
  166. asset_value = None
  167. if data.get('asset_value'):
  168. try:
  169. asset_value = Decimal(str(data['asset_value']).replace(',', ''))
  170. except (InvalidOperation, ValueError):
  171. pass
  172. # 处理ID - 如果提供了ID且已存在,则更新该记录
  173. import_id = data.get('id', '').strip()
  174. asset = None
  175. is_update = False
  176. if import_id:
  177. try:
  178. asset = Asset.objects.get(id=int(import_id))
  179. is_update = True
  180. except (Asset.DoesNotExist, ValueError):
  181. asset = None
  182. if asset:
  183. # 更新已有记录
  184. asset.asset_number = asset_number
  185. asset.name = data.get('name', '')
  186. asset.category = category
  187. asset.brand = data.get('brand', '')
  188. asset.model = data.get('model', '')
  189. asset.asset_value = asset_value
  190. asset.serial_number = data.get('serial_number', '')
  191. asset.location = data.get('location', '')
  192. asset.cabinet = data.get('cabinet', '')
  193. asset.cabinet_position = data.get('cabinet_position', '')
  194. asset.bmc_address = bmc_address
  195. asset.ip_address = ip_address
  196. asset.gpu_type = data.get('gpu_type', '')
  197. gpu_count_str = data.get('gpu_count', '').strip()
  198. asset.gpu_count = int(gpu_count_str) if gpu_count_str else None
  199. asset.purchase_date = purchase_date
  200. asset.warranty_expire = warranty_expire
  201. asset.supplier = data.get('supplier', '')
  202. asset.responsible_person = data.get('responsible_person', '')
  203. asset.department = data.get('department', '')
  204. asset.user = data.get('user', '')
  205. asset.business_type = data.get('business_type', '')
  206. asset.status = status
  207. asset.remark = data.get('remark', '')
  208. asset.save()
  209. AssetChangeLog.objects.create(
  210. asset=asset,
  211. asset_number=asset.asset_number,
  212. action='import',
  213. description=f'通过Excel导入更新(ID:{asset.id})',
  214. operator=operator,
  215. )
  216. else:
  217. # 创建新记录
  218. gpu_count_str = data.get('gpu_count', '').strip()
  219. gpu_count = int(gpu_count_str) if gpu_count_str else None
  220. asset = Asset.objects.create(
  221. asset_number=asset_number,
  222. name=data.get('name', ''),
  223. category=category,
  224. brand=data.get('brand', ''),
  225. model=data.get('model', ''),
  226. asset_value=asset_value,
  227. serial_number=data.get('serial_number', ''),
  228. location=data.get('location', ''),
  229. cabinet=data.get('cabinet', ''),
  230. cabinet_position=data.get('cabinet_position', ''),
  231. bmc_address=bmc_address,
  232. ip_address=ip_address,
  233. gpu_type=data.get('gpu_type', ''),
  234. gpu_count=gpu_count,
  235. purchase_date=purchase_date,
  236. warranty_expire=warranty_expire,
  237. supplier=data.get('supplier', ''),
  238. responsible_person=data.get('responsible_person', ''),
  239. department=data.get('department', ''),
  240. user=data.get('user', ''),
  241. business_type=data.get('business_type', ''),
  242. status=status,
  243. remark=data.get('remark', ''),
  244. created_by=operator,
  245. )
  246. AssetChangeLog.objects.create(
  247. asset=asset,
  248. asset_number=asset.asset_number,
  249. action='import',
  250. description=f'通过Excel导入创建',
  251. operator=operator,
  252. )
  253. results['success'] += 1
  254. except Exception as e:
  255. results['errors'].append(f'第{row_idx}行: {str(e)}')
  256. return results