feat: BOM批量导入型号/名称可编辑 + 生产订单查看按下单前库存快照 + 编辑冲销回下单状态
This commit is contained in:
@@ -9,6 +9,7 @@ M3 治理: 多角色(admin/manager/operator) + 操作日志 + 报表打印导出
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import hashlib
|
||||
import webbrowser
|
||||
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}
|
||||
|
||||
|
||||
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):
|
||||
"""库存状态: danger=负库存, warn=低于安全库存, ok=正常"""
|
||||
if stock < 0:
|
||||
@@ -157,9 +187,13 @@ def outbound_total_map(factory_id=None):
|
||||
|
||||
|
||||
# ---------------- 替代料关系 ----------------
|
||||
def substitute_rows(main_id, factory_id=None):
|
||||
"""返回主料 main_id 的可替代料列表: [{sub:Material, stock:float, available:bool}]。"""
|
||||
sm = stock_map(factory_id)
|
||||
def substitute_rows(main_id, factory_id=None, as_of=None):
|
||||
"""返回主料 main_id 的可替代料列表: [{sub:Material, stock:float, available:bool}]。
|
||||
|
||||
as_of 传入时, 库存按「截至 as_of 之前」快照计算(用于生产订单查看时
|
||||
呈现下单前的库存)。默认 None 取当前库存。
|
||||
"""
|
||||
sm = stock_map_as_of(factory_id, as_of)
|
||||
subs = db.session.scalars(
|
||||
db.select(Substitute).where(Substitute.main_id == main_id)).all()
|
||||
out = []
|
||||
@@ -1298,7 +1332,10 @@ def order_detail(oid):
|
||||
return redirect(url_for("orders"))
|
||||
bom = db.session.get(Bom, o.bom_id)
|
||||
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
|
||||
# 已录入的「调入数量」计划
|
||||
allocs = db.session.scalars(
|
||||
@@ -1312,7 +1349,7 @@ def order_detail(oid):
|
||||
a = alloc_map.get(m.id)
|
||||
subs = [{"pn": s["sub"].part_number, "mf": s["sub"].manufacturer or "-",
|
||||
"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,
|
||||
"gap": need - have, "short": need > have, "subs": subs,
|
||||
"alloc": a.alloc_qty if a else 0,
|
||||
@@ -1323,7 +1360,7 @@ def order_detail(oid):
|
||||
factories = db.session.scalars(
|
||||
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,
|
||||
factories=factories,
|
||||
factories=factories, as_of=as_of,
|
||||
posted_total=posted_total, planned_total=planned_total,
|
||||
posted_count=posted_count)
|
||||
|
||||
@@ -1378,6 +1415,7 @@ def order_edit(oid):
|
||||
if qty <= 0:
|
||||
flash("接单量必须大于 0", "error")
|
||||
else:
|
||||
old_no = 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(
|
||||
db.select(ProdOrder).where(ProdOrder.order_no == order_no)):
|
||||
@@ -1388,9 +1426,20 @@ def order_edit(oid):
|
||||
o.factory_id = factory_id
|
||||
o.qty = qty
|
||||
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()
|
||||
log_action("编辑生产订单", o.order_no, f"接单量 {qty}")
|
||||
flash("订单已更新", "ok")
|
||||
log_action("编辑生产订单", o.order_no,
|
||||
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 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"))
|
||||
|
||||
|
||||
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"])
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
|
||||
@@ -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
@@ -143,6 +143,9 @@ tr.row-sim-danger td { color: #cf1322; }
|
||||
/* 批量导入窗口: 多 BOM 卡片 */
|
||||
.bom-card { border: 1px solid #e3e8ef; border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
|
||||
.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 .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; }
|
||||
|
||||
+39
-33
@@ -1,58 +1,58 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}BOM 清单{% endblock %}
|
||||
{% block content %}
|
||||
<div class="toolbar">
|
||||
<h3 style="margin:0">BOM 清单 ({{ data|length }})</h3>
|
||||
<div>
|
||||
<a class="btn" href="{{ url_for('download_template', kind='bom') }}">⬇ 下载模板</a>
|
||||
<button class="primary" type="button" onclick="openBatch()">⊞ 批量导入窗口</button>
|
||||
<div class="toolbar" data-page-node-id="vZTw3kPadHZliSPinOK7Z1">
|
||||
<h3 style="margin:0" data-page-node-id="4wsrdtS5FO7LVSlfo7CSNG">BOM 清单 ({{ data|length }})</h3>
|
||||
<div data-page-node-id="wqIH8X3KYDPieHFmuHJk1U">
|
||||
<a class="btn" href="{{ url_for('download_template', kind='bom') }}" data-page-node-id="1uWZtIhFdzaXWtcVfxDuOg">⬇ 下载模板</a>
|
||||
<button class="primary" type="button" onclick="openBatch()" data-page-node-id="gzZTAfVl4A7uSSGZbnvUiB">⊞ 批量导入窗口</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="tbl">
|
||||
<thead><tr><th>BOM 型号</th><th>名称</th><th>组成项数</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<table class="tbl" data-page-node-id="b0iXNI8PpWQuniQYav5O0I">
|
||||
<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 data-page-node-id="GlcDs3bYGt9ktLuIMCnQ4S">
|
||||
{% for d in data %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('bom_detail', bid=d.b.id) }}">{{ d.b.code }}</a></td>
|
||||
<td>{{ d.b.name or '-' }}</td>
|
||||
<td class="num"><a href="{{ url_for('bom_detail', bid=d.b.id) }}">{{ d.count }}</a></td>
|
||||
<td>
|
||||
<a href="{{ url_for('bom_detail', bid=d.b.id) }}">查看</a>
|
||||
<a href="{{ url_for('bom_edit', bid=d.b.id) }}">编辑</a>
|
||||
<form method="post" action="{{ url_for('bom_delete', bid=d.b.id) }}" style="display:inline" onsubmit="return confirm('确认删除该 BOM?');">
|
||||
<button class="link">删除</button>
|
||||
<tr data-page-node-id="oIa73ogUxPXPW7hd6mSQqR">
|
||||
<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 data-page-node-id="4ZgT9JXaM292L2DxjmPWdn">{{ d.b.name or '-' }}</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 data-page-node-id="z8hNVcUqGGY3LogEDYFO6M">
|
||||
<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) }}" 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?');" data-page-node-id="GRCffEreUmFcM3CoH1FEUC">
|
||||
<button class="link" data-page-node-id="fk4q7Q2tIJdpd368XDifLE">删除</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% 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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 批量导入窗口(模态) -->
|
||||
<div class="modal-mask" id="batchMask" style="display:none">
|
||||
<div class="modal">
|
||||
<div class="modal-head">
|
||||
<span>批量导入 BOM · 多 BOM 清单</span>
|
||||
<button class="x" onclick="closeBatch()">✕</button>
|
||||
<div class="modal-mask" id="batchMask" style="display:none" data-page-node-id="na2dwI5Srtijy3wkeG1h5s">
|
||||
<div class="modal" data-page-node-id="tEI8MSJjVrQXENBtLlS1dA">
|
||||
<div class="modal-head" data-page-node-id="EHG4VZ8LF2jK6OQuZpZ1ij">
|
||||
<span data-page-node-id="xHsrz06nBQLPcxe4wbnUF5">批量导入 BOM · 多 BOM 清单</span>
|
||||
<button class="x" onclick="closeBatch()" data-page-node-id="aoDJmcOoqnCSk7ji1ol2al">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="small muted">支持两种录入:① 直接上传 <b>Altium / 嘉立创 BOM</b>(每个 worksheet 即一个 BOM,<b>Part Number</b> 关联物料、<b>Quantity</b> 列即单套用量,下载模板即此格式);② 粘贴文本,每行一个 BOM,以 <b>#BOM <型号> <名称></b> 开头,其下为 <b>物料编号 单套用量</b>,空行分隔。物料不存在时导入会自动新建主数据。</p>
|
||||
<div class="modal-body" data-page-node-id="v1ZVsZhJV9kTmwR8H3UTll">
|
||||
<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
|
||||
P0001 2
|
||||
P0002 0.5
|
||||
|
||||
#BOM M4022 示例产品B
|
||||
P0003 4"></textarea>
|
||||
<div class="row-actions">
|
||||
<button type="button" onclick="parsePaste()">解析预览</button>
|
||||
<label class="btn" style="cursor:pointer">📁 上传文件
|
||||
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()">
|
||||
<div class="row-actions" data-page-node-id="Un0xhQE1iuii1GXRoLOGEy">
|
||||
<button type="button" onclick="parsePaste()" data-page-node-id="2nie414a2o4ULz4FNMbFVC">解析预览</button>
|
||||
<label class="btn" style="cursor:pointer" data-page-node-id="s3iZeq1kL8Etsahvv2hGKO">📁 上传文件
|
||||
<input type="file" id="batchFile" accept=".xlsx,.csv" style="display:none" onchange="parseFile()" data-page-node-id="sj1omvbTTh7dteUj3QePnl">
|
||||
</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 id="batchResult" style="margin-top:12px"></div>
|
||||
<div id="batchResult" style="margin-top:12px" data-page-node-id="zdtCCsWu38gEMMgkruNxOI"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,14 +83,20 @@ function postPreview(payload){
|
||||
body:JSON.stringify(payload)
|
||||
}).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){
|
||||
lastBoms = data.boms || [];
|
||||
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>`;
|
||||
lastBoms.forEach(b=>{
|
||||
lastBoms.forEach((b, i)=>{
|
||||
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-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>';
|
||||
b.items.forEach(it=>{
|
||||
const cls = it.ok? '' : 'row-danger';
|
||||
|
||||
@@ -7,17 +7,20 @@
|
||||
<a class="btn" href="{{ url_for('order_edit', oid=o.id) }}">编辑订单</a>
|
||||
</div>
|
||||
<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 %}
|
||||
<p class="small muted" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
|
||||
</div>
|
||||
|
||||
<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) }}">
|
||||
<table class="tbl">
|
||||
<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>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in reqs %}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
{% block title %}{{ '编辑生产订单' if o else '新建生产订单' }}{% endblock %}
|
||||
{% block content %}
|
||||
<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>
|
||||
<label>订单号 {% if o %}(单号不可重复){% else %}(留空自动生成){% endif %}</label>
|
||||
<input name="order_no" value="{{ o.order_no if o else '' }}" placeholder="如 WO20260911xxx">
|
||||
|
||||
Reference in New Issue
Block a user