feat: BOM批量导入型号/名称可编辑 + 生产订单查看按下单前库存快照 + 编辑冲销回下单状态

This commit is contained in:
2026-10-10 12:26:21 +08:00
parent 5060d027e5
commit 574d608e11
6 changed files with 162 additions and 45 deletions
+76 -8
View File
@@ -9,6 +9,7 @@ M3 治理: 多角色(admin/manager/operator) + 操作日志 + 报表打印导出
""" """
import os import os
import sys import sys
import shutil
import hashlib import hashlib
import webbrowser import webbrowser
from datetime import date, datetime from datetime import date, datetime
@@ -127,6 +128,35 @@ def stock_map(factory_id=None):
return {i: inb.get(i, 0) - out.get(i, 0) for i in ids} return {i: inb.get(i, 0) - out.get(i, 0) for i in ids}
def stock_map_as_of(factory_id=None, as_of=None):
"""返回截至 as_of 之前(不含 as_of 及之后创建)的净库存 {material_id: qty}。
- as_of 为 datetime(通常用订单的 created_at)。
- 用 created_at 作截断, 从而自动排除该订单自身后续的调入/领料流水,
以及下单之后才发生的其他出入库 —— 即「下单那一刻的库存快照」。
- as_of 为 None 时等价 stock_map(当前库存)。
- 兼容极少数字段 created_at 为空: 空值不参与(视为缺失), 不影响其余统计。
"""
if as_of is None:
return stock_map(factory_id)
fcond = " AND factory_id = :fid" if factory_id else ""
params = {"asof": as_of}
if factory_id:
params["fid"] = factory_id
rows = db.session.execute(db.text(
f"SELECT material_id, COALESCE(SUM(qty),0) FROM inbound "
f"WHERE created_at IS NOT NULL AND created_at < :asof{fcond} GROUP BY material_id"),
params).fetchall()
inb = {r[0]: float(r[1]) for r in rows}
rows = db.session.execute(db.text(
f"SELECT material_id, COALESCE(SUM(qty),0) FROM outbound "
f"WHERE created_at IS NOT NULL AND created_at < :asof{fcond} GROUP BY material_id"),
params).fetchall()
out = {r[0]: float(r[1]) for r in rows}
ids = set(inb) | set(out)
return {i: inb.get(i, 0) - out.get(i, 0) for i in ids}
def _status_of(stock, safety): def _status_of(stock, safety):
"""库存状态: danger=负库存, warn=低于安全库存, ok=正常""" """库存状态: danger=负库存, warn=低于安全库存, ok=正常"""
if stock < 0: if stock < 0:
@@ -157,9 +187,13 @@ def outbound_total_map(factory_id=None):
# ---------------- 替代料关系 ---------------- # ---------------- 替代料关系 ----------------
def substitute_rows(main_id, factory_id=None): def substitute_rows(main_id, factory_id=None, as_of=None):
"""返回主料 main_id 的可替代料列表: [{sub:Material, stock:float, available:bool}]。""" """返回主料 main_id 的可替代料列表: [{sub:Material, stock:float, available:bool}]。
sm = stock_map(factory_id)
as_of 传入时, 库存按「截至 as_of 之前」快照计算(用于生产订单查看时
呈现下单前的库存)。默认 None 取当前库存。
"""
sm = stock_map_as_of(factory_id, as_of)
subs = db.session.scalars( subs = db.session.scalars(
db.select(Substitute).where(Substitute.main_id == main_id)).all() db.select(Substitute).where(Substitute.main_id == main_id)).all()
out = [] out = []
@@ -1298,7 +1332,10 @@ def order_detail(oid):
return redirect(url_for("orders")) return redirect(url_for("orders"))
bom = db.session.get(Bom, o.bom_id) bom = db.session.get(Bom, o.bom_id)
items = db.session.scalars(db.select(BomItem).where(BomItem.bom_id == o.bom_id)).all() items = db.session.scalars(db.select(BomItem).where(BomItem.bom_id == o.bom_id)).all()
sm = stock_map(o.factory_id) # 查看订单时, 库存按「下单那一刻之前」的快照展示, 而非当前实时库存,
# 这样能还原下单时的真实可供料情况(已自动排除本单后续的调入/领料)。
as_of = o.created_at
sm = stock_map_as_of(o.factory_id, as_of)
fac = db.session.get(Factory, o.factory_id) if o.factory_id else None fac = db.session.get(Factory, o.factory_id) if o.factory_id else None
# 已录入的「调入数量」计划 # 已录入的「调入数量」计划
allocs = db.session.scalars( allocs = db.session.scalars(
@@ -1312,7 +1349,7 @@ def order_detail(oid):
a = alloc_map.get(m.id) a = alloc_map.get(m.id)
subs = [{"pn": s["sub"].part_number, "mf": s["sub"].manufacturer or "-", subs = [{"pn": s["sub"].part_number, "mf": s["sub"].manufacturer or "-",
"stock": round(s["stock"], 3), "available": s["available"]} "stock": round(s["stock"], 3), "available": s["available"]}
for s in substitute_rows(m.id, o.factory_id)] for s in substitute_rows(m.id, o.factory_id, as_of)]
reqs.append({"m": m, "need": need, "have": have, reqs.append({"m": m, "need": need, "have": have,
"gap": need - have, "short": need > have, "subs": subs, "gap": need - have, "short": need > have, "subs": subs,
"alloc": a.alloc_qty if a else 0, "alloc": a.alloc_qty if a else 0,
@@ -1323,7 +1360,7 @@ def order_detail(oid):
factories = db.session.scalars( factories = db.session.scalars(
db.select(Factory).where(Factory.enabled == True).order_by(Factory.code)).all() db.select(Factory).where(Factory.enabled == True).order_by(Factory.code)).all()
return render_template("order_detail.html", o=o, bom=bom, reqs=reqs, factory=fac, return render_template("order_detail.html", o=o, bom=bom, reqs=reqs, factory=fac,
factories=factories, factories=factories, as_of=as_of,
posted_total=posted_total, planned_total=planned_total, posted_total=posted_total, planned_total=planned_total,
posted_count=posted_count) posted_count=posted_count)
@@ -1378,6 +1415,7 @@ def order_edit(oid):
if qty <= 0: if qty <= 0:
flash("接单量必须大于 0", "error") flash("接单量必须大于 0", "error")
else: else:
old_no = o.order_no
order_no = request.form.get("order_no", "").strip() or o.order_no order_no = request.form.get("order_no", "").strip() or o.order_no
if order_no != o.order_no and db.session.scalar( if order_no != o.order_no and db.session.scalar(
db.select(ProdOrder).where(ProdOrder.order_no == order_no)): db.select(ProdOrder).where(ProdOrder.order_no == order_no)):
@@ -1388,9 +1426,20 @@ def order_edit(oid):
o.factory_id = factory_id o.factory_id = factory_id
o.qty = qty o.qty = qty
o.remark = request.form.get("remark", "").strip() o.remark = request.form.get("remark", "").strip()
# 冲销回「下单时」状态: 先备份库, 再撤销该订单已产生的调入/调出/领料流水 + 清空调入计划
try:
_bak = os.path.join(EXE_DIR, "_backup",
f"reset_bak_{datetime.now():%Y%m%d_%H%M%S}.db")
os.makedirs(os.path.dirname(_bak), exist_ok=True)
shutil.copy(db_path, _bak)
except Exception:
_bak = None
n_in, n_out, n_alloc = _reset_order_to_placed(o, old_no)
db.session.commit() db.session.commit()
log_action("编辑生产订单", o.order_no, f"接单量 {qty}") log_action("编辑生产订单", o.order_no,
flash("订单已更新", "ok") f"接单量 {qty}; 冲销回下单状态: 调入入库{n_in}/调出领料{n_out}/计划{n_alloc}"
+ (f"; 备份 {os.path.basename(_bak)}" if _bak else ""))
flash(f"订单已更新并回到「下单状态」(冲销调入/领料流水 {n_in + n_out} 笔、清空调入计划 {n_alloc} 行)", "ok")
return redirect(url_for("order_detail", oid=oid)) return redirect(url_for("order_detail", oid=oid))
return render_template("order_form.html", boms=boms_all, factories=factories, o=o) return render_template("order_form.html", boms=boms_all, factories=factories, o=o)
@@ -1419,6 +1468,25 @@ def order_delete(oid):
return redirect(url_for("orders")) return redirect(url_for("orders"))
def _reset_order_to_placed(o, order_no):
"""把订单冲销回「刚下单」状态: 撤销该订单关联的调入入库 / 调出 / 领料流水, 清空调入计划, 状态置回 planned。
- 调入入库(Inbound, source=生产订单调入) 与 调出/领料(Outbound, purpose in (生产领用, 生产订单调出)) 均按 order_ref 关联, 直接删除即冲销(总库存/厂间余额恢复)。
- 调入计划(prod_order_alloc) 按 order_id 清除。
- 返回 (冲销入库笔数, 冲销出库笔数, 清除计划行数)。
"""
n_in = db.session.execute(
db.delete(Inbound).where(Inbound.order_ref == order_no)).rowcount
n_out = db.session.execute(
db.delete(Outbound).where(
Outbound.order_ref == order_no,
Outbound.purpose.in_(["生产领用", "生产订单调出"]))).rowcount
n_alloc = db.session.execute(
db.delete(ProdOrderAlloc).where(ProdOrderAlloc.order_id == o.id)).rowcount
o.status = "planned"
return n_in, n_out, n_alloc
@app.route("/order/<int:oid>/alloc", methods=["POST"]) @app.route("/order/<int:oid>/alloc", methods=["POST"])
@login_required @login_required
@role_required("admin", "manager") @role_required("admin", "manager")
+36
View File
@@ -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
+4 -1
View File
@@ -143,6 +143,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; }
+39 -33
View File
@@ -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 &lt;型号&gt; &lt;名称&gt;</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 &lt;型号&gt; &lt;名称&gt;</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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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';
+5 -2
View File
@@ -7,17 +7,20 @@
<a class="btn" href="{{ url_for('order_edit', oid=o.id) }}">编辑订单</a> <a class="btn" href="{{ url_for('order_edit', oid=o.id) }}">编辑订单</a>
</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>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 '-' }}{% 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" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
</div> </div>
<h3>领料需求与调入 (单套用量 × 接单量){% if factory %} | 按 {{ factory.code }} 工厂库存核算{% endif %}</h3> <h3>领料需求与调入 (单套用量 × 接单量){% if factory %} | 按 {{ factory.code }} 工厂库存核算{% endif %}</h3>
<p class="small muted" style="margin-top:-6px">库存口径:下方「下单前库存」为 <b>该订单下单那一刻</b>的库存快照(已自动剔除本单后续调入/领料及下单之后的出入库),用于还原下单时的可供料情况,与实时库存可能不同。{% if as_of %}快照时点:{{ as_of.strftime('%Y-%m-%d %H:%M') }}。{% 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>
</tr></thead> </tr></thead>
<tbody> <tbody>
{% for r in reqs %} {% for r in reqs %}
+2 -1
View File
@@ -2,7 +2,8 @@
{% 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:520px"
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('保存将把该订单冲销回「刚下单」状态(撤销已调入/领料流水、清空调入计划),确认继续?');"{% 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">