Compare commits
10
Commits
5060d027e5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27fb218c40 | ||
|
|
38ee24f6ee | ||
|
|
93f3ef458b | ||
|
|
099496dec9 | ||
|
|
5a6f4ea4df | ||
|
|
e9b605fca6 | ||
|
|
639d7f1545 | ||
|
|
012e018b12 | ||
|
|
882508aee9 | ||
|
|
574d608e11 |
+11
-1
@@ -32,4 +32,14 @@ args = [
|
|||||||
]
|
]
|
||||||
print("PyInstaller args:", args)
|
print("PyInstaller args:", args)
|
||||||
run(args)
|
run(args)
|
||||||
print("BUILD DONE ->", os.path.join(HERE, "dist", "MaterialWMS.exe"))
|
# 构建产物在 dist_pkg/; 统一拷贝到 dist/ (EXE 同目录即数据库所在, 才是实际发布入口)
|
||||||
|
built = os.path.join(HERE, "dist_pkg", "MaterialWMS.exe")
|
||||||
|
target = os.path.join(HERE, "dist", "MaterialWMS.exe")
|
||||||
|
if os.path.exists(built):
|
||||||
|
import shutil
|
||||||
|
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||||
|
shutil.copyfile(built, target)
|
||||||
|
print("COPIED ->", target)
|
||||||
|
else:
|
||||||
|
print("WARN: built exe not found at", built)
|
||||||
|
print("BUILD DONE ->", target)
|
||||||
|
|||||||
+142
@@ -245,6 +245,35 @@ def export_inventory(path, rows):
|
|||||||
wb.save(path)
|
wb.save(path)
|
||||||
|
|
||||||
|
|
||||||
|
def export_prep(path, rows, scope=""):
|
||||||
|
"""导出备料清单(采购下单用, 不记账; 手工维护, 与生产订单无关)。
|
||||||
|
|
||||||
|
rows: [{id, m, plan(计划数量), stock(实际库存), diff(缺口=计划-库存), short}]
|
||||||
|
scope: 范围说明(工厂/全部)。
|
||||||
|
"""
|
||||||
|
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"]
|
||||||
|
status = "需采购" if r["diff"] > 0 else ("刚好够" if r["diff"] == 0 else "已有盈余")
|
||||||
|
ws.append([m.part_number, m.manufacturer or "", m.unit or "个",
|
||||||
|
r["plan"], r["stock"], r["diff"], status, scope])
|
||||||
|
# 缺口着色: >0 需采购标红; <0 盈余标绿
|
||||||
|
if r["diff"] > 0:
|
||||||
|
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="FFC7CE")
|
||||||
|
elif r["diff"] < 0:
|
||||||
|
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="C6EFCE")
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
wb.save(path)
|
||||||
|
|
||||||
|
|
||||||
def build_material_template(path):
|
def build_material_template(path):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
@@ -298,6 +327,119 @@ def export_ledger(target, rows):
|
|||||||
wb.save(target)
|
wb.save(target)
|
||||||
|
|
||||||
|
|
||||||
|
def export_orders(target, data):
|
||||||
|
"""导出生产订单列表。
|
||||||
|
|
||||||
|
data: [{o, lines, factory, total}] (与 orders() 返回一致)。
|
||||||
|
lines: [(Bom, qty)] 该订单的 BOM 行(可多个)。
|
||||||
|
"""
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "生产订单"
|
||||||
|
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"]
|
||||||
|
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_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=4).fill = PatternFill("solid", fgColor="C6EFCE")
|
||||||
|
else:
|
||||||
|
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, lines, reqs, factory,
|
||||||
|
posted_total, planned_total, posted_count):
|
||||||
|
"""导出单个生产订单详情(派工单):订单信息 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清单", "\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 ""),
|
||||||
|
("状态", "已领料" if o.status == "picked" else "待领料"),
|
||||||
|
("备注", o.remark or "-"),
|
||||||
|
("调入总览", f"已调入 {posted_count} 项 / 合计 {posted_total} 件;计划调入合计 {planned_total} 件"),
|
||||||
|
]
|
||||||
|
r = 3
|
||||||
|
for k, v in info:
|
||||||
|
ws.cell(row=r, column=1, value=k).font = Font(bold=True)
|
||||||
|
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("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 = ["物料编号", "型号", "下单前库存", "需求数量", "缺口",
|
||||||
|
"调入数量", "状态", "工厂库存", "可替代料"]
|
||||||
|
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 "—"
|
||||||
|
ws3.append([
|
||||||
|
x["m"].part_number,
|
||||||
|
x["m"].manufacturer or "-",
|
||||||
|
round(x["have"], 3),
|
||||||
|
round(x["need"], 3),
|
||||||
|
round(x["gap"], 3),
|
||||||
|
round(x["alloc"], 3),
|
||||||
|
"缺料" if x["short"] else "充足",
|
||||||
|
round(x["gap"] + x["alloc"], 3),
|
||||||
|
subs,
|
||||||
|
])
|
||||||
|
if x["short"]:
|
||||||
|
ws3.cell(row=ws3.max_row, column=7).fill = PatternFill("solid", fgColor="FFC7CE")
|
||||||
|
else:
|
||||||
|
ws3.cell(row=ws3.max_row, column=7).fill = PatternFill("solid", fgColor="C6EFCE")
|
||||||
|
ws3.freeze_panes = "A2"
|
||||||
|
wb.save(target)
|
||||||
|
|
||||||
|
|
||||||
def build_bom_template(path):
|
def build_bom_template(path):
|
||||||
"""生成与真实 BOM 模板一致的格式(Altium/嘉立创导出):
|
"""生成与真实 BOM 模板一致的格式(Altium/嘉立创导出):
|
||||||
每个 worksheet 一个 BOM, sheet 名即 BOM 型号; 列头 Item/Part Number/
|
每个 worksheet 一个 BOM, sheet 名即 BOM 型号; 列头 Item/Part Number/
|
||||||
|
|||||||
@@ -118,6 +118,21 @@ class ProdOrderAlloc(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
class ProdOrderBom(db.Model):
|
||||||
|
"""生产订单包含的 BOM 行: 一个订单可含多个 BOM, 每个 BOM 独立的生产数量。
|
||||||
|
|
||||||
|
- 这是「一订单多 BOM」的唯一数据源, 取代原 ProdOrder.bom_id 的单值绑定。
|
||||||
|
- (order_id, bom_id) 唯一: 同一 BOM 在同一订单中只出现一行(数量可累加合并)。
|
||||||
|
- ProdOrder.bom_id / qty 仅作为兼容旧数据与列表快速展示的冗余(主BOM / 总接单量)。
|
||||||
|
"""
|
||||||
|
__tablename__ = "prod_order_bom"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
order_id = db.Column(db.Integer, db.ForeignKey("prod_order.id"), nullable=False, index=True)
|
||||||
|
bom_id = db.Column(db.Integer, db.ForeignKey("bom.id"), nullable=False, index=True)
|
||||||
|
qty = db.Column(db.Float, nullable=False, default=0) # 该 BOM 在本订单的生产数量
|
||||||
|
__table_args__ = (UniqueConstraint("order_id", "bom_id", name="uq_pob_order_bom"),)
|
||||||
|
|
||||||
|
|
||||||
class Substitute(db.Model):
|
class Substitute(db.Model):
|
||||||
"""替代料关系: 某主料(main)可由某替代料(sub)替代使用。
|
"""替代料关系: 某主料(main)可由某替代料(sub)替代使用。
|
||||||
|
|
||||||
@@ -157,6 +172,25 @@ class Factory(db.Model):
|
|||||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 备料计划数量(人工录入, 不记账) ----------------
|
||||||
|
class PrepPlan(db.Model):
|
||||||
|
"""备料「计划数量」人工录入(不记账): 每个物料在某工厂范围下的计划采购量。
|
||||||
|
|
||||||
|
- (material_id, factory_id) 组合唯一; factory_id 为 NULL 表示「全部工厂(汇总)」范围的计划。
|
||||||
|
- plan_qty 为 NULL 表示「尚未填写」; 0 表示明确计划不采购。
|
||||||
|
- 仅用于采购下单参考, 不产生任何入库/出库流水。
|
||||||
|
"""
|
||||||
|
__tablename__ = "prep_plan"
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
material_id = db.Column(db.Integer, db.ForeignKey("material.id"), nullable=False, index=True)
|
||||||
|
factory_id = db.Column(db.Integer, db.ForeignKey("factory.id"), nullable=True, index=True)
|
||||||
|
plan_qty = db.Column(db.Float, nullable=True, default=None) # 计划采购数量(空=未填写)
|
||||||
|
note = db.Column(db.String(255), default="")
|
||||||
|
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||||
|
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
|
__table_args__ = (UniqueConstraint("material_id", "factory_id", name="uq_prep_plan_mf"),)
|
||||||
|
|
||||||
|
|
||||||
# ---------------- M2-2: 厂间调拨 ----------------
|
# ---------------- M2-2: 厂间调拨 ----------------
|
||||||
class Transfer(db.Model):
|
class Transfer(db.Model):
|
||||||
"""厂间调拨: 从源工厂调出、调入目标工厂(同一颗物料)。
|
"""厂间调拨: 从源工厂调出、调入目标工厂(同一颗物料)。
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================
|
||||||
|
REM 一键推送本地提交到 Gitea
|
||||||
|
REM 使用前提:本机能访问公司内网 / 已连 VPN,能解析 git.btw-ocr.com
|
||||||
|
REM Gitea 凭据由系统凭据助手(helper-selector / GCM)提供,无需手填
|
||||||
|
REM ============================================================
|
||||||
|
cd /d "D:\2026-09-10-task-20\material_wms"
|
||||||
|
|
||||||
|
echo [1/3] 拉取远程最新...
|
||||||
|
git fetch origin
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo fetch 失败:请确认已连内网/VPN,且能访问 git.btw-ocr.com
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [2/3] 将本地提交 rebase 到远程 main 之上...
|
||||||
|
git rebase origin/main
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo rebase 冲突:请手动解决冲突后 git rebase --continue,或运行 git rebase --abort 撤销
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [3/3] 推送到 Gitea...
|
||||||
|
git push origin main
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo push 失败,请检查凭据或网络后重试
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ===== 推送完成 =====
|
||||||
|
echo 可在 https://git.btw-ocr.com/star/Inventory-Management-System 核对最新提交
|
||||||
|
pause
|
||||||
+17
-2
@@ -44,12 +44,17 @@ label { display: block; margin: 10px 0 4px; font-size: 13px; color: #44525f; }
|
|||||||
input, select, textarea {
|
input, select, textarea {
|
||||||
width: 100%; padding: 8px 10px; border: 1px solid #cdd5df; border-radius: 6px; font-size: 14px;
|
width: 100%; padding: 8px 10px; border: 1px solid #cdd5df; border-radius: 6px; font-size: 14px;
|
||||||
}
|
}
|
||||||
|
.cell-inp { width: 100%; box-sizing: border-box; padding: 4px 6px; border: 1px solid #c9d2dc; border-radius: 4px; font-size: 13px; }
|
||||||
|
.cell-inp.num { text-align: right; }
|
||||||
|
.readonly-field { width: 100%; box-sizing: border-box; padding: 8px 10px; border: 1px solid #dfe3e8; border-radius: 6px; background: #f1f3f6; color: #6b7280; font-size: 14px; }
|
||||||
|
.btn.small, button.small { padding: 5px 10px; font-size: 12px; border-radius: 5px; }
|
||||||
button, .btn {
|
button, .btn {
|
||||||
display: inline-block; cursor: pointer; border: none; border-radius: 6px;
|
display: inline-block; cursor: pointer; border: none; border-radius: 6px;
|
||||||
padding: 9px 16px; font-size: 14px; text-decoration: none; background: #eef1f5; color: #1f2733;
|
padding: 9px 16px; font-size: 14px; text-decoration: none; background: #eef1f5; color: #1f2733;
|
||||||
}
|
}
|
||||||
button.primary, .btn.primary { background: #1f3a5f; color: #fff; }
|
button.primary, .btn.primary { background: #1f3a5f; color: #fff; }
|
||||||
button.secondary, .btn.secondary { background: #2f7d5b; color: #fff; }
|
button.secondary, .btn.secondary { background: #2f7d5b; color: #fff; }
|
||||||
|
button.danger, .btn.danger { background: #e6453c; color: #fff; border-color: #d23b32; }
|
||||||
button:hover, .btn:hover { filter: brightness(.96); }
|
button:hover, .btn:hover { filter: brightness(.96); }
|
||||||
.link-danger { background: none; border: none; color: #c0392b; cursor: pointer; padding: 0 4px; font-size: 14px; }
|
.link-danger { background: none; border: none; color: #c0392b; cursor: pointer; padding: 0 4px; font-size: 14px; }
|
||||||
.link-danger:hover { text-decoration: underline; }
|
.link-danger:hover { text-decoration: underline; }
|
||||||
@@ -69,11 +74,18 @@ button:hover, .btn:hover { filter: brightness(.96); }
|
|||||||
.row-danger { background: #fff1f0; }
|
.row-danger { background: #fff1f0; }
|
||||||
.row-warn { background: #fffbe6; }
|
.row-warn { background: #fffbe6; }
|
||||||
|
|
||||||
.badge { padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
.badge { display: inline-block; white-space: nowrap; padding: 2px 8px; border-radius: 10px; font-size: 12px; }
|
||||||
.badge.ok { background: #e6f7ed; color: #1a7f47; }
|
.badge.ok { background: #e6f7ed; color: #1a7f47; }
|
||||||
.badge.warn { background: #fff7e6; color: #ad6800; }
|
.badge.warn { background: #fff7e6; color: #ad6800; }
|
||||||
.badge.danger { background: #fff1f0; color: #cf1322; }
|
.badge.danger { background: #fff1f0; color: #cf1322; }
|
||||||
|
|
||||||
|
/* 备料: 计划数量差异指示 */
|
||||||
|
.plan-input { padding: 3px 6px; border: 1px solid #d0d5dd; border-radius: 4px; font-size: 13px; }
|
||||||
|
.plan-input:focus { outline: none; border-color: #4096ff; box-shadow: 0 0 0 2px rgba(64,150,255,.15); }
|
||||||
|
.diff { font-variant-numeric: tabular-nums; }
|
||||||
|
.diff-pos { color: #1a7f47; font-weight: 600; } /* 计划 > 采购(多订) 绿 */
|
||||||
|
.diff-neg { color: #cf1322; font-weight: 600; } /* 计划 < 采购(少订) 红 */
|
||||||
|
|
||||||
.flash { padding: 10px 14px; border-radius: 6px; margin-bottom: 12px; font-size: 14px; }
|
.flash { padding: 10px 14px; border-radius: 6px; margin-bottom: 12px; font-size: 14px; }
|
||||||
.flash.ok { background: #e6f7ed; color: #1a7f47; }
|
.flash.ok { background: #e6f7ed; color: #1a7f47; }
|
||||||
.flash.error { background: #fff1f0; color: #cf1322; }
|
.flash.error { background: #fff1f0; color: #cf1322; }
|
||||||
@@ -143,6 +155,9 @@ tr.row-sim-danger td { color: #cf1322; }
|
|||||||
/* 批量导入窗口: 多 BOM 卡片 */
|
/* 批量导入窗口: 多 BOM 卡片 */
|
||||||
.bom-card { border: 1px solid #e3e8ef; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
.bom-card { border: 1px solid #e3e8ef; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
||||||
.bom-card.card-danger { border-color: #ffccc7; }
|
.bom-card.card-danger { border-color: #ffccc7; }
|
||||||
.bom-card .bom-head { background: #f0f3f7; padding: 8px 12px; font-size: 14px; display: flex; align-items: center; gap: 8px; }
|
.bom-card .bom-head { background: #f0f3f7; padding: 8px 12px; font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
||||||
.bom-card.card-danger .bom-head { background: #fff1f0; }
|
.bom-card.card-danger .bom-head { background: #fff1f0; }
|
||||||
|
.bom-card .bom-head label.bh-lbl { margin: 0; white-space: nowrap; color: #44525f; font-size: 12px; }
|
||||||
|
.bom-card .bom-head input.bom-code-inp { width: 160px; flex: 0 0 auto; padding: 5px 8px; font-size: 13px; }
|
||||||
|
.bom-card .bom-head input.bom-name-inp { flex: 1 1 220px; min-width: 180px; padding: 5px 8px; font-size: 13px; }
|
||||||
.bom-card .tbl { box-shadow: none; border-radius: 0; margin: 0; }
|
.bom-card .tbl { box-shadow: none; border-radius: 0; margin: 0; }
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
{% if g.user and g.user.role in ('admin','manager') %}
|
{% if g.user and g.user.role in ('admin','manager') %}
|
||||||
<a href="{{ url_for('boms') }}">BOM</a>
|
<a href="{{ url_for('boms') }}">BOM</a>
|
||||||
<a href="{{ url_for('orders') }}">生产订单</a>
|
<a href="{{ url_for('orders') }}">生产订单</a>
|
||||||
|
<a href="{{ url_for('prepare') }}">备料</a>
|
||||||
<a href="{{ url_for('report') }}">报表</a>
|
<a href="{{ url_for('report') }}">报表</a>
|
||||||
<a href="{{ url_for('logs') }}">日志</a>
|
<a href="{{ url_for('logs') }}">日志</a>
|
||||||
<a href="{{ url_for('factories') }}">工厂</a>
|
<a href="{{ url_for('factories') }}">工厂</a>
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<form method="post" class="card form-card" style="max-width:640px">
|
<form method="post" class="card form-card" style="max-width:640px">
|
||||||
<h3>{{ '编辑 BOM' if b else '新增 BOM' }}</h3>
|
<h3>{{ '编辑 BOM' if b else '新增 BOM' }}</h3>
|
||||||
<label>BOM 型号 *</label>
|
<label>BOM 型号 *</label>
|
||||||
<input name="code" value="{{ b.code if b else '' }}" required {% if b %}readonly{% endif %}>
|
<input name="code" value="{{ form_code if form_code is defined else (b.code if b else '') }}" required>
|
||||||
<label>名称</label>
|
<label>名称</label>
|
||||||
<input name="name" value="{{ b.name if b else '' }}">
|
<input name="name" value="{{ b.name if b else '' }}">
|
||||||
<label>备注</label>
|
<label>备注</label>
|
||||||
|
|||||||
+39
-33
@@ -1,58 +1,58 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}BOM 清单{% endblock %}
|
{% block title %}BOM 清单{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="toolbar">
|
<div class="toolbar" data-page-node-id="vZTw3kPadHZliSPinOK7Z1">
|
||||||
<h3 style="margin:0">BOM 清单 ({{ data|length }})</h3>
|
<h3 style="margin:0" data-page-node-id="4wsrdtS5FO7LVSlfo7CSNG">BOM 清单 ({{ data|length }})</h3>
|
||||||
<div>
|
<div data-page-node-id="wqIH8X3KYDPieHFmuHJk1U">
|
||||||
<a class="btn" href="{{ url_for('download_template', kind='bom') }}">⬇ 下载模板</a>
|
<a class="btn" href="{{ url_for('download_template', kind='bom') }}" data-page-node-id="1uWZtIhFdzaXWtcVfxDuOg">⬇ 下载模板</a>
|
||||||
<button class="primary" type="button" onclick="openBatch()">⊞ 批量导入窗口</button>
|
<button class="primary" type="button" onclick="openBatch()" data-page-node-id="gzZTAfVl4A7uSSGZbnvUiB">⊞ 批量导入窗口</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="tbl">
|
<table class="tbl" data-page-node-id="b0iXNI8PpWQuniQYav5O0I">
|
||||||
<thead><tr><th>BOM 型号</th><th>名称</th><th>组成项数</th><th>操作</th></tr></thead>
|
<thead data-page-node-id="iYtImpRt9uTnLT1EuDXgCb"><tr data-page-node-id="9EW4NPakt3ZhDfzYevNQb6"><th data-page-node-id="sp7aqyu2wPcBXC14JySMsV">BOM 型号</th><th data-page-node-id="ON7POYFoxBWVPwDTJ5YFVh">名称</th><th data-page-node-id="H6pAPeWhIGXasJf7is1PlR">组成项数</th><th data-page-node-id="IQPsDz71IQrUtVnzjSvTqE">操作</th></tr></thead>
|
||||||
<tbody>
|
<tbody data-page-node-id="GlcDs3bYGt9ktLuIMCnQ4S">
|
||||||
{% for d in data %}
|
{% for d in data %}
|
||||||
<tr>
|
<tr data-page-node-id="oIa73ogUxPXPW7hd6mSQqR">
|
||||||
<td><a href="{{ url_for('bom_detail', bid=d.b.id) }}">{{ d.b.code }}</a></td>
|
<td data-page-node-id="txkFINu0hqDcunfToMsIHJ"><a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="wZa7pJWwB3UVob7w2rMPYi">{{ d.b.code }}</a></td>
|
||||||
<td>{{ d.b.name or '-' }}</td>
|
<td data-page-node-id="4ZgT9JXaM292L2DxjmPWdn">{{ d.b.name or '-' }}</td>
|
||||||
<td class="num"><a href="{{ url_for('bom_detail', bid=d.b.id) }}">{{ d.count }}</a></td>
|
<td class="num" data-page-node-id="81E2RDtrSvIDDtmXamJaKy"><a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="xibV2oxiCVTyWQxHQ3GBvC">{{ d.count }}</a></td>
|
||||||
<td>
|
<td data-page-node-id="z8hNVcUqGGY3LogEDYFO6M">
|
||||||
<a href="{{ url_for('bom_detail', bid=d.b.id) }}">查看</a>
|
<a href="{{ url_for('bom_detail', bid=d.b.id) }}" data-page-node-id="y99zemTyOQfPCzgAuGfoHX">查看</a>
|
||||||
<a href="{{ url_for('bom_edit', bid=d.b.id) }}">编辑</a>
|
<a href="{{ url_for('bom_edit', bid=d.b.id) }}" data-page-node-id="Xlkt2Bi0z38orXYfmovRTF">编辑</a>
|
||||||
<form method="post" action="{{ url_for('bom_delete', bid=d.b.id) }}" style="display:inline" onsubmit="return confirm('确认删除该 BOM?');">
|
<form method="post" action="{{ url_for('bom_delete', bid=d.b.id) }}" style="display:inline" onsubmit="return confirm('确认删除该 BOM?');" data-page-node-id="GRCffEreUmFcM3CoH1FEUC">
|
||||||
<button class="link">删除</button>
|
<button class="link" data-page-node-id="fk4q7Q2tIJdpd368XDifLE">删除</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="4" class="muted">还没有 BOM, 点右上角新增</td></tr>
|
<tr data-page-node-id="8dATczjWBQGQA7MAsGu9f8"><td colspan="4" class="muted" data-page-node-id="7Zbca69N7opSKetW07LkHr">还没有 BOM, 点右上角新增</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- 批量导入窗口(模态) -->
|
<!-- 批量导入窗口(模态) -->
|
||||||
<div class="modal-mask" id="batchMask" style="display:none">
|
<div class="modal-mask" id="batchMask" style="display:none" data-page-node-id="na2dwI5Srtijy3wkeG1h5s">
|
||||||
<div class="modal">
|
<div class="modal" data-page-node-id="tEI8MSJjVrQXENBtLlS1dA">
|
||||||
<div class="modal-head">
|
<div class="modal-head" data-page-node-id="EHG4VZ8LF2jK6OQuZpZ1ij">
|
||||||
<span>批量导入 BOM · 多 BOM 清单</span>
|
<span data-page-node-id="xHsrz06nBQLPcxe4wbnUF5">批量导入 BOM · 多 BOM 清单</span>
|
||||||
<button class="x" onclick="closeBatch()">✕</button>
|
<button class="x" onclick="closeBatch()" data-page-node-id="aoDJmcOoqnCSk7ji1ol2al">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body" data-page-node-id="v1ZVsZhJV9kTmwR8H3UTll">
|
||||||
<p class="small muted">支持两种录入:① 直接上传 <b>Altium / 嘉立创 BOM</b>(每个 worksheet 即一个 BOM,<b>Part Number</b> 关联物料、<b>Quantity</b> 列即单套用量,下载模板即此格式);② 粘贴文本,每行一个 BOM,以 <b>#BOM <型号> <名称></b> 开头,其下为 <b>物料编号 单套用量</b>,空行分隔。物料不存在时导入会自动新建主数据。</p>
|
<p class="small muted" data-page-node-id="uBFCxuwHOoYf4HDB7GHjqB">支持两种录入:① 直接上传 <b data-page-node-id="qPxNhhrFxrTrmWD4tIcNEf">Altium / 嘉立创 BOM</b>(每个 worksheet 即一个 BOM,<b data-page-node-id="6TXeirBIr5BJkAkpI9CMod">Part Number</b> 关联物料、<b data-page-node-id="NYIE9aPWd3xD4Tr0hLOqI3">Quantity</b> 列即单套用量,下载模板即此格式);② 粘贴文本,每行一个 BOM,以 <b data-page-node-id="jy8nTX15GDqKSV6wYSm78A">#BOM <型号> <名称></b> 开头,其下为 <b data-page-node-id="Lgr6U8MBrInBJO29ZLmnN3">物料编号 单套用量</b>,空行分隔。物料不存在时导入会自动新建主数据。</p>
|
||||||
<textarea id="batchText" rows="11" placeholder="#BOM M4021 示例产品A
|
<textarea id="batchText" rows="11" placeholder="#BOM M4021 示例产品A
|
||||||
P0001 2
|
P0001 2
|
||||||
P0002 0.5
|
P0002 0.5
|
||||||
|
|
||||||
#BOM M4022 示例产品B
|
#BOM M4022 示例产品B
|
||||||
P0003 4"></textarea>
|
P0003 4"></textarea>
|
||||||
<div class="row-actions">
|
<div class="row-actions" data-page-node-id="Un0xhQE1iuii1GXRoLOGEy">
|
||||||
<button type="button" onclick="parsePaste()">解析预览</button>
|
<button type="button" onclick="parsePaste()" data-page-node-id="2nie414a2o4ULz4FNMbFVC">解析预览</button>
|
||||||
<label class="btn" style="cursor:pointer">📁 上传文件
|
<label class="btn" style="cursor:pointer" data-page-node-id="s3iZeq1kL8Etsahvv2hGKO">📁 上传文件
|
||||||
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()">
|
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()" data-page-node-id="sj1omvbTTh7dteUj3QePnl">
|
||||||
</label>
|
</label>
|
||||||
<button type="button" class="primary" id="commitBtn" onclick="commitBatch()" disabled>确认导入</button>
|
<button type="button" class="primary" id="commitBtn" onclick="commitBatch()" disabled data-page-node-id="LV1nhOjsxrIhBszlCoxA2s">确认导入</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="batchResult" style="margin-top:12px"></div>
|
<div id="batchResult" style="margin-top:12px" data-page-node-id="zdtCCsWu38gEMMgkruNxOI"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,14 +83,20 @@ function postPreview(payload){
|
|||||||
body:JSON.stringify(payload)
|
body:JSON.stringify(payload)
|
||||||
}).then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
}).then(r=>r.json()).then(renderPreview).catch(e=>alert('解析失败: '+e));
|
||||||
}
|
}
|
||||||
|
function esc(s){
|
||||||
|
return String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
||||||
|
}
|
||||||
function renderPreview(data){
|
function renderPreview(data){
|
||||||
lastBoms = data.boms || [];
|
lastBoms = data.boms || [];
|
||||||
const valid = data.valid, invalid = data.invalid;
|
const valid = data.valid, invalid = data.invalid;
|
||||||
let html = `<div class="flash ${invalid? 'error':'ok'}">共 ${lastBoms.length} 个 BOM · 可导入 <b>${valid}</b> 个 · 异常 <b>${invalid}</b> 个</div>`;
|
let html = `<div class="flash ${invalid? 'error':'ok'}">共 ${lastBoms.length} 个 BOM · 可导入 <b>${valid}</b> 个 · 异常 <b>${invalid}</b> 个</div>`;
|
||||||
lastBoms.forEach(b=>{
|
lastBoms.forEach((b, i)=>{
|
||||||
const btag = b.ok? '<span class="badge ok">OK</span>' : `<span class="badge danger">${b.error||'错误'}</span>`;
|
const btag = b.ok? '<span class="badge ok">OK</span>' : `<span class="badge danger">${b.error||'错误'}</span>`;
|
||||||
html += `<div class="bom-card ${b.ok?'':'card-danger'}">`;
|
html += `<div class="bom-card ${b.ok?'':'card-danger'}">`;
|
||||||
html += `<div class="bom-head"><b>${b.code||''}</b> ${b.name||''} ${btag} <span class="muted small">成功项 ${b.valid_items}/${b.items.length}</span></div>`;
|
html += `<div class="bom-head">`;
|
||||||
|
html += `<label class="bh-lbl">型号</label><input class="bom-code-inp" value="${esc(b.code)}" oninput="lastBoms[${i}].code=this.value" title="可编辑 BOM 型号">`;
|
||||||
|
html += `<label class="bh-lbl">名称</label><input class="bom-name-inp" value="${esc(b.name)}" oninput="lastBoms[${i}].name=this.value" title="可编辑 BOM 名称(全部显示,可修改)">`;
|
||||||
|
html += `${btag} <span class="muted small">成功项 ${b.valid_items}/${b.items.length}</span></div>`;
|
||||||
html += '<table class="tbl"><thead><tr><th>物料编号</th><th>型号/规格</th><th>单套用量</th><th>校验</th></tr></thead><tbody>';
|
html += '<table class="tbl"><thead><tr><th>物料编号</th><th>型号/规格</th><th>单套用量</th><th>校验</th></tr></thead><tbody>';
|
||||||
b.items.forEach(it=>{
|
b.items.forEach(it=>{
|
||||||
const cls = it.ok? '' : 'row-danger';
|
const cls = it.ok? '' : 'row-danger';
|
||||||
|
|||||||
@@ -2,93 +2,107 @@
|
|||||||
{% block title %}工厂工作台 · {{ f.code }}{% endblock %}
|
{% block title %}工厂工作台 · {{ f.code }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="toolbar">
|
<div class="toolbar" data-page-node-id="h8tr6moQVxGSdE9dKEg6bd">
|
||||||
<h3 style="margin:0">工厂工作台:{{ f.code }} · {{ f.name or '' }}
|
<h3 style="margin:0" data-page-node-id="mN5n1l6U4DnPTMkfHhjkpo">工厂工作台:{{ f.code }} · {{ f.name or '' }}
|
||||||
<span class="badge {{ 'ok' if f.enabled else 'warn' }}">{{ '启用' if f.enabled else '停用' }}</span>
|
<span class="badge {{ 'ok' if f.enabled else 'warn' }}" data-page-node-id="9lYmmds1mElbCd1ib8GNYD">{{ '启用' if f.enabled else '停用' }}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<a class="btn" href="{{ url_for('factories') }}">← 返回工厂列表</a>
|
<a class="btn" href="{{ url_for('factories') }}" data-page-node-id="gLM7aoKY7uRuTSOFXVSBaL">← 返回工厂列表</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs" data-page-node-id="ante6pBhAiQZDcPHSkk7pm">
|
||||||
<button class="tab active" onclick="switchTab('kanban')">📊 物料看板</button>
|
<button class="tab active" onclick="switchTab('kanban')" data-page-node-id="t8Y9L0Rv6nJWGYeAc4Butp">📊 物料看板</button>
|
||||||
<button class="tab" onclick="switchTab('bom')">🧩 BOM 齐套</button>
|
<button class="tab" onclick="switchTab('bom')" data-page-node-id="1vqssKcNPuRFxIfonFhl2W">🧩 BOM 齐套</button>
|
||||||
<button class="tab" onclick="switchTab('import')">⊞ 批量导入</button>
|
<button class="tab" onclick="switchTab('import')" data-page-node-id="VulCglmTCTpl2I8BI40TEX">⊞ 批量导入</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 物料看板 -->
|
<!-- 物料看板 -->
|
||||||
<div id="tab-kanban" class="tab-pane">
|
<div id="tab-kanban" class="tab-pane" data-page-node-id="O5tr8yIq5PuggrETsUYwhX">
|
||||||
<div class="stat-row" style="margin:12px 0">
|
<form method="get" action="{{ url_for('factory_workbench', fid=f.id) }}" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:12px 0" data-page-node-id="DP9ksglRvmmkKQtWX8GzZC">
|
||||||
<div class="stat"><div class="num">{{ rows|length }}</div><div class="lbl">有库存物料</div></div>
|
<input type="text" name="q" value="{{ q }}" placeholder="搜索物料编号 / 型号规格"
|
||||||
<div class="stat warn"><div class="num">{{ low }}</div><div class="lbl">低于安全库存</div></div>
|
style="width:260px;padding:8px 10px;border:1px solid #cdd5df;border-radius:6px;font-size:14px" data-page-node-id="DtGH4sWm3Q8IaidV8Vcsvl">
|
||||||
<div class="stat danger"><div class="num">{{ negative }}</div><div class="lbl">库存为负</div></div>
|
<button class="primary" type="submit" data-page-node-id="9zgPeVdBfILSpKfD4TsDB2">搜索</button>
|
||||||
|
{% if q %}<a class="btn" href="{{ url_for('factory_workbench', fid=f.id) }}" data-page-node-id="y6UrlI6Z8PXiCHpif7cspa">清除</a>{% endif %}
|
||||||
|
</form>
|
||||||
|
{% if q %}<p class="small muted" style="margin:0 0 10px" data-page-node-id="bSjz9L1Hcc4kra4Z4DRTIT">筛选「{{ q }}」命中 <b data-page-node-id="gYjIGP7vAGwZROP3und6Mt">{{ rows|length }}</b> 条(仅含在厂库存不为 0 的物料)</p>{% endif %}
|
||||||
|
<div class="stat-row" style="margin:12px 0" data-page-node-id="XdjnYnwPez6NSo8RabtdEw">
|
||||||
|
<div class="stat" data-page-node-id="iWmneOIg5GaNg13oi8Tpml"><div class="num" data-page-node-id="sDfnYz7FDudHWa1F7hWxUj">{{ rows|length }}</div><div class="lbl" data-page-node-id="yUuytA7Ee2uZjYvyOVwdGW">有库存物料</div></div>
|
||||||
|
<div class="stat warn" data-page-node-id="9WprNxfiSAhkHKJoAfKVUs"><div class="num" data-page-node-id="PFYgLN2kA2eh3ehng7sL4E">{{ low }}</div><div class="lbl" data-page-node-id="cFVCxICUvwmYDEFJZ5LDBc">低于安全库存</div></div>
|
||||||
|
<div class="stat danger" data-page-node-id="OqyIPhwXMA2FKrB5ElPOnP"><div class="num" data-page-node-id="ymG0ZfkOIrB5d4yj8v3EMR">{{ negative }}</div><div class="lbl" data-page-node-id="EgDYjJQoOn0L8dkCLwkgE1">库存为负</div></div>
|
||||||
</div>
|
</div>
|
||||||
<table class="tbl">
|
<table class="tbl" data-page-node-id="v2dpNrmLrYl4iBusCHbYId">
|
||||||
<thead><tr><th>物料编号</th><th>型号/规格</th><th>单位</th><th>安全库存</th><th>在厂库存</th><th>状态</th></tr></thead>
|
<thead data-page-node-id="pfxxaWDexxHnrtTIFGOSvu"><tr data-page-node-id="7tQDDZE1giJATqe3rbI0V0"><th data-page-node-id="7PQzAHQJqUuBvUEQahPffK">物料编号</th><th data-page-node-id="u4s8ycQodHcEKS8Aou9MLv">型号/规格</th><th data-page-node-id="BJqNtTZALVaxrbIcqkZsb1">单位</th><th data-page-node-id="GIF5kOqgw826EHX7PEfF27">安全库存</th><th data-page-node-id="SK7R13nwY2nvGYlhCeRu37">在厂库存</th><th data-page-node-id="wImv6pW0xYFq35YHkUHEqk">状态</th><th data-page-node-id="MwGW0LuvnhgNhyhix4ge2L">操作</th></tr></thead>
|
||||||
<tbody>
|
<tbody data-page-node-id="taNmAZ8i4abDFIQijzVWOs">
|
||||||
{% for r in rows %}
|
{% for r in rows %}
|
||||||
<tr class="{{ 'row-danger' if r.status=='danger' else ('row-warn' if r.status=='warn' else '') }}">
|
<tr class="{{ 'row-danger' if r.status=='danger' else ('row-warn' if r.status=='warn' else '') }}"
|
||||||
<td>{{ r.m.part_number }}</td>
|
data-mid="{{ r.m.id }}" data-fid="{{ f.id }}"
|
||||||
<td>{{ r.m.manufacturer or '-' }}</td>
|
data-pn="{{ r.m.part_number }}" data-mf="{{ r.m.manufacturer or '' }}"
|
||||||
<td>{{ r.m.unit or '-' }}</td>
|
data-unit="{{ r.m.unit or '' }}" data-ss="{{ r.m.safety_stock or 0 }}" data-page-node-id="2cidlTmV6rQwsq9PrbpeeR">
|
||||||
<td class="num">{{ r.m.safety_stock or 0 }}</td>
|
<td data-page-node-id="wJDGDx053XiyTJva6zouSf">{{ r.m.part_number }}</td>
|
||||||
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
|
<td data-page-node-id="GLAB4ys6K8tlRL09h0SVSX">{{ r.m.manufacturer or '-' }}</td>
|
||||||
<td><span class="badge {{ 'danger' if r.status=='danger' else ('warn' if r.status=='warn' else 'ok') }}">{{ '负库存' if r.status=='danger' else ('低于安全库存' if r.status=='warn' else '正常') }}</span></td>
|
<td data-page-node-id="VDNrmbPZHQlBIJ05AoQly5">{{ r.m.unit or '-' }}</td>
|
||||||
|
<td class="num" data-page-node-id="EjDg4DmtWkB5OjEr7Jaw0J">{{ r.m.safety_stock or 0 }}</td>
|
||||||
|
<td class="num" data-page-node-id="V5Fp579iiSV2MBYW3ZF2i6">{{ '%.2f'|format(r.stock) }}</td>
|
||||||
|
<td data-page-node-id="z4HE32qyI5SFj3Kkb0rhS2"><span class="badge {{ 'danger' if r.status=='danger' else ('warn' if r.status=='warn' else 'ok') }}" data-page-node-id="n6KadEnTE72NuD06GKXEIx">{{ '负库存' if r.status=='danger' else ('低于安全库存' if r.status=='warn' else '正常') }}</span></td>
|
||||||
|
<td data-page-node-id="7DvKrplCD4RKbwZvM67dCa">
|
||||||
|
<span id="acts_{{ r.m.id }}" data-page-node-id="LvRHno48lkdWEGjdKxTHXX"><button class="btn small" type="button" onclick="editRow(this)" data-page-node-id="NSU10L6H6zY5TGeTPGQFEz">编辑</button></span>
|
||||||
|
<button class="btn small danger" type="button" onclick="delRow(this)" data-page-node-id="0sV7BYBp0CUIRvBU73nREM">删除</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="6" class="muted">该厂暂无库存数据</td></tr>
|
<tr data-page-node-id="osy9WfjSd5DiIkWUlHquBR"><td colspan="7" class="muted" data-page-node-id="TR0Ye9v5IRzsMkB8O5DadZ">该厂暂无库存数据</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- BOM 齐套 -->
|
<!-- BOM 齐套 -->
|
||||||
<div id="tab-bom" class="tab-pane" style="display:none">
|
<div id="tab-bom" class="tab-pane" style="display:none" data-page-node-id="0dcZdYpNL1yp8lUhBww5DZ">
|
||||||
{% for bd in bom_data %}
|
{% for bd in bom_data %}
|
||||||
<div class="card" style="margin-bottom:16px">
|
<div class="card" style="margin-bottom:16px" data-page-node-id="KQOJWc5nHbG4VjhSaQPuMk">
|
||||||
<h3 style="margin-top:0">{{ bd.b.code }} ({{ bd.b.name or '-' }})
|
<h3 style="margin-top:0" data-page-node-id="ycs6WD46CdMJ8UcSRCy5f6">{{ bd.b.code }} ({{ bd.b.name or '-' }})
|
||||||
<span class="small muted">· 单套用量 × 该厂库存</span></h3>
|
<span class="small muted" data-page-node-id="U6NLfEkrS2iQqgZimxy4zl">· 单套用量 × 该厂库存</span></h3>
|
||||||
{% if bd.comps %}
|
{% if bd.comps %}
|
||||||
<table class="tbl">
|
<table class="tbl" data-page-node-id="UdJnn9kfq0ajEZWGKyzVOL">
|
||||||
<thead><tr><th>物料编号</th><th>型号/规格</th><th>单套用量</th><th>该厂库存</th><th>状态</th></tr></thead>
|
<thead data-page-node-id="ARRG4xD4kWgudXephSkWRB"><tr data-page-node-id="nlsJQ9mpAJYIhFm75kH05N"><th data-page-node-id="V2dk9RMlqoxhgKdJtcLOMh">物料编号</th><th data-page-node-id="Kjj7qJMrh899UoknQcDLNA">型号/规格</th><th data-page-node-id="pkEIgqReAFzWQmTG58bBhE">单套用量</th><th data-page-node-id="amLBx4jB5xYtw9KX74SBbU">该厂库存</th><th data-page-node-id="4cEYL4lgAaUBmObLLkX0Km">状态</th></tr></thead>
|
||||||
<tbody>
|
<tbody data-page-node-id="mhRWoazwMuPyFzrrAIVuq7">
|
||||||
{% for c in bd.comps %}
|
{% for c in bd.comps %}
|
||||||
<tr class="{{ 'row-danger' if c.status=='danger' else ('row-warn' if c.status=='warn' else '') }}">
|
<tr class="{{ 'row-danger' if c.status=='danger' else ('row-warn' if c.status=='warn' else '') }}" data-page-node-id="9d7M31Zqmc5feItheDMB71">
|
||||||
<td>{{ c.m.part_number }}</td>
|
<td data-page-node-id="jv8YvfrtYpEsLkFsFyrwiO">{{ c.m.part_number }}</td>
|
||||||
<td>{{ c.m.manufacturer or '-' }}</td>
|
<td data-page-node-id="pRtaop8GPgnCJGFS4Cugle">{{ c.m.manufacturer or '-' }}</td>
|
||||||
<td class="num">{{ '%.3f'|format(c.qty) }}</td>
|
<td class="num" data-page-node-id="3uop7BWAV6eYdlVz5eqakf">{{ '%.3f'|format(c.qty) }}</td>
|
||||||
<td class="num">{{ '%.2f'|format(c.stock) }}</td>
|
<td class="num" data-page-node-id="jfNj0QeLPUrrBSfsPjzsHT">{{ '%.2f'|format(c.stock) }}</td>
|
||||||
<td><span class="badge {{ 'danger' if c.status=='danger' else ('warn' if c.status=='warn' else 'ok') }}">{{ '负库存' if c.status=='danger' else ('低于安全库存' if c.status=='warn' else '正常') }}</span></td>
|
<td data-page-node-id="MToeQ16NLjIvuPdfGFLojv"><span class="badge {{ 'danger' if c.status=='danger' else ('warn' if c.status=='warn' else 'ok') }}" data-page-node-id="3a5chV8jORWtOZKE3kVKYM">{{ '负库存' if c.status=='danger' else ('低于安全库存' if c.status=='warn' else '正常') }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="small muted">该 BOM 没有组成物料</p>
|
<p class="small muted" data-page-node-id="htEBvmB0x6pS6bfdx1SFaA">该 BOM 没有组成物料</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="muted">暂无 BOM</p>
|
<p class="muted" data-page-node-id="f188trWNRvLEGIEFhxwrC1">暂无 BOM</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 批量导入 -->
|
<!-- 批量导入 -->
|
||||||
<div id="tab-import" class="tab-pane" style="display:none">
|
<div id="tab-import" class="tab-pane" style="display:none" data-page-node-id="NRRM2DunropDEi85SDdAeY">
|
||||||
<div class="card" style="margin-bottom:16px">
|
<div class="card" style="margin-bottom:16px" data-page-node-id="pmIAFmv6QG2cAhVHKguzCC">
|
||||||
<h3 style="margin-top:0">导入入库流水到 {{ f.code }}</h3>
|
<h3 style="margin-top:0" data-page-node-id="gAGSs6J0QvEnz8OmMFZdaW">导入入库流水到 {{ f.code }}</h3>
|
||||||
<p class="small muted">上传 Excel/CSV(列:物料编号 / 数量 / 批次 / 日期 / 备注),将生成归属 {{ f.code }} 的入库流水。</p>
|
<p class="small muted" data-page-node-id="dRwIvucQurC2wYwZd7grcI">上传 Excel/CSV(列:物料编号 / 数量 / 批次 / 日期 / 备注),将生成归属 {{ f.code }} 的入库流水。</p>
|
||||||
<form method="post" action="{{ url_for('factory_import_inbound', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
<form method="post" action="{{ url_for('factory_import_inbound', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap" data-page-node-id="Oli4teU6igJvERarUlj8i8">
|
||||||
<input type="file" name="file" accept=".xlsx,.csv" required>
|
<input type="file" name="file" accept=".xlsx,.csv" required data-page-node-id="QvuR34OQvaVh8GOCZ4g69g">
|
||||||
<button class="primary" type="submit">上传并导入入库</button>
|
<button class="primary" type="submit" data-page-node-id="62RYuou3Nbtk1RAVVli1eE">上传并导入入库</button>
|
||||||
<a class="btn" href="{{ url_for('download_template', kind='transactions') }}">⬇ 下载模板</a>
|
<a class="btn" href="{{ url_for('download_template', kind='transactions') }}" data-page-node-id="XukAuHeUyL7T9wJs6aDfhT">⬇ 下载模板</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card" data-page-node-id="G6s6IIwsdxtO2QoglkQu5O">
|
||||||
<h3 style="margin-top:0">导入物料主数据(期初库存归属 {{ f.code }})</h3>
|
<h3 style="margin-top:0" data-page-node-id="sBnAiFr1fC7vAnge2CYt9L">导入物料主数据(期初库存归属 {{ f.code }})</h3>
|
||||||
<p class="small muted">上传物料 Excel(列:物料编号 / 型号 / 单位 / 安全库存 / 期初库存),期初库存将写入 {{ f.code }}。</p>
|
<p class="small muted" data-page-node-id="NHW4ZB8CHbwjA6xqxkOAJK">上传物料 Excel(列:物料编号 / 型号 / 单位 / 安全库存 / 期初库存),期初库存将写入 {{ f.code }}。</p>
|
||||||
<form method="post" action="{{ url_for('factory_import_materials', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap">
|
<form method="post" action="{{ url_for('factory_import_materials', fid=f.id) }}" enctype="multipart/form-data" style="display:flex;gap:10px;align-items:center;flex-wrap:wrap" data-page-node-id="9Y7Ec13v0Yo6okhmgErNLS">
|
||||||
<input type="file" name="file" accept=".xlsx,.csv" required>
|
<input type="file" name="file" accept=".xlsx,.csv" required data-page-node-id="3PqBIl65X6AWBok5cKoO4S">
|
||||||
<button class="primary" type="submit">上传并导入物料</button>
|
<button class="primary" type="submit" data-page-node-id="iU26xXbC5XkB2oLWJtUVws">上传并导入物料</button>
|
||||||
<a class="btn" href="{{ url_for('download_template', kind='materials') }}">⬇ 下载模板</a>
|
<a class="btn" href="{{ url_for('download_template', kind='materials') }}" data-page-node-id="o5mKNqqJtZJffB2hM595GA">⬇ 下载模板</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,5 +114,77 @@ function switchTab(name){
|
|||||||
event.target.classList.add('active');
|
event.target.classList.add('active');
|
||||||
document.getElementById('tab-'+name).style.display='block';
|
document.getElementById('tab-'+name).style.display='block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function esc(s){
|
||||||
|
return (s==null?'':String(s)).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进入某行编辑态: 物料编号、型号/规格、单位、安全库存 变为输入框
|
||||||
|
function editRow(btn){
|
||||||
|
var tr = btn.closest('tr');
|
||||||
|
tr.dataset.view = tr.innerHTML; // 保存视图态, 供取消还原
|
||||||
|
var mid = tr.dataset.mid, pn = tr.dataset.pn, mf = tr.dataset.mf, unit = tr.dataset.unit, ss = tr.dataset.ss;
|
||||||
|
var c = tr.children;
|
||||||
|
c[0].innerHTML = '<input class="cell-inp" id="pn_'+mid+'" value="'+esc(pn)+'">';
|
||||||
|
c[1].innerHTML = '<input class="cell-inp" id="mf_'+mid+'" value="'+esc(mf)+'">';
|
||||||
|
c[2].innerHTML = '<input class="cell-inp" id="unit_'+mid+'" value="'+esc(unit)+'">';
|
||||||
|
c[3].innerHTML = '<input class="cell-inp num" id="ss_'+mid+'" type="number" step="0.01" min="0" value="'+esc(ss)+'">';
|
||||||
|
document.getElementById('acts_'+mid).innerHTML =
|
||||||
|
'<button class="btn small primary" type="button" onclick="saveRow(this)">保存</button> '
|
||||||
|
+ '<button class="btn small" type="button" onclick="cancelRow(this)">取消</button>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelRow(btn){
|
||||||
|
var tr = btn.closest('tr');
|
||||||
|
tr.innerHTML = tr.dataset.view; // 还原视图态
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交保存(可递归: 二次确认合并后重提)
|
||||||
|
function postSave(fid, mid, fd){
|
||||||
|
fetch('/factory/'+fid+'/material/'+mid+'/quickedit', {method:'POST', body: fd})
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(d){
|
||||||
|
if(d.ok){
|
||||||
|
location.reload(); // 安全库存/合并可能影响状态与列表, 整页刷新保证一致
|
||||||
|
} else if(d.needs_confirm){
|
||||||
|
if(confirm(d.msg)){
|
||||||
|
fd.append('confirm_merge','1'); // 用户确认把当前物料合并到已存在的号
|
||||||
|
postSave(fid, mid, fd);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert('保存失败: ' + (d.msg || '未知错误'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(e){ alert('请求失败: ' + e); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRow(btn){
|
||||||
|
var tr = btn.closest('tr');
|
||||||
|
var mid = tr.dataset.mid, fid = tr.dataset.fid;
|
||||||
|
var pn = document.getElementById('pn_'+mid).value.trim();
|
||||||
|
var mf = document.getElementById('mf_'+mid).value;
|
||||||
|
var unit = document.getElementById('unit_'+mid).value;
|
||||||
|
var ss = document.getElementById('ss_'+mid).value;
|
||||||
|
if(!pn){ alert('物料编号不能为空'); return; }
|
||||||
|
var fd = new FormData();
|
||||||
|
fd.append('part_number', pn);
|
||||||
|
fd.append('manufacturer', mf);
|
||||||
|
fd.append('unit', unit);
|
||||||
|
fd.append('safety_stock', ss);
|
||||||
|
postSave(fid, mid, fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除整条物料记录(含其全部关联记录); 后端会自动备份数据库
|
||||||
|
function delRow(btn){
|
||||||
|
var tr = btn.closest('tr');
|
||||||
|
var mid = tr.dataset.mid, fid = tr.dataset.fid;
|
||||||
|
var pn = tr.dataset.pn;
|
||||||
|
if(!confirm('确定删除物料「'+pn+'」?\n将同时删除其所有入库/出库/BOM组成/替代料/调拨/生产订单调入计划记录,'
|
||||||
|
+ '且不可撤销(系统会自动备份数据库)。')) return;
|
||||||
|
fetch('/factory/'+fid+'/material/'+mid+'/delete', {method:'POST'})
|
||||||
|
.then(function(r){ return r.json(); })
|
||||||
|
.then(function(d){ if(d.ok){ location.reload(); } else { alert('删除失败: ' + (d.msg||'未知错误')); } })
|
||||||
|
.catch(function(e){ alert('请求失败: ' + e); });
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -27,6 +27,22 @@
|
|||||||
<div class="stat"><div class="num">{{ '%.2f'|format(out_total) }}</div><div class="lbl">消耗总数量</div></div>
|
<div class="stat"><div class="num">{{ '%.2f'|format(out_total) }}</div><div class="lbl">消耗总数量</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>各工厂实时库存(实时聚合)</h3>
|
||||||
|
<table class="tbl">
|
||||||
|
<thead><tr><th>工厂编号</th><th>工厂名称</th><th>实时库存</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for pf in per_factory %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ pf.f.code }}</td>
|
||||||
|
<td>{{ pf.f.name or '-' }}</td>
|
||||||
|
<td class="num">{{ '%.2f'|format(pf.stock) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="3" class="muted">暂无工厂</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
<h3>入库流水(每次入库时间 / 数量)</h3>
|
<h3>入库流水(每次入库时间 / 数量)</h3>
|
||||||
<table class="tbl">
|
<table class="tbl">
|
||||||
<thead><tr><th>入库日期</th><th>工厂</th><th>送货日期</th><th>录入时间</th><th>数量</th><th>批次号</th><th>来源 / 供应商</th><th>经手人</th><th>备注</th></tr></thead>
|
<thead><tr><th>入库日期</th><th>工厂</th><th>送货日期</th><th>录入时间</th><th>数量</th><th>批次号</th><th>来源 / 供应商</th><th>经手人</th><th>备注</th></tr></thead>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
<form method="post" class="card form-card">
|
<form method="post" class="card form-card">
|
||||||
<h3>{{ '编辑物料' if m else '新增物料' }}</h3>
|
<h3>{{ '编辑物料' if m else '新增物料' }}</h3>
|
||||||
<label>物料编号 *</label>
|
<label>物料编号 *</label>
|
||||||
<input name="part_number" value="{{ m.part_number if m else '' }}" {{ 'readonly' if m }} required>
|
<input name="part_number" value="{{ m.part_number if m else '' }}" {{ 'required' if m else 'required' }}>
|
||||||
|
<p class="small muted" style="margin-top:2px">物料编码为唯一业务编号(不可与其他物料重复)。</p>
|
||||||
<label>型号 / 规格</label>
|
<label>型号 / 规格</label>
|
||||||
<input name="manufacturer" value="{{ m.manufacturer if m else '' }}">
|
<input name="manufacturer" value="{{ m.manufacturer if m else '' }}">
|
||||||
<label>单位</label>
|
<label>单位</label>
|
||||||
|
|||||||
@@ -1,42 +1,69 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}订单 {{ o.order_no }}{% endblock %}
|
{% block title %}订单 {{ o.order_no }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
/* 打印(派工单): 仅保留订单信息与明细表, 隐藏导航/按钮, 输入框显示为纯文本 */
|
||||||
|
@media print {
|
||||||
|
header.topbar { display: none !important; }
|
||||||
|
.toolbar .btn, .toolbar a.btn, .toolbar button { display: none !important; }
|
||||||
|
.toolbar .badge { display: inline-block !important; }
|
||||||
|
.row-actions { display: none !important; }
|
||||||
|
.print-hide { display: none !important; }
|
||||||
|
input[name^="alloc_"] {
|
||||||
|
border: none !important; background: none !important; padding: 0 !important;
|
||||||
|
width: auto !important; -webkit-appearance: none; appearance: none;
|
||||||
|
font: inherit; color: inherit;
|
||||||
|
}
|
||||||
|
form { display: block; }
|
||||||
|
@page { margin: 12mm; }
|
||||||
|
body { background: #fff !important; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h3 style="margin:0">生产订单 {{ o.order_no }}</h3>
|
<h3 style="margin:0">生产订单 {{ o.order_no }}</h3>
|
||||||
<span class="badge {{ 'ok' if o.status=='picked' else 'warn' }}">{{ '已领料' if o.status=='picked' else '待领料' }}</span>
|
<span class="badge {{ 'ok' if o.status=='picked' else 'warn' }}">{{ '已领料' if o.status=='picked' else '待领料' }}</span>
|
||||||
<a class="btn" href="{{ url_for('order_edit', oid=o.id) }}">编辑订单</a>
|
<a class="btn" href="{{ url_for('order_edit', oid=o.id) }}">编辑订单</a>
|
||||||
|
<a class="btn" href="{{ url_for('order_export', oid=o.id) }}">导出 Excel</a>
|
||||||
|
<button class="btn" type="button" onclick="window.print()">打印</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" style="margin-bottom:16px">
|
<div class="card" style="margin-bottom:16px">
|
||||||
<p>BOM: <b>{{ bom.code if bom else '-' }}</b> ({{ bom.name if bom else '' }}) | 接单量: <b>{{ '%.2f'|format(o.qty) }}</b> | 生产工厂: <b>{{ factory.code if factory else '未指定(按汇总库存)' }}</b> | 经手人: {{ o.operator or '-' }}</p>
|
<p class="small muted" style="margin-top:0"><b>下单信息(按下单时记录)</b></p>
|
||||||
|
<p><b>BOM 清单:</b>{% for b, q in lines %}<span style="display:inline-block;margin-right:14px">{{ b.code }}{% if b.name %} · {{ b.name }}{% endif %} × <b>{{ '%.2f'|format(q) }}</b></span>{% else %}-{% endfor %}</p>
|
||||||
|
<p>总接单量(合计): <b>{{ '%.2f'|format(total_qty) }}</b> | 生产工厂: <b>{% if factory %}{{ factory.code }}{% if factory.name %} · {{ factory.name }}{% endif %}{% else %}未指定(按汇总库存){% endif %}</b> | 经手人: {{ o.operator or '-' }}{% if o.created_at %} | 下单时间: {{ o.created_at.strftime('%Y-%m-%d %H:%M') }}{% endif %}</p>
|
||||||
{% if o.remark %}<p class="small muted">备注: {{ o.remark }}</p>{% endif %}
|
{% if o.remark %}<p class="small muted">备注: {{ o.remark }}</p>{% endif %}
|
||||||
|
<p class="small muted print-hide" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>领料需求与调入 (单套用量 × 接单量){% if factory %} | 按 {{ factory.code }} 工厂库存核算{% endif %}</h3>
|
<h3>领料需求与调入 (按各 BOM 单套用量 × 各自生产数量 汇总){% if factory %} | 按下单前库存核算{% endif %}</h3>
|
||||||
|
<p class="small muted" style="margin-top:-6px">库存口径:下方「下单前库存」为 <b>订单下单那一刻、所属工厂的库存快照</b>(按下单时间截断,已自动排除该订单自身后续的调入/领料及下单后发生的其他出入库),用于还原下单时的可供料情况。{% if not factory %}本单未指定生产工厂,按下单前全部工厂汇总库存展示。{% endif %}</p>
|
||||||
|
|
||||||
<form method="post" action="{{ url_for('order_alloc_save', oid=o.id) }}">
|
<form method="post" action="{{ url_for('order_alloc_save', oid=o.id) }}">
|
||||||
<table class="tbl">
|
<table class="tbl">
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th>物料编号</th><th>型号</th><th>需求数量</th>
|
<th>物料编号</th><th>型号</th><th>下单前库存</th>
|
||||||
<th>调入数量</th><th>当前库存</th><th>缺口</th><th>状态</th><th>可替代料</th>
|
<th>需求数量</th><th>缺口</th><th>调入数量</th><th>状态</th><th>工厂库存</th><th>可替代料</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in reqs %}
|
{% for r in reqs %}
|
||||||
<tr class="{{ 'row-danger' if r.short else ('row-warn' if r.have < (r.m.safety_stock or 0)) }}">
|
<tr class="{{ 'row-danger' if r.short else ('row-warn' if r.have < (r.m.safety_stock or 0)) }}">
|
||||||
<td><a href="{{ url_for('material_detail', mid=r.m.id) }}" title="进入工厂物料">{{ r.m.part_number }}</a></td>
|
<td><a href="{{ url_for('material_detail', mid=r.m.id) }}" title="进入工厂物料">{{ r.m.part_number }}</a></td>
|
||||||
<td>{{ r.m.manufacturer or '-' }}</td>
|
<td>{{ r.m.manufacturer or '-' }}</td>
|
||||||
|
<td class="num">{{ '%.2f'|format(r.have) }}</td>
|
||||||
<td class="num">{{ '%.2f'|format(r.need) }}</td>
|
<td class="num">{{ '%.2f'|format(r.need) }}</td>
|
||||||
|
<td class="num">{{ '%.2f'|format(r.gap) }}</td>
|
||||||
<td class="num">
|
<td class="num">
|
||||||
{% if r.posted %}
|
{% if r.posted %}
|
||||||
<span class="badge ok" title="已生成入库流水">已调入 {{ '%.2f'|format(r.alloc) }}</span>
|
<div style="margin-bottom:3px"><span class="badge ok" title="已生成入库流水, 可直接修改后保存/确认">已调入</span></div>
|
||||||
|
<input name="alloc_{{ r.m.id }}" type="text" inputmode="decimal"
|
||||||
|
value="{{ '%.2f'|format(r.alloc) }}" style="width:96px">
|
||||||
{% else %}
|
{% else %}
|
||||||
<input name="alloc_{{ r.m.id }}" type="number" step="0.01" min="0"
|
<input name="alloc_{{ r.m.id }}" type="text" inputmode="decimal"
|
||||||
value="{{ '%.2f'|format(r.alloc) if r.alloc else '' }}"
|
value="{{ '%.2f'|format(r.alloc) if r.alloc else '' }}"
|
||||||
placeholder="{{ '%.2f'|format(r.need) }}" style="width:96px">
|
placeholder="{{ '%.2f'|format(r.need) }}" style="width:96px">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="num">{{ '%.2f'|format(r.have) }}</td>
|
|
||||||
<td class="num">{{ '%.2f'|format(r.gap) }}</td>
|
|
||||||
<td><span class="badge {{ 'danger' if r.short else 'ok' }}">{{ '缺料' if r.short else '充足' }}</span></td>
|
<td><span class="badge {{ 'danger' if r.short else 'ok' }}">{{ '缺料' if r.short else '充足' }}</span></td>
|
||||||
|
<td class="num">{{ '%.2f'|format(r.gap + r.alloc) }}</td>
|
||||||
<td class="small">
|
<td class="small">
|
||||||
{% if r.subs %}
|
{% if r.subs %}
|
||||||
{% for s in r.subs %}
|
{% for s in r.subs %}
|
||||||
@@ -67,7 +94,7 @@
|
|||||||
formaction="{{ url_for('order_alloc_post', oid=o.id) }}"
|
formaction="{{ url_for('order_alloc_post', oid=o.id) }}"
|
||||||
onclick="return confirm('确认将待调入数量从上方所选来源工厂调拨到生产工厂? 总库存不变, 仅厂间转移。');">确认调入(厂间调拨)</button>
|
onclick="return confirm('确认将待调入数量从上方所选来源工厂调拨到生产工厂? 总库存不变, 仅厂间转移。');">确认调入(厂间调拨)</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="small muted">「确认调入」是一次厂间调拨:从来源工厂出库减、生产工厂入库加,系统总库存不变。</p>
|
<p class="small muted print-hide">「确认调入」是一次厂间调拨:从来源工厂出库减、生产工厂入库加,系统总库存不变。</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="card" style="margin:14px 0">
|
<div class="card" style="margin:14px 0">
|
||||||
@@ -82,4 +109,28 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<p class="flash ok">该订单已领料出库。</p>
|
<p class="flash ok">该订单已领料出库。</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
// 调入数量输入框:上下方向键在「上一行/下一行」的同列输入框之间移动焦点
|
||||||
|
// (左右方向键保持默认行为:在文本内移动光标)
|
||||||
|
function allocInputs(){
|
||||||
|
return Array.prototype.slice.call(document.querySelectorAll('input[name^="alloc_"]'));
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', function(e){
|
||||||
|
if(e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
|
||||||
|
var t = e.target;
|
||||||
|
if(!t || t.tagName !== 'INPUT' || t.name.indexOf('alloc_') !== 0) return;
|
||||||
|
var inputs = allocInputs();
|
||||||
|
var idx = inputs.indexOf(t);
|
||||||
|
if(idx < 0) return;
|
||||||
|
var next;
|
||||||
|
if(e.key === 'ArrowUp'){ if(idx <= 0) return; next = inputs[idx - 1]; }
|
||||||
|
else { if(idx >= inputs.length - 1) return; next = inputs[idx + 1]; }
|
||||||
|
e.preventDefault();
|
||||||
|
next.focus();
|
||||||
|
try { var v = next.value; next.setSelectionRange(v.length, v.length); } catch(_){}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+48
-12
@@ -1,20 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ '编辑生产订单' if o else '新建生产订单' }}{% endblock %}
|
{% block title %}{{ '编辑生产订单' if o else '新建生产订单' }}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<form method="post" class="card form-card" style="max-width:520px"
|
<form method="post" class="card form-card" style="max-width:720px"
|
||||||
action="{{ url_for('order_edit', oid=o.id) if o else url_for('order_new') }}">
|
action="{{ url_for('order_edit', oid=o.id) if o else url_for('order_new') }}"
|
||||||
|
{% if o %}onsubmit="return confirm('确认保存修改?已录入的调入数量与已入账流水将保持不变(被移除 BOM 中已入账的物料仍保留其流水)。');"{% endif %}>
|
||||||
<h3>{{ '编辑生产订单' if o else '新建生产订单' }}</h3>
|
<h3>{{ '编辑生产订单' if o else '新建生产订单' }}</h3>
|
||||||
<label>订单号 {% if o %}(单号不可重复){% else %}(留空自动生成){% endif %}</label>
|
<label>订单号 {% if o %}(单号不可重复){% else %}(留空自动生成){% endif %}</label>
|
||||||
<input name="order_no" value="{{ o.order_no if o else '' }}" placeholder="如 WO20260911xxx">
|
<input name="order_no" value="{{ o.order_no if o else '' }}" placeholder="如 WO20260911xxx">
|
||||||
<label>BOM 型号 *</label>
|
|
||||||
<select name="bom_id" required>
|
<label>BOM 与生产数量 *</label>
|
||||||
<option value="">-- 选择 BOM --</option>
|
<p class="small muted" style="margin:0 0 6px">可在同一订单中加入多个 BOM,每个 BOM 独立填写生产数量(限<b>正整数,不含小数点</b>,如 1、12、100);点击「+ 添加 BOM」增加一行,不需要的行可删除。</p>
|
||||||
{% for b in boms %}
|
<div id="bomRows" class="bom-rows"></div>
|
||||||
<option value="{{ b.id }}" {% if o and o.bom_id == b.id %}selected{% endif %}>{{ b.code }} ({{ b.name or '-' }})</option>
|
<button type="button" class="btn" style="margin:2px 0 12px" onclick="addBomRow()">+ 添加 BOM</button>
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
<label>接单量 (台/套) *</label>
|
|
||||||
<input name="qty" type="number" step="0.01" min="0.01" required value="{{ '%.2f'|format(o.qty) if o else '' }}">
|
|
||||||
<label>生产工厂</label>
|
<label>生产工厂</label>
|
||||||
<select name="factory_id">
|
<select name="factory_id">
|
||||||
<option value="">-- 不指定(汇总库存校验)--</option>
|
<option value="">-- 不指定(汇总库存校验)--</option>
|
||||||
@@ -28,6 +26,44 @@
|
|||||||
<button class="primary">{{ '保存修改' if o else '生成订单' }}</button>
|
<button class="primary">{{ '保存修改' if o else '生成订单' }}</button>
|
||||||
<a class="btn" href="{{ url_for('orders') }}">取消</a>
|
<a class="btn" href="{{ url_for('orders') }}">取消</a>
|
||||||
</div>
|
</div>
|
||||||
<p class="small muted">生成后可在订单详情页按 BOM 展开领料需求, 并手动录入每个物料的「调入数量」。</p>
|
<p class="small muted">生成后可在订单详情页按各 BOM 展开领料需求, 并手动录入每个物料的「调入数量」。</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{# 隐藏模板: 供 JS 克隆(含全部 BOM 选项) #}
|
||||||
|
<template id="bomTpl">
|
||||||
|
<div class="bom-row" style="display:flex; gap:10px; align-items:center; margin-bottom:6px">
|
||||||
|
<select name="bom_id[]" required>
|
||||||
|
<option value="">-- 选择 BOM --</option>
|
||||||
|
{% for b in boms %}<option value="{{ b.id }}">{{ b.code }}{% if b.name %} · {{ b.name }}{% endif %}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input name="qty[]" type="text" inputmode="numeric" required placeholder="数量(整数)" style="width:130px" oninput="capQty(this)">
|
||||||
|
<button type="button" class="link-danger" onclick="this.closest('.bom-row').remove();checkRows()">删除</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function capQty(el){
|
||||||
|
// 仅保留数字, 禁止小数点/负号, 允许多位正整数(不限位数)
|
||||||
|
var v = (el.value || '').replace(/[^\d]/g, '');
|
||||||
|
el.value = v === '' ? '' : String(parseInt(v, 10));
|
||||||
|
}
|
||||||
|
function addBomRow(sel, qty){
|
||||||
|
var tpl = document.getElementById('bomTpl');
|
||||||
|
var row = tpl.content.firstElementChild.cloneNode(true);
|
||||||
|
if (sel) row.querySelector('select').value = String(sel);
|
||||||
|
if (qty != null && qty !== undefined) row.querySelector('input').value = qty;
|
||||||
|
document.getElementById('bomRows').appendChild(row);
|
||||||
|
checkRows();
|
||||||
|
}
|
||||||
|
function checkRows(){
|
||||||
|
var rows = document.querySelectorAll('#bomRows .bom-row');
|
||||||
|
rows.forEach(function(r){
|
||||||
|
var btn = r.querySelector('button');
|
||||||
|
if (btn) btn.style.display = rows.length > 1 ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var init = {{ initial_lines|tojson }};
|
||||||
|
if (init && init.length){ init.forEach(function(x){ addBomRow(x[0], x[1]); }); }
|
||||||
|
else { addBomRow(); }
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,16 +4,17 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<h3 style="margin:0">生产订单 ({{ data|length }})</h3>
|
<h3 style="margin:0">生产订单 ({{ data|length }})</h3>
|
||||||
<a class="btn primary" href="{{ url_for('order_new') }}">+ 新建订单</a>
|
<a class="btn primary" href="{{ url_for('order_new') }}">+ 新建订单</a>
|
||||||
|
<a class="btn" href="{{ url_for('orders_export') }}">导出 Excel</a>
|
||||||
</div>
|
</div>
|
||||||
<table class="tbl">
|
<table class="tbl">
|
||||||
<thead><tr><th>订单号</th><th>BOM</th><th>接单量</th><th>生产工厂</th><th>状态</th><th>经手人</th><th>操作</th></tr></thead>
|
<thead><tr><th>订单号</th><th>BOM 清单(型号 × 数量)</th><th>总接单量</th><th>生产工厂</th><th>状态</th><th>经手人</th><th>操作</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for d in data %}
|
{% for d in data %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ d.o.order_no }}</td>
|
<td>{{ d.o.order_no }}</td>
|
||||||
<td>{{ d.bom.code if d.bom else '-' }} ({{ d.bom.name if d.bom else '' }})</td>
|
<td>{% for b, q in d.lines %}<div>{{ b.code }}{% if b.name %} · {{ b.name }}{% endif %} × {{ '%.2f'|format(q) }}</div>{% else %}-{% endfor %}</td>
|
||||||
<td class="num">{{ '%.2f'|format(d.o.qty) }}</td>
|
<td class="num">{{ '%.2f'|format(d.total) }}</td>
|
||||||
<td>{{ d.factory.code if d.factory else '未指定' }}</td>
|
<td>{% if d.factory %}{{ d.factory.code }}{% if d.factory.name %} · {{ d.factory.name }}{% endif %}{% else %}未指定{% endif %}</td>
|
||||||
<td><span class="badge {{ 'ok' if d.o.status=='picked' else 'warn' }}">{{ '已领料' if d.o.status=='picked' else '待领料' }}</span></td>
|
<td><span class="badge {{ 'ok' if d.o.status=='picked' else 'warn' }}">{{ '已领料' if d.o.status=='picked' else '待领料' }}</span></td>
|
||||||
<td>{{ d.o.operator or '-' }}</td>
|
<td>{{ d.o.operator or '-' }}</td>
|
||||||
<td class="row-actions">
|
<td class="row-actions">
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}备料清单{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="toolbar">
|
||||||
|
<h3 style="margin:0">备料清单(采购下单用)</h3>
|
||||||
|
<span class="badge warn">不记账 · 按需用×套数预填</span>
|
||||||
|
<form method="post" action="{{ url_for('prepare_clear') }}" style="display:inline"
|
||||||
|
onsubmit="return confirm('确定清空当前范围内的全部备料计划?此操作不可撤销。');">
|
||||||
|
<button type="submit" class="btn danger">清空清单</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn" href="{{ url_for('prepare_export') }}">导出 Excel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card" style="margin-bottom:16px">
|
||||||
|
<p class="small muted" style="margin-top:0">
|
||||||
|
本页为<b>独立的采购备料清单</b>:选一个 <b>BOM</b> 并输入<b>套数</b>,即按
|
||||||
|
<b>「计划数量 = BOM单套用量 × 套数」</b> 一键生成(可再手工改);
|
||||||
|
也可在下方手工添加任意物料并填数量。当前范围:<b>{% if current_factory %}{{ current_factory.code }} · {{ current_factory.name }}{% else %}全部工厂(汇总){% endif %}</b>;
|
||||||
|
「实际库存」「缺口」随顶栏工厂切换实时计算。
|
||||||
|
</p>
|
||||||
|
<p class="small muted" style="margin-bottom:0">
|
||||||
|
说明:本页与 Excel 导出均<b>只读</b>,不产生任何入库 / 出库流水,仅用于向供应商下单采购。
|
||||||
|
<b>缺口 = 计划数量 − 实际库存</b>(>0 表示还需采购,<0 表示库存已盈余)。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 添加物料 #}
|
||||||
|
<form method="post" action="{{ url_for('prepare_add') }}" class="card" style="margin-bottom:16px;display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">
|
||||||
|
<div>
|
||||||
|
<label class="small muted">物料编号</label><br>
|
||||||
|
<input list="matlist" name="part_number" placeholder="输入或选择物料编号" style="width:240px" autocomplete="off" required>
|
||||||
|
<datalist id="matlist">
|
||||||
|
{% for m in materials %}<option value="{{ m.part_number }}">{{ m.manufacturer or '' }}</option>{% endfor %}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="small muted">计划数量</label><br>
|
||||||
|
<input type="text" inputmode="decimal" name="qty" placeholder="可留空" style="width:120px;text-align:right">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn primary">添加物料</button>
|
||||||
|
<span class="muted small">手工录入单个物料(计划数量可留空,稍后填)</span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{# 按 BOM 批量生成计划(计划数量 = 单套用量 × 套数) #}
|
||||||
|
<form method="post" action="{{ url_for('prepare_add_bom') }}" class="card" style="margin-bottom:16px;display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap">
|
||||||
|
<div>
|
||||||
|
<label class="small muted">① 选择 BOM</label><br>
|
||||||
|
<select id="bomSel" name="bom_id" style="width:300px">
|
||||||
|
<option value="">-- 选择 BOM --</option>
|
||||||
|
{% for b in bom_opts %}<option value="{{ b.id }}">{{ b.label }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="small muted">② 套数</label><br>
|
||||||
|
<input type="text" inputmode="decimal" name="sets" value="1" style="width:120px;text-align:right">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn primary">生成计划(需用×套数)</button>
|
||||||
|
<span class="muted small">计划数量 = BOM单套用量 × 套数,生成后可在表中手工调整</span>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="post" action="{{ url_for('prepare_save') }}">
|
||||||
|
<div class="toolbar" style="justify-content:flex-end">
|
||||||
|
<span class="muted small">修改计划数量后点此保存</span>
|
||||||
|
<button type="submit" class="btn primary">保存计划数量</button>
|
||||||
|
</div>
|
||||||
|
<table class="tbl">
|
||||||
|
<thead><tr>
|
||||||
|
<th>物料编号</th><th>型号/规格</th><th>单位</th>
|
||||||
|
<th>计划数量</th><th>实际库存</th><th>缺口</th><th>状态</th><th>操作</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr class="{{ 'row-danger' if r.diff > 0 else '' }}">
|
||||||
|
<td><a href="{{ url_for('material_detail', mid=r.m.id) }}" title="进入工厂物料">{{ r.m.part_number }}</a></td>
|
||||||
|
<td>{{ r.m.manufacturer or '-' }}</td>
|
||||||
|
<td>{{ r.m.unit or '个' }}</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" inputmode="decimal" name="plan_{{ r.id }}"
|
||||||
|
class="plan-input" style="width:90px;text-align:right"
|
||||||
|
value="{{ '%.2f'|format(r.plan) if r.plan else '' }}"
|
||||||
|
data-stock="{{ '%.2f'|format(r.stock) }}">
|
||||||
|
</td>
|
||||||
|
<td class="num">{{ '%.2f'|format(r.stock) }}</td>
|
||||||
|
<td class="num"><span class="diff" id="diff_{{ r.id }}">{{ '%.2f'|format(r.diff) }}</span></td>
|
||||||
|
<td><span class="badge {{ 'danger' if r.diff > 0 else 'ok' }}">{{ '需采购' if r.diff > 0 else ('盈余' if r.diff < 0 else '刚好') }}</span></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="{{ url_for('prepare_delete', pid=r.id) }}" style="display:inline"
|
||||||
|
onsubmit="return confirm('确定删除该备料物料?');">
|
||||||
|
<button type="submit" class="btn danger small">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr><td colspan="8" class="muted">还没有添加任何备料物料,请选择 BOM 加入或手工录入。</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// 计划数量实时计算缺口 = 计划 - 实际库存
|
||||||
|
function recalcDiff(input) {
|
||||||
|
var row = input.closest('tr');
|
||||||
|
var diffEl = row.querySelector('.diff');
|
||||||
|
if (!diffEl) return;
|
||||||
|
var stock = parseFloat(input.getAttribute('data-stock')) || 0;
|
||||||
|
var val = input.value.trim();
|
||||||
|
if (val === '') { diffEl.textContent = ''; diffEl.className = 'diff'; return; }
|
||||||
|
var plan = parseFloat(val);
|
||||||
|
if (isNaN(plan)) { diffEl.textContent = '—'; diffEl.className = 'diff'; return; }
|
||||||
|
var d = Math.round((plan - stock) * 100) / 100;
|
||||||
|
diffEl.textContent = d.toFixed(2);
|
||||||
|
diffEl.className = 'diff ' + (d < 0 ? 'diff-neg' : (d > 0 ? 'diff-pos' : ''));
|
||||||
|
var badge = row.querySelector('.badge');
|
||||||
|
if (badge) {
|
||||||
|
badge.className = 'badge ' + (d > 0 ? 'danger' : 'ok');
|
||||||
|
badge.textContent = d > 0 ? '需采购' : (d < 0 ? '盈余' : '刚好');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.querySelectorAll('.plan-input').forEach(function (inp) {
|
||||||
|
inp.addEventListener('input', function () { recalcDiff(this); });
|
||||||
|
recalcDiff(inp); // 初始(回填值)
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
+1
-1
@@ -27,7 +27,7 @@
|
|||||||
<label>所属工厂 *</label>
|
<label>所属工厂 *</label>
|
||||||
<select name="factory_id" required>
|
<select name="factory_id" required>
|
||||||
{% for f in factories %}
|
{% for f in factories %}
|
||||||
<option value="{{ f.id }}" {% if (current_factory_id == f.id) or (not current_factory_id and loop.first) %}selected{% endif %}>{{ f.code }} · {{ f.name }}</option>
|
<option value="{{ f.id }}" {% if kind=='inbound' and default_factory_id %}{% if f.id == default_factory_id %}selected{% endif %}{% else %}{% if (current_factory_id == f.id) or (not current_factory_id and loop.first) %}selected{% endif %}{% endif %}>{{ f.code }} · {{ f.name }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<label>批次号</label>
|
<label>批次号</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user