web.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. from flask import Flask, render_template, request, jsonify, send_file
  2. import pandas as pd
  3. from openpyxl import load_workbook
  4. import os
  5. import shutil
  6. from datetime import datetime
  7. import re
  8. from collections import Counter
  9. app = Flask(__name__)
  10. app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
  11. # 配置路径
  12. BASE_DIR = os.path.dirname(os.path.abspath(__file__))
  13. UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
  14. TEMPLATES_FOLDER = os.path.join(BASE_DIR, 'templates_excel')
  15. OUTPUT_FOLDER = os.path.join(BASE_DIR, 'output')
  16. # 创建必要的目录
  17. for folder in [UPLOAD_FOLDER, TEMPLATES_FOLDER, OUTPUT_FOLDER]:
  18. if not os.path.exists(folder):
  19. os.makedirs(folder)
  20. def load_valid_values(wb):
  21. """从 Valid Values 工作表读取有效值"""
  22. valid_data = {}
  23. if "Valid Values" not in wb.sheetnames:
  24. return valid_data
  25. ws_valid = wb["Valid Values"]
  26. # 读取第1行作为列名
  27. headers = []
  28. for col in range(1, ws_valid.max_column + 1):
  29. val = ws_valid.cell(row=1, column=col).value
  30. if val:
  31. headers.append(str(val).strip())
  32. else:
  33. headers.append(f"Column_{col}")
  34. # 读取数据行
  35. for row in range(2, ws_valid.max_row + 1):
  36. for col in range(1, len(headers) + 1):
  37. val = ws_valid.cell(row=row, column=col).value
  38. if val:
  39. header = headers[col - 1]
  40. if header not in valid_data:
  41. valid_data[header] = []
  42. if str(val).strip() not in valid_data[header]:
  43. valid_data[header].append(str(val).strip())
  44. return valid_data
  45. def parse_excel_file(file_path, source_header_row):
  46. """读取来源Excel"""
  47. df = pd.read_excel(file_path, header=source_header_row - 1)
  48. return df
  49. def get_template_list():
  50. """获取所有可用模板"""
  51. templates = []
  52. for f in os.listdir(TEMPLATES_FOLDER):
  53. if f.endswith('.xlsx') or f.endswith('.xls'):
  54. templates.append(f)
  55. return templates
  56. @app.route('/')
  57. def index():
  58. templates = get_template_list()
  59. return render_template('index.html', templates=templates)
  60. @app.route('/get_templates')
  61. def get_templates():
  62. return jsonify({'templates': get_template_list()})
  63. @app.route('/preview', methods=['POST'])
  64. def preview():
  65. """预览来源数据"""
  66. try:
  67. source_file = request.files.get('source_file')
  68. template_name = request.form.get('template_name')
  69. source_header = int(request.form.get('source_header', 2))
  70. if not source_file:
  71. return jsonify({'error': '请选择来源文件'}), 400
  72. if not template_name:
  73. return jsonify({'error': '请选择模板'}), 400
  74. # 保存来源文件
  75. timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
  76. source_path = os.path.join(UPLOAD_FOLDER, f'source_{timestamp}_{source_file.filename}')
  77. source_file.save(source_path)
  78. # 读取来源数据
  79. df = pd.read_excel(source_path, header=source_header - 1)
  80. # 预览前10行
  81. preview_data = df.head(10).fillna('').to_dict('records')
  82. columns = df.columns.tolist()
  83. return jsonify({
  84. 'success': True,
  85. 'columns': columns,
  86. 'data': preview_data,
  87. 'total_rows': len(df),
  88. 'source_path': source_path
  89. })
  90. except Exception as e:
  91. return jsonify({'error': str(e)}), 500
  92. @app.route('/generate', methods=['POST'])
  93. def generate():
  94. """生成并下载"""
  95. try:
  96. source_path = request.form.get('source_path')
  97. template_name = request.form.get('template_name')
  98. source_header = int(request.form.get('source_header', 2))
  99. header_row = int(request.form.get('header_row', 4))
  100. if not source_path or not os.path.exists(source_path):
  101. return jsonify({'error': '来源文件不存在'}), 400
  102. if not template_name:
  103. return jsonify({'error': '请选择模板'}), 400
  104. template_path = os.path.join(TEMPLATES_FOLDER, template_name)
  105. if not os.path.exists(template_path):
  106. return jsonify({'error': '模板文件不存在'}), 400
  107. # ========== 读取来源数据 ==========
  108. df = pd.read_excel(source_path, header=source_header - 1)
  109. # ========== 加载模板 ==========
  110. wb = load_workbook(template_path)
  111. ws = wb.active
  112. # 加载 Valid Values
  113. valid_values = load_valid_values(wb)
  114. # ========== 读取模板表头 ==========
  115. col_indices = {}
  116. for col in range(1, ws.max_column + 1):
  117. cell_value = ws.cell(row=header_row, column=col).value
  118. if cell_value:
  119. col_name = str(cell_value).strip()
  120. if col_name and not col_name.startswith("Unnamed"):
  121. col_indices[col_name] = col
  122. # ========== 找到已有数据的最后一行 ==========
  123. last_row = 5
  124. for row in range(5, ws.max_row + 2):
  125. is_empty = True
  126. for col in range(1, min(20, ws.max_column + 1)):
  127. if ws.cell(row=row, column=col).value is not None:
  128. is_empty = False
  129. break
  130. if is_empty:
  131. last_row = row
  132. break
  133. else:
  134. last_row = ws.max_row + 1
  135. # ========== 匹配来源列 ==========
  136. source_mapping = {
  137. "Item Code": "Item Code",
  138. "产品名称": "产品名称",
  139. "优惠单价": "优惠单价",
  140. "产品重量": "产品重量(磅)",
  141. "包装尺寸-重量": "包装尺寸-重量(磅)",
  142. "包装尺寸-高度": "包装尺寸-高度(英寸)",
  143. "包装尺寸-宽度": "包装尺寸-宽度(英寸)",
  144. "包装尺寸-长度": "包装尺寸-长度(英寸)",
  145. "颜色": "颜色",
  146. "材质": "材质",
  147. "组装长度": "组装长度(英寸)",
  148. "组装高度": "组装高度(英寸)",
  149. "组装宽度": "组装宽度(英寸)",
  150. "产品类目": "产品类目",
  151. }
  152. source_map = {}
  153. for key, expected in source_mapping.items():
  154. found = False
  155. for col in df.columns:
  156. if col == expected:
  157. source_map[key] = col
  158. found = True
  159. break
  160. if not found:
  161. source_map[key] = None
  162. # ========== 默认值映射 ==========
  163. default_mapping = {
  164. "Variant Type": ("fixed", "Not Variant"),
  165. "Minimum Order Quantity": ("fixed", 1),
  166. "Force Quantity Multiplier": ("fixed", 1),
  167. "Display Set Quantity": ("fixed", 1),
  168. "Ship Type": ("valid", "Ship Type"),
  169. "Lead Time": ("fixed", 48),
  170. "Replacement Lead Time": ("fixed", 120),
  171. "Flat Pack": ("fixed", "Yes"),
  172. "Assembly Required": ("fixed", "Yes"),
  173. "Canada Product Restriction": ("fixed", "Yes"),
  174. "CARB Phase II Compliant (formaldehyde emissions)": ("fixed", "Yes"),
  175. "Commercial Warranty": ("fixed", "Yes"),
  176. "CANFER Compliant": ("fixed", "Yes"),
  177. "Commercial Warranty Length": ("fixed", "1 Years"),
  178. "Composite Wood Product (CWP)": ("fixed", "Yes"),
  179. "Country Of Manufacturer": ("fixed", "China"),
  180. "General Certificate of Conformity (GCC)": ("fixed", "Yes"),
  181. "Hazard Class(es)": ("fixed", "Does Not Apply"),
  182. "Hazardous Material / Dangerous Goods": ("fixed", "No"),
  183. "Hazardous Material Weight": ("fixed", "Does Not Apply"),
  184. "Level of Assembly": ("fixed", "Full Assembly Needed"),
  185. "Packing Group": ("fixed", "Does Not Apply"),
  186. "Reason for Restriction": ("fixed", "Does Not Apply"),
  187. "Supplier Intended and Approved Use": ("fixed", "Residential Use"),
  188. "TSCA Title VI Compliant (formaldehyde emissions)": ("fixed", "Yes"),
  189. "UN or ID number": ("fixed", "Does Not Apply"),
  190. "Uniform Packaging and Labeling Regulations (UPLR) Compliant": ("fixed", "Yes"),
  191. "Warning Required": ("fixed", "No"),
  192. "Warranty Length": ("fixed", "1 Years"),
  193. "Battery or Batteries Included": ("fixed", "No"),
  194. "Product Type": ("source", "产品类目"),
  195. "Brand": ("valid", "Brand"),
  196. }
  197. # ========== 填充数据 ==========
  198. def get_key(row):
  199. item = row.get("Item Code", "")
  200. name = row.get("产品名称", "")
  201. return f"{item}_{name}"
  202. keys = [get_key(row) for _, row in df.iterrows()]
  203. key_count = Counter(keys)
  204. key_index = Counter()
  205. row_num = last_row
  206. filled_count = 0
  207. for idx, (_, src_row) in enumerate(df.iterrows()):
  208. item_code = src_row.get("Item Code", "")
  209. product_name = src_row.get("产品名称", "")
  210. if pd.isna(item_code):
  211. item_code = ""
  212. if pd.isna(product_name):
  213. product_name = ""
  214. key = get_key(src_row)
  215. if key_count[key] > 1:
  216. key_index[key] += 1
  217. suffix = f".{key_index[key] - 1}"
  218. mfr_suffix = f".{key_index[key]}"
  219. else:
  220. suffix = ""
  221. mfr_suffix = ""
  222. fill_count = 0
  223. # Supplier Part Number
  224. if "Supplier Part Number" in col_indices:
  225. val = f"{item_code}{product_name}{suffix}"
  226. ws.cell(row=row_num, column=col_indices["Supplier Part Number"], value=val)
  227. fill_count += 1
  228. # Manufacturer Part Number
  229. if "Manufacturer Part Number" in col_indices:
  230. val = f"{item_code}{product_name}{mfr_suffix}"
  231. ws.cell(row=row_num, column=col_indices["Manufacturer Part Number"], value=val)
  232. fill_count += 1
  233. # Base Cost
  234. if "Base Cost" in col_indices and source_map.get("优惠单价"):
  235. col = source_map["优惠单价"]
  236. val = src_row.get(col, "")
  237. if pd.notna(val):
  238. ws.cell(row=row_num, column=col_indices["Base Cost"], value=val)
  239. fill_count += 1
  240. # MSRP
  241. if "Manufacturer Suggested Retail Price" in col_indices and source_map.get("优惠单价"):
  242. col = source_map["优惠单价"]
  243. val = src_row.get(col, 0)
  244. try:
  245. num = float(val) if pd.notna(val) else 0
  246. ws.cell(row=row_num, column=col_indices["Manufacturer Suggested Retail Price"], value=num + 70)
  247. fill_count += 1
  248. except:
  249. pass
  250. # Product Weight
  251. if "Product Weight" in col_indices and source_map.get("产品重量"):
  252. col = source_map["产品重量"]
  253. val = src_row.get(col, "")
  254. if pd.notna(val):
  255. ws.cell(row=row_num, column=col_indices["Product Weight"], value=val)
  256. fill_count += 1
  257. # Carton
  258. carton_map = [
  259. ("Carton Weight 1", "包装尺寸-重量"),
  260. ("Carton Height 1", "包装尺寸-高度"),
  261. ("Carton Width 1", "包装尺寸-宽度"),
  262. ("Carton Depth 1", "包装尺寸-长度"),
  263. ]
  264. for target, src_name in carton_map:
  265. if target in col_indices and source_map.get(src_name):
  266. col = source_map[src_name]
  267. val = src_row.get(col, "")
  268. if pd.notna(val):
  269. ws.cell(row=row_num, column=col_indices[target], value=val)
  270. fill_count += 1
  271. # Color/Material
  272. cm_map = [
  273. ("Base Color", "颜色"),
  274. ("Base Material", "材质"),
  275. ("Color", "颜色"),
  276. ("Material", "材质"),
  277. ("Top Color", "颜色"),
  278. ("Top Material", "材质"),
  279. ]
  280. for target, src_name in cm_map:
  281. if target in col_indices and source_map.get(src_name):
  282. col = source_map[src_name]
  283. val = src_row.get(col, "")
  284. if pd.notna(val):
  285. ws.cell(row=row_num, column=col_indices[target], value=val)
  286. fill_count += 1
  287. # Overall
  288. overall_map = [
  289. ("Overall Depth - Front to Back", "组装长度"),
  290. ("Overall Height - Top to Bottom", "组装高度"),
  291. ("Overall Product Weight", "产品重量"),
  292. ("Overall Width - Side to Side", "组装宽度"),
  293. ]
  294. for target, src_name in overall_map:
  295. if target in col_indices and source_map.get(src_name):
  296. col = source_map[src_name]
  297. val = src_row.get(col, "")
  298. if pd.notna(val):
  299. ws.cell(row=row_num, column=col_indices[target], value=val)
  300. fill_count += 1
  301. # 默认值字段
  302. variant_type = "Not Variant"
  303. for target_col, (source_type, source_val) in default_mapping.items():
  304. if target_col not in col_indices:
  305. continue
  306. col_idx = col_indices[target_col]
  307. if source_type == "fixed":
  308. ws.cell(row=row_num, column=col_idx, value=source_val)
  309. fill_count += 1
  310. elif source_type == "valid":
  311. if source_val in valid_values and valid_values[source_val]:
  312. val = valid_values[source_val][0]
  313. ws.cell(row=row_num, column=col_idx, value=val)
  314. fill_count += 1
  315. else:
  316. ws.cell(row=row_num, column=col_idx, value="Not Variant")
  317. fill_count += 1
  318. elif source_type == "source":
  319. if source_map.get(source_val):
  320. col = source_map[source_val]
  321. val = src_row.get(col, "")
  322. if pd.notna(val):
  323. ws.cell(row=row_num, column=col_idx, value=val)
  324. fill_count += 1
  325. # Variant Grouping 特殊逻辑
  326. if "Variant Grouping 1" in col_indices:
  327. if variant_type != "Not Variant":
  328. ws.cell(row=row_num, column=col_indices["Variant Grouping 1"], value="Color")
  329. fill_count += 1
  330. if "Variant Attribute Name On Site 1" in col_indices:
  331. if variant_type != "Not Variant":
  332. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 1"], value="Finish")
  333. fill_count += 1
  334. if "Variant Grouping 2" in col_indices:
  335. if variant_type != "Not Variant":
  336. ws.cell(row=row_num, column=col_indices["Variant Grouping 2"], value="Size")
  337. fill_count += 1
  338. if "Variant Attribute Name On Site 2" in col_indices:
  339. if variant_type != "Not Variant":
  340. ws.cell(row=row_num, column=col_indices["Variant Attribute Name On Site 2"], value="Dimensions")
  341. fill_count += 1
  342. row_num += 1
  343. filled_count += 1
  344. # ========== 保存输出文件 ==========
  345. output_filename = f"output_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
  346. output_path = os.path.join(OUTPUT_FOLDER, output_filename)
  347. wb.save(output_path)
  348. # 清理上传文件
  349. try:
  350. os.remove(source_path)
  351. except:
  352. pass
  353. return jsonify({
  354. 'success': True,
  355. 'filled_count': filled_count,
  356. 'download_url': f'/download/{output_filename}',
  357. 'message': f'成功追加 {filled_count} 行数据'
  358. })
  359. except Exception as e:
  360. return jsonify({'error': str(e)}), 500
  361. @app.route('/download/<filename>')
  362. def download(filename):
  363. """下载生成的文件"""
  364. file_path = os.path.join(OUTPUT_FOLDER, filename)
  365. if not os.path.exists(file_path):
  366. return '文件不存在', 404
  367. return send_file(file_path, as_attachment=True, download_name=filename)
  368. if __name__ == '__main__':
  369. # 确保有默认模板
  370. default_template = os.path.join(TEMPLATES_FOLDER, '模板.xlsx')
  371. if not os.path.exists(default_template):
  372. # 创建空模板提示
  373. print(f'⚠️ 请将模板文件放入 {TEMPLATES_FOLDER} 目录')
  374. app.run(host='0.0.0.0', port=5000, debug=True)