feat: 多工厂/厂间调拨/替代料/流水台账折叠 + 重写 README

- 多工厂(factory 表) + 顶栏工厂下拉过滤, 库存按厂聚合
- 厂间调拨(transfer): 源厂减+目标厂加, 总库存不变, 双向流水对账
- 替代料关系(substitute): 出库/BOM领料/生产领料缺料时提示可替代料
- 流水台账(ledger): 入出按日期合并折叠, 按日期导出同一 Excel
- 生产订单增强: 编辑/删除(级联清关联流水)/调入数量/确认调入=厂间调拨
- 入库新增送货日期字段; 出库物料编码扫码定位
- 重写 README 补全全部模块; .gitignore 排除 dist_pkg/build_pkg/*.db
- 停止跟踪 instance/app.db(运行时自动生成)
This commit is contained in:
2026-09-29 11:17:48 +08:00
parent 33efc7c846
commit 5060d027e5
22 changed files with 1961 additions and 149 deletions
+45 -3
View File
@@ -118,7 +118,7 @@ def read_materials(path):
return out
def import_materials(rows, operator="导入"):
def import_materials(rows, operator="导入", factory_id=None):
"""upsert 物料; opening_stock>0 写入一条"期初"入库流水。返回 (新增, 更新)"""
added = 0
updated = 0
@@ -138,11 +138,12 @@ def import_materials(rows, operator="导入"):
op = d.get("opening_stock", 0) or 0
if op > 0:
exists = db.session.scalar(
db.select(Inbound).where(Inbound.material_id == m.id, Inbound.source == "期初"))
db.select(Inbound).where(Inbound.material_id == m.id, Inbound.source == "期初",
Inbound.factory_id == factory_id))
if exists:
exists.qty = op
else:
db.session.add(Inbound(material_id=m.id, qty=op, source="期初",
db.session.add(Inbound(material_id=m.id, factory_id=factory_id, qty=op, source="期初",
occur_date=date.today(), operator=operator))
db.session.commit()
return added, updated
@@ -256,6 +257,47 @@ def build_material_template(path):
return path
def export_ledger(target, rows):
"""导出流水台账。target 可为路径或文件对象(如 BytesIO)。
rows: _build_ledger_rows() 返回的流水行列表, 每项含
date/type/cat/m/qty/fac/who/ref/op 字段。
"""
wb = Workbook()
ws = wb.active
ws.title = "流水台账"
head = ["日期", "类型", "业务类型", "物料编号", "型号/规格", "单位",
"数量", "工厂", "来源/用途", "调拨/关联单号", "经手人"]
ws.append(head)
for c in ws[1]:
c.font = Font(bold=True)
c.fill = PatternFill("solid", fgColor="DDEBF7")
for r in rows:
m = r["m"]
ws.append([
r["date"].isoformat() if r["date"] else "",
r["type"],
r["cat"],
m.part_number if m else "",
m.manufacturer if m else "",
m.unit if m else "",
r["qty"],
r["fac"].code if r["fac"] else "",
r["who"] or "",
r["ref"] or "",
r["op"] or "",
])
# 出入着色: 入(绿) / 出(红)
for i in range(2, ws.max_row + 1):
tval = ws.cell(row=i, column=2).value
if tval == "入":
ws.cell(row=i, column=2).fill = PatternFill("solid", fgColor="C6EFCE")
elif tval == "出":
ws.cell(row=i, column=2).fill = PatternFill("solid", fgColor="FFC7CE")
ws.freeze_panes = "A2"
wb.save(target)
def build_bom_template(path):
"""生成与真实 BOM 模板一致的格式(Altium/嘉立创导出):
每个 worksheet 一个 BOM, sheet 名即 BOM 型号; 列头 Item/Part Number/