jw_report_excel.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. # -*- coding: utf-8 -*-
  2. # Author : Charley
  3. # Python : 3.12.10
  4. # Date : 2026/08/19
  5. """集物星球报表 Excel 样式工具库(openpyxl):供在售/已售日报复用统一排版。
  6. 提供分区标题条、表头样式、明细表(斑马纹/新品高亮/金额百分比格式/冻结首行/列宽自适应)、
  7. 键值竖排(概览/汇总)等写入函数。金额一律以「元」浮点写入单元格并套 #,##0.00 格式,
  8. 调用方负责把原始金额值 ÷1000000 后传入(1 元 = 1000000)。
  9. """
  10. from openpyxl import Workbook
  11. from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
  12. from openpyxl.utils import get_column_letter
  13. # 配色(沿用参考项目 deca 报表风格)
  14. FILL_SECTION = PatternFill("solid", fgColor="4472C4") # 分区标题条:蓝底
  15. FILL_HEADER = PatternFill("solid", fgColor="D9E1F2") # 表头:浅蓝底
  16. FILL_ZEBRA = PatternFill("solid", fgColor="F5F8FC") # 明细隔行斑马
  17. FILL_SUBTOTAL = PatternFill("solid", fgColor="FCE4D6") # 汇总数值:浅橙底
  18. FILL_NEWROW = PatternFill("solid", fgColor="FEF2F2") # 今日新上架高亮:淡红底
  19. FONT_SECTION = Font(color="FFFFFF", bold=True, size=12) # 分区标题:白字加粗
  20. FONT_HEADER = Font(color="1F3864", bold=True) # 表头:深蓝字加粗
  21. FONT_LABEL = Font(color="1F3864", bold=True) # 键值标签
  22. _SIDE = Side(style="thin", color="D6DCE5")
  23. BORDER = Border(left=_SIDE, right=_SIDE, top=_SIDE, bottom=_SIDE)
  24. MONEY_FMT = "#,##0.00" # 金额(元)
  25. INT_FMT = "#,##0" # 计数
  26. PCT_FMT = "0.0%" # 进度(传入 0~1 小数)
  27. _ALIGN_L = Alignment(horizontal="left", vertical="center", wrap_text=True)
  28. _ALIGN_L_NOWRAP = Alignment(horizontal="left", vertical="center") # 标题/分区条:不换行,长文本自然向右溢出
  29. _ALIGN_R = Alignment(horizontal="right", vertical="center")
  30. _ALIGN_C = Alignment(horizontal="center", vertical="center", wrap_text=True)
  31. def new_workbook() -> Workbook:
  32. """新建一个不含默认 sheet 的工作簿。
  33. Returns:
  34. Workbook: 已删掉默认 Sheet 的空工作簿,后续用 add_sheet 逐个加。
  35. """
  36. wb = Workbook()
  37. wb.remove(wb.active)
  38. return wb
  39. def add_sheet(wb: Workbook, title: str):
  40. """新增一个工作表(表名超长自动截断到 31 字符)。
  41. Args:
  42. wb (Workbook): 目标工作簿。
  43. title (str): 工作表名。
  44. Returns:
  45. Worksheet: 新建的工作表。
  46. """
  47. return wb.create_sheet(title[:31])
  48. def _disp_width(text: str) -> int:
  49. """按中文占 2、其余占 1 估算字符串显示宽度。
  50. Args:
  51. text (str): 文本。
  52. Returns:
  53. int: 估算显示宽度。
  54. """
  55. return sum(2 if ord(c) > 127 else 1 for c in str(text))
  56. def _autosize(ws, headers: list, rows: list, base_col: int = 1) -> None:
  57. """按表头与数据估算并设置各列列宽(clamp 到 [8,60])。
  58. Args:
  59. ws: 工作表。
  60. headers (list): 表头列表。
  61. rows (list): 数据行(元组列表)。
  62. base_col (int): 首列列号(1 起)。Defaults to 1。
  63. """
  64. for i, h in enumerate(headers):
  65. w = _disp_width(h)
  66. for r in rows:
  67. if i < len(r) and r[i] is not None:
  68. w = max(w, _disp_width(r[i]))
  69. letter = get_column_letter(base_col + i)
  70. cur = ws.column_dimensions[letter].width or 0 # 只增不减:不覆盖调用方预设的更宽列宽
  71. ws.column_dimensions[letter].width = max(cur, min(60, max(8, w + 2)))
  72. def _apply_fmt(cell, col_type: str) -> None:
  73. """按列类型给单元格设数字格式与对齐。
  74. Args:
  75. cell: 单元格。
  76. col_type (str): 列类型 money / int / pct / text。
  77. """
  78. if col_type == "money":
  79. cell.number_format = MONEY_FMT
  80. cell.alignment = _ALIGN_R
  81. elif col_type == "int":
  82. cell.number_format = INT_FMT
  83. cell.alignment = _ALIGN_R
  84. elif col_type == "pct":
  85. cell.number_format = PCT_FMT
  86. cell.alignment = _ALIGN_R
  87. else:
  88. cell.alignment = _ALIGN_L
  89. def write_section_title(ws, row: int, ncols: int, text: str) -> int:
  90. """写一条横跨 ncols 列的分区标题条。
  91. Args:
  92. ws: 工作表。
  93. row (int): 起始行(1 起)。
  94. ncols (int): 跨列数。
  95. text (str): 标题文字。
  96. Returns:
  97. int: 下一可写行号。
  98. """
  99. # 不用合并单元格:合并区内文字无法溢出、列窄时长标题会被切。改为「给若干列铺底色的空单元格 +
  100. # 文字写在 A 列不换行」,让文字自然溢出到后面这些有色空单元格上(永远落在色条内、不被截断)。
  101. span = max(1, ncols) # 铺色跨度=调用方给定列数(同 sheet 内各标题条传同一 ncols→长度统一、严谨)
  102. for col in range(1, span + 1):
  103. ws.cell(row=row, column=col).fill = FILL_SECTION
  104. c = ws.cell(row=row, column=1, value=text)
  105. c.font = FONT_SECTION
  106. c.alignment = _ALIGN_L_NOWRAP # 不换行,长文本自然向右溢出到有色空单元格
  107. ws.row_dimensions[row].height = 22
  108. return row + 1
  109. def write_title(ws, row: int, text: str) -> int:
  110. """写一条大标题(加粗大字,不填色不合并,自然向右溢出到空列,风格贴近 deca)。
  111. Args:
  112. ws: 工作表。
  113. row (int): 行号。
  114. text (str): 标题文字。
  115. Returns:
  116. int: 下一可写行号。
  117. """
  118. c = ws.cell(row=row, column=1, value=text)
  119. c.font = Font(color="1F3864", bold=True, size=14)
  120. c.alignment = _ALIGN_L_NOWRAP
  121. return row + 1
  122. def write_kv(ws, row: int, items: list, title: str = None) -> int:
  123. """竖排写键值区(指标/数值两列),用于概览、单商家汇总。
  124. Args:
  125. ws: 工作表。
  126. row (int): 起始行。
  127. items (list): 元素为 (标签, 值, 类型) 的列表;类型取 text/money/int/pct。
  128. title (str, optional): 若给出,先写一条分区标题条。Defaults to None。
  129. Returns:
  130. int: 下一可写行号。
  131. """
  132. if title:
  133. row = write_section_title(ws, row, 2, title)
  134. for label, value, col_type in items:
  135. lc = ws.cell(row=row, column=1, value=label)
  136. lc.font = FONT_LABEL # 标签:深蓝加粗 + 浅蓝底(对齐 deca 汇总块配色)
  137. lc.fill = FILL_HEADER
  138. lc.alignment = _ALIGN_L_NOWRAP
  139. lc.border = BORDER
  140. vc = ws.cell(row=row, column=2, value=value)
  141. vc.fill = FILL_SUBTOTAL # 数值:浅橙底(对齐 deca)
  142. vc.border = BORDER
  143. _apply_fmt(vc, col_type)
  144. row += 1
  145. ws.column_dimensions["A"].width = max(ws.column_dimensions["A"].width or 8,
  146. min(40, max(_disp_width(i[0]) for i in items) + 2))
  147. ws.column_dimensions["B"].width = max(ws.column_dimensions["B"].width or 8, 20)
  148. return row + 1 # 区块后空一行
  149. def write_table(ws, row: int, headers: list, rows: list, col_types: list,
  150. new_flags: list = None, freeze: bool = True) -> int:
  151. """写一张明细表(表头样式 + 斑马纹 + 可选新品高亮 + 数字格式 + 冻结表头 + 列宽自适应)。
  152. Args:
  153. ws: 工作表。
  154. row (int): 表头起始行。
  155. headers (list): 列标题。
  156. rows (list): 数据行,每行元组,元素已是展示单位(金额传元、进度传 0~1 小数)。
  157. col_types (list): 与 headers 等长的列类型 text/money/int/pct。
  158. new_flags (list, optional): 与 rows 等长的布尔列表,True 的行套新品高亮底色。Defaults to None。
  159. freeze (bool, optional): 是否冻结表头行。Defaults to True。
  160. Returns:
  161. int: 下一可写行号。
  162. """
  163. header_row = row
  164. for i, h in enumerate(headers):
  165. c = ws.cell(row=header_row, column=1 + i, value=h)
  166. c.fill = FILL_HEADER
  167. c.font = FONT_HEADER
  168. c.alignment = _ALIGN_C
  169. c.border = BORDER
  170. ws.row_dimensions[header_row].height = 26
  171. for ri, data in enumerate(rows):
  172. r = header_row + 1 + ri
  173. is_new = bool(new_flags[ri]) if new_flags else False
  174. for i, val in enumerate(data):
  175. c = ws.cell(row=r, column=1 + i, value=val)
  176. c.border = BORDER
  177. _apply_fmt(c, col_types[i] if i < len(col_types) else "text")
  178. if is_new:
  179. c.fill = FILL_NEWROW
  180. elif ri % 2 == 1:
  181. c.fill = FILL_ZEBRA
  182. _autosize(ws, headers, rows)
  183. if freeze:
  184. ws.freeze_panes = f"A{header_row + 1}"
  185. return header_row + 1 + len(rows) + 1 # 表后空一行
  186. def save(wb: Workbook, path: str) -> None:
  187. """保存工作簿到磁盘。
  188. Args:
  189. wb (Workbook): 工作簿。
  190. path (str): 输出文件路径。
  191. """
  192. wb.save(path)