feat: 生产订单支持一次多选 BOM 且各 BOM 独立设置生产数量

- 新增中间表 prod_order_bom(order_id, bom_id, qty),一个订单可挂多个 BOM,每个 BOM 单独设生产数量
- prod_order.qty 改为各 BOM 数量汇总,bom_id 仅作主 BOM 冗余摘要
- 物料需求按 Σ(BOM 单套用量 × 各自生产数量) 跨 BOM 合并聚合,覆盖详情/领料/调入/导出
- 订单表单改为动态多行(BOM 下拉 + 数量输入 + 删除行),列表与导出展示「BOM清单(型号×数量)」
- 导出 Excel 增至 3 个 sheet:订单信息 / BOM清单 / 领料明细
- 修复 _parse_bom_pairs 表单字段名 bom_id[]/qty[] 解析;order_delete 一并级联删除 prod_order_bom
- build_exe.py 增加构建后自动拷贝 dist_pkg→dist(EXE 同目录即数据库所在)

Co-Authored-By: WorkBuddy <workbuddy@tencent.com>
This commit is contained in:
2026-10-26 10:24:25 +08:00
co-authored by WorkBuddy
parent 099496dec9
commit 93f3ef458b
7 changed files with 279 additions and 121 deletions
+41 -21
View File
@@ -330,53 +330,62 @@ def export_ledger(target, rows):
def export_orders(target, data):
"""导出生产订单列表。
data: [{o, bom, factory}] (与 orders() 返回一致)。
data: [{o, lines, factory, total}] (与 orders() 返回一致)。
lines: [(Bom, qty)] 该订单的 BOM 行(可多个)。
"""
wb = Workbook()
ws = wb.active
ws.title = "生产订单"
head = ["订单号", "BOM型号", "BOM名称", "接单量", "生产工厂", "状态",
"经手人", "下单时间", "备注"]
head = ["订单号", "BOM清单(型号×数量)", "生产工厂", "状态",
"经手人", "下单时间", "备注", "总接单量"]
ws.append(head)
for c in ws[1]:
c.font = Font(bold=True)
c.fill = PatternFill("solid", fgColor="DDEBF7")
for d in data:
o = d["o"]
bom = d["bom"]
lines = d.get("lines") or []
fac = d["factory"]
total = d.get("total") or 0
bom_info = "\n".join(
f"{b.code}{('·' + b.name) if b.name else ''}×{q}" for b, q in lines) or "—"
status = "已领料" if o.status == "picked" else "待领料"
ws.append([
o.order_no,
bom.code if bom else "",
bom.name if bom else "",
o.qty,
bom_info,
(f"{fac.code}·{fac.name}" if fac else "未指定"),
status,
o.operator or "",
o.created_at.strftime("%Y-%m-%d %H:%M") if o.created_at else "",
o.remark or "",
total,
])
if o.status == "picked":
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="C6EFCE")
ws.cell(row=ws.max_row, column=4).fill = PatternFill("solid", fgColor="C6EFCE")
else:
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="FFE699")
ws.cell(row=ws.max_row, column=4).fill = PatternFill("solid", fgColor="FFE699")
ws.cell(row=ws.max_row, column=2).alignment = Alignment(wrap_text=True, vertical="top")
ws.freeze_panes = "A2"
wb.save(target)
def export_order_detail(target, o, bom, reqs, factory,
def export_order_detail(target, o, lines, reqs, factory,
posted_total, planned_total, posted_count):
"""导出单个生产订单详情(派工单):订单信息 sheet + 领料明细 sheet。"""
"""导出单个生产订单详情(派工单):订单信息 sheet + BOM清单 sheet + 领料明细 sheet。
lines: [(Bom, qty)] 该订单的 BOM 行(可多个); 领料明细已按全部 BOM 汇总。
"""
wb = Workbook()
ws = wb.active
ws.title = "订单信息"
ws.append(["生产订单派工单"])
ws["A1"].font = Font(bold=True, size=14)
total_qty = sum(q for _, q in (lines or []))
info = [
("订单号", o.order_no),
("BOM型号", (bom.code if bom else "-") + (f" · {bom.name}" if bom and bom.name else "")),
("接单量", o.qty),
("BOM清单", "\n".join(
f"{b.code}{('·' + b.name) if b.name else ''}×{q}" for b, q in (lines or [])) or "-"),
("总接单量", total_qty),
("生产工厂", (f"{factory.code}·{factory.name}" if factory else "未指定(按汇总库存)")),
("经手人", o.operator or "-"),
("下单时间", o.created_at.strftime("%Y-%m-%d %H:%M") if o.created_at else ""),
@@ -387,21 +396,32 @@ def export_order_detail(target, o, bom, reqs, factory,
r = 3
for k, v in info:
ws.cell(row=r, column=1, value=k).font = Font(bold=True)
ws.cell(row=r, column=2, value=v)
cell = ws.cell(row=r, column=2, value=v)
if k == "BOM清单":
cell.alignment = Alignment(wrap_text=True, vertical="top")
r += 1
ws2 = wb.create_sheet("领料明细")
ws2 = wb.create_sheet("BOM清单")
ws2.append(["BOM型号", "BOM名称", "生产数量"])
for c in ws2[1]:
c.font = Font(bold=True)
c.fill = PatternFill("solid", fgColor="DDEBF7")
for b, q in (lines or []):
ws2.append([b.code, b.name or "", q])
ws2.freeze_panes = "A2"
ws3 = wb.create_sheet("领料明细")
head = ["物料编号", "型号", "下单前库存", "需求数量", "缺口",
"调入数量", "状态", "工厂库存", "可替代料"]
ws2.append(head)
for c in ws2[1]:
ws3.append(head)
for c in ws3[1]:
c.font = Font(bold=True)
c.fill = PatternFill("solid", fgColor="DDEBF7")
for x in reqs:
subs = "、".join(
f"{s['pn']}·{s['stock']}{'✓' if s['available'] else '✗'}" for s in x["subs"]
) if x["subs"] else "—"
ws2.append([
ws3.append([
x["m"].part_number,
x["m"].manufacturer or "-",
round(x["have"], 3),
@@ -413,10 +433,10 @@ def export_order_detail(target, o, bom, reqs, factory,
subs,
])
if x["short"]:
ws2.cell(row=ws2.max_row, column=7).fill = PatternFill("solid", fgColor="FFC7CE")
ws3.cell(row=ws3.max_row, column=7).fill = PatternFill("solid", fgColor="FFC7CE")
else:
ws2.cell(row=ws2.max_row, column=7).fill = PatternFill("solid", fgColor="C6EFCE")
ws2.freeze_panes = "A2"
ws3.cell(row=ws3.max_row, column=7).fill = PatternFill("solid", fgColor="C6EFCE")
ws3.freeze_panes = "A2"
wb.save(target)