feat: 生产订单库存口径/排序/编辑体验与入库默认工厂优化
- 入库登记「所属工厂」默认选中总部(SELF),出库不变 - 编辑生产订单改为仅更新字段:改单号同步关联出入库 order_ref、保留已录入调入数量与已入账流水(移除冲销逻辑) - 生产订单列表/详情按最终订单号(WO+YYYYMMDD+序号)降序排序 - 生产订单详情库存/缺口改用「下单前库存快照」(按下单时间截断),缺口=库存-需求,升序使缺料排前 - 生产工厂列显示「编号 · 公司名称」 - 详情列表列序调整:下单前库存置需求左边、调入数量置缺口右边;状态右侧新增「工厂库存」列(=缺口+调入数量) - 状态「缺料/充足」徽标强制单行显示(.badge white-space:nowrap) - 已调入(已入账)数量可编辑:保存/确认后按新值自动撤销原流水并从原来源厂重新入账,源厂库存不足判定缺料保留原入账,系统总库存不变
This commit is contained in:
@@ -84,6 +84,16 @@ def current_fid():
|
||||
return session.get("factory_id")
|
||||
|
||||
|
||||
def headquarters_fid():
|
||||
"""总部(自厂)工厂 id, 即 code='SELF'(name='自厂/总部')。
|
||||
入库登记默认归属总部。"""
|
||||
try:
|
||||
f = db.session.scalar(db.select(Factory).where(Factory.code == "SELF"))
|
||||
return f.id if f else None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_factory_ctx():
|
||||
"""向所有模板注入工厂列表与当前选中工厂, 供顶栏切换器使用。"""
|
||||
@@ -861,7 +871,9 @@ def inbound():
|
||||
if fid:
|
||||
stmt = stmt.where(Inbound.factory_id == fid)
|
||||
recs = db.session.scalars(stmt).all()
|
||||
return render_template("txn.html", kind="inbound", mats=mats, recs=recs, today=date.today().isoformat())
|
||||
return render_template("txn.html", kind="inbound", mats=mats, recs=recs,
|
||||
today=date.today().isoformat(),
|
||||
default_factory_id=headquarters_fid())
|
||||
|
||||
|
||||
@app.route("/outbound", methods=["GET", "POST"])
|
||||
@@ -1424,7 +1436,7 @@ def bom_detail(bid):
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
def orders():
|
||||
os_ = db.session.scalars(db.select(ProdOrder).order_by(ProdOrder.id.desc())).all()
|
||||
os_ = db.session.scalars(db.select(ProdOrder).order_by(ProdOrder.order_no.desc())).all()
|
||||
data = []
|
||||
for o in os_:
|
||||
bom = db.session.get(Bom, o.bom_id)
|
||||
@@ -1469,10 +1481,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()
|
||||
# 查看订单时, 库存按「下单那一刻之前」的快照展示, 而非当前实时库存,
|
||||
# 这样能还原下单时的真实可供料情况(已自动排除本单后续的调入/领料)。
|
||||
as_of = o.created_at
|
||||
sm = stock_map_as_of(o.factory_id, as_of)
|
||||
# 查看订单时, 库存/缺口按「下单之前」的工厂库存快照展示(即按下单那一刻的库存),
|
||||
# 按下单时间截断, 自动排除该订单自身后续的调入/领料及下单后发生的其他出入库,
|
||||
# 用于还原下单时的可供料情况, 不受后续出入库影响。
|
||||
sm = stock_map_as_of(o.factory_id, o.created_at)
|
||||
fac = db.session.get(Factory, o.factory_id) if o.factory_id else None
|
||||
# 已录入的「调入数量」计划
|
||||
allocs = db.session.scalars(
|
||||
@@ -1486,18 +1498,21 @@ 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, as_of)]
|
||||
for s in substitute_rows(m.id, o.factory_id, None)]
|
||||
reqs.append({"m": m, "need": need, "have": have,
|
||||
"gap": need - have, "short": need > have, "subs": subs,
|
||||
"gap": have - need, "short": need > have, "subs": subs,
|
||||
"alloc": a.alloc_qty if a else 0,
|
||||
"posted": a.posted if a else False})
|
||||
# 缺口 = 工厂库存 - 需求(少为负=缺料, 多为正=富余); 按缺口升序(从小到大),
|
||||
# 使缺料最严重(缺口最负)的物料排在最前, 便于优先调度
|
||||
reqs.sort(key=lambda x: x["gap"])
|
||||
posted_total = sum(a.alloc_qty for a in allocs if a.posted)
|
||||
planned_total = sum(a.alloc_qty for a in allocs)
|
||||
posted_count = sum(1 for a in allocs if a.posted)
|
||||
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, as_of=as_of,
|
||||
factories=factories,
|
||||
posted_total=posted_total, planned_total=planned_total,
|
||||
posted_count=posted_count)
|
||||
|
||||
@@ -1563,20 +1578,31 @@ 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)
|
||||
# 仅更新订单头; 已录入的「调入数量」计划与已入账流水保持不变
|
||||
# (需求: 改订单号/改生产数量均不冲销, 调入数量保持编辑前的值)。
|
||||
if order_no != old_no:
|
||||
# 备份后, 把关联流水(订单号作为关联键 order_ref)同步为新单号, 使其余数据保持一致
|
||||
try:
|
||||
_bak = os.path.join(EXE_DIR, "_backup",
|
||||
f"edit_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
|
||||
db.session.execute(
|
||||
update(Inbound).where(Inbound.order_ref == old_no)
|
||||
.values(order_ref=order_no))
|
||||
db.session.execute(
|
||||
update(Outbound).where(Outbound.order_ref == old_no)
|
||||
.values(order_ref=order_no))
|
||||
log_action("编辑生产订单", order_no,
|
||||
f"接单量 {qty}; 订单号 {old_no}→{order_no}, 已同步关联流水 order_ref"
|
||||
+ (f"; 备份 {os.path.basename(_bak)}" if _bak else ""))
|
||||
flash(f"订单已更新(订单号 {old_no}→{order_no}),调入数量与已入账流水保持不变", "ok")
|
||||
else:
|
||||
log_action("编辑生产订单", order_no, f"接单量 {qty}; 订单头字段更新(其余数据不变)")
|
||||
flash("订单已更新,调入数量与已入账流水保持不变", "ok")
|
||||
db.session.commit()
|
||||
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)
|
||||
|
||||
@@ -1605,34 +1631,123 @@ def order_delete(oid):
|
||||
return redirect(url_for("orders"))
|
||||
|
||||
|
||||
def _reset_order_to_placed(o, order_no):
|
||||
"""把订单冲销回「刚下单」状态: 撤销该订单关联的调入入库 / 调出 / 领料流水, 清空调入计划, 状态置回 planned。
|
||||
def _find_posted_flows(order_no, material_id):
|
||||
"""返回某生产订单某物料已入账调入流水(源厂出库 + 生产厂入库)。"""
|
||||
outs = db.session.scalars(db.select(Outbound).where(
|
||||
Outbound.order_ref == order_no, Outbound.material_id == material_id,
|
||||
Outbound.purpose == "生产订单调出")).all()
|
||||
ins = db.session.scalars(db.select(Inbound).where(
|
||||
Inbound.order_ref == order_no, Inbound.material_id == material_id,
|
||||
Inbound.source == "生产订单调入")).all()
|
||||
return outs, ins
|
||||
|
||||
- 调入入库(Inbound, source=生产订单调入) 与 调出/领料(Outbound, purpose in (生产领用, 生产订单调出)) 均按 order_ref 关联, 直接删除即冲销(总库存/厂间余额恢复)。
|
||||
- 调入计划(prod_order_alloc) 按 order_id 清除。
|
||||
- 返回 (冲销入库笔数, 冲销出库笔数, 清除计划行数)。
|
||||
|
||||
def _cancel_posted(o, material_id, op):
|
||||
"""撤销某物料已入账的调入流水(删除源厂出库 + 生产厂入库), 不改变 alloc 行本身。"""
|
||||
outs, ins = _find_posted_flows(o.order_no, material_id)
|
||||
for x in outs + ins:
|
||||
db.session.delete(x)
|
||||
if outs or ins:
|
||||
log_action("撤销调入", o.order_no,
|
||||
f"物料ID {material_id} 撤销原调入流水 出{len(outs)}/入{len(ins)} 笔")
|
||||
return len(outs), len(ins)
|
||||
|
||||
|
||||
def _repost(o, material_id, qty, from_id, op, ff):
|
||||
"""按指定源厂重新生成调入流水(源厂出库减 + 生产厂入库加, 总库存不变)。"""
|
||||
db.session.add(Outbound(
|
||||
material_id=material_id, factory_id=from_id, qty=qty,
|
||||
operator=op, purpose="生产订单调出", order_ref=o.order_no,
|
||||
occur_date=date.today(),
|
||||
remark=f"生产订单 {o.order_no} 调入(自 {ff.code})"))
|
||||
db.session.add(Inbound(
|
||||
material_id=material_id, factory_id=o.factory_id, qty=qty,
|
||||
operator=op, source="生产订单调入", order_ref=o.order_no,
|
||||
occur_date=date.today(),
|
||||
remark=f"生产订单 {o.order_no} 调入(自 {ff.code})"))
|
||||
|
||||
|
||||
def _apply_alloc_change(o, material_id, new_qty, op, from_id=None, account_new=False):
|
||||
"""把某物料的「调入数量」调整到 new_qty, 并保证实际库存流水与之一致。
|
||||
|
||||
返回状态: 'unchanged' / 'planned' / 'posted' / 'cancel' / 'short' / 'nosource'
|
||||
- 未入账行: account_new=True 且给定 from_id(≠生产厂) 时直接入账; 否则仅更新计划(不生成流水)。
|
||||
- 已入账行: 若 new_qty 与已入账数量不同, 先撤销原流水, 再从原始来源厂重新入账(或归零撤销)。
|
||||
源厂库存校验在撤销前做(避免撤销后因源厂不足而无法回补、丢失调入)。
|
||||
"""
|
||||
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
|
||||
new_qty = max(float(new_qty), 0.0)
|
||||
a = db.session.scalar(db.select(ProdOrderAlloc).where(
|
||||
ProdOrderAlloc.order_id == o.id,
|
||||
ProdOrderAlloc.material_id == material_id))
|
||||
posted = bool(a and a.posted)
|
||||
changed = (a is None) or (abs((a.alloc_qty or 0) - new_qty) > 1e-9)
|
||||
|
||||
if new_qty <= 0:
|
||||
if posted:
|
||||
_cancel_posted(o, material_id, op)
|
||||
a.alloc_qty = 0
|
||||
a.posted = False
|
||||
return "cancel"
|
||||
if a is not None:
|
||||
a.alloc_qty = 0 # 未入账计划归零(保留行)
|
||||
return "planned"
|
||||
|
||||
if posted:
|
||||
if not changed:
|
||||
return "unchanged"
|
||||
outs, _ = _find_posted_flows(o.order_no, material_id)
|
||||
if not outs:
|
||||
return "nosource"
|
||||
src_fid = outs[0].factory_id
|
||||
old_qty = outs[0].qty
|
||||
# 撤销后源厂可用 = 当前源厂库存(仍含原调出扣减) + 原调出量
|
||||
available = stock_map(src_fid).get(material_id, 0) + old_qty
|
||||
if new_qty > available:
|
||||
return "short" # 源厂不足, 保留原入账不动
|
||||
_cancel_posted(o, material_id, op)
|
||||
ff = db.session.get(Factory, src_fid)
|
||||
_repost(o, material_id, new_qty, src_fid, op, ff)
|
||||
a.alloc_qty = new_qty
|
||||
a.posted = True
|
||||
return "posted"
|
||||
|
||||
# 未入账
|
||||
if account_new and from_id is not None and from_id != (o.factory_id or -1):
|
||||
src_stock = stock_map(from_id).get(material_id, 0)
|
||||
if new_qty > src_stock:
|
||||
return "short"
|
||||
ff = db.session.get(Factory, from_id)
|
||||
_repost(o, material_id, new_qty, from_id, op, ff)
|
||||
if a is None:
|
||||
a = ProdOrderAlloc(order_id=o.id, material_id=material_id)
|
||||
db.session.add(a)
|
||||
a.alloc_qty = new_qty
|
||||
a.posted = True
|
||||
return "posted"
|
||||
# 仅更新计划, 不生成流水
|
||||
if a is None:
|
||||
a = ProdOrderAlloc(order_id=o.id, material_id=material_id, alloc_qty=new_qty)
|
||||
db.session.add(a)
|
||||
else:
|
||||
a.alloc_qty = new_qty
|
||||
return "planned"
|
||||
|
||||
|
||||
@app.route("/order/<int:oid>/alloc", methods=["POST"])
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
def order_alloc_save(oid):
|
||||
"""保存生产订单各物料的「调入数量」(手动录入, 不生成流水)。"""
|
||||
"""保存生产订单各物料的「调入数量」。
|
||||
|
||||
- 未入账行: 更新计划(不生成流水)。
|
||||
- 已入账(已调入)行: 若数量被改动, 自动撤销原流水并按原始来源厂重新入账(最终调入数量=确认后的数量); 改为 0 则撤销调入。
|
||||
"""
|
||||
o = db.session.get(ProdOrder, oid)
|
||||
if not o:
|
||||
return redirect(url_for("orders"))
|
||||
op = g.user.display_name or g.user.username
|
||||
items = db.session.scalars(db.select(BomItem).where(BomItem.bom_id == o.bom_id)).all()
|
||||
edited = 0
|
||||
for it in items:
|
||||
raw = request.form.get(f"alloc_{it.material_id}", "").strip()
|
||||
if raw == "":
|
||||
@@ -1643,17 +1758,15 @@ def order_alloc_save(oid):
|
||||
continue
|
||||
if qty < 0:
|
||||
qty = 0
|
||||
a = db.session.scalar(db.select(ProdOrderAlloc).where(
|
||||
ProdOrderAlloc.order_id == oid,
|
||||
ProdOrderAlloc.material_id == it.material_id))
|
||||
if a is None:
|
||||
a = ProdOrderAlloc(order_id=oid, material_id=it.material_id, alloc_qty=qty)
|
||||
db.session.add(a)
|
||||
else:
|
||||
a.alloc_qty = qty
|
||||
st = _apply_alloc_change(o, it.material_id, qty, op, account_new=False)
|
||||
if st in ("posted", "cancel"):
|
||||
edited += 1
|
||||
db.session.commit()
|
||||
log_action("保存调入数量", o.order_no, "更新调入计划")
|
||||
flash("已保存调入数量", "ok")
|
||||
if edited:
|
||||
log_action("保存调入数量", o.order_no, f"已调整 {edited} 项已入账调入(撤销并重入)")
|
||||
flash(f"已保存调入数量(其中 {edited} 项已入账调入已按新值调整)", "ok")
|
||||
else:
|
||||
flash("已保存调入数量", "ok")
|
||||
return redirect(url_for("order_detail", oid=oid))
|
||||
|
||||
|
||||
@@ -1661,23 +1774,22 @@ def order_alloc_save(oid):
|
||||
@login_required
|
||||
@role_required("admin", "manager")
|
||||
def order_alloc_post(oid):
|
||||
"""确认调入: 这是一次「调拨」——从选定的源工厂出库减, 同时计入生产工厂入库加, 总库存不变。
|
||||
"""确认调入: 厂间调拨(源厂出库减 + 生产厂入库加, 总库存不变)。
|
||||
|
||||
与「保存调入数量」共用同一表单(formaction 提交), 一次点击即可先记录再入账。
|
||||
源工厂由表单 from_factory_id 指定(默认自厂 SELF); 自动校验源厂库存, 不足则跳过并提示。
|
||||
与「保存调入数量」共用同一表单(formaction 提交)。
|
||||
- 未入账行: 按所选来源厂入账。
|
||||
- 已入账(已调入)行: 若数量被改动, 先撤销原流水、再从原始来源厂重新入账; 改为 0 则撤销调入。
|
||||
源厂不足时跳过并提示, 原入账保持不变。
|
||||
"""
|
||||
o = db.session.get(ProdOrder, oid)
|
||||
if not o:
|
||||
return redirect(url_for("orders"))
|
||||
# 解析源工厂
|
||||
# 解析源工厂(用于未入账行的首次入账)
|
||||
from_raw = (request.form.get("from_factory_id") or "").strip()
|
||||
if not from_raw.isdigit():
|
||||
flash("请先选择「调入来源工厂」", "error")
|
||||
return redirect(url_for("order_detail", oid=oid))
|
||||
from_id = int(from_raw)
|
||||
if from_id == (o.factory_id or -1):
|
||||
flash("调入来源工厂不能与生产工厂相同", "error")
|
||||
return redirect(url_for("order_detail", oid=oid))
|
||||
ff = db.session.get(Factory, from_id)
|
||||
if not ff:
|
||||
flash("调入来源工厂不存在", "error")
|
||||
@@ -1698,36 +1810,23 @@ def order_alloc_post(oid):
|
||||
a = db.session.scalar(db.select(ProdOrderAlloc).where(
|
||||
ProdOrderAlloc.order_id == oid,
|
||||
ProdOrderAlloc.material_id == it.material_id))
|
||||
if qty is None and a and not a.posted and a.alloc_qty > 0:
|
||||
qty = a.alloc_qty # 回退到已保存计划
|
||||
if qty is None or qty <= 0:
|
||||
continue
|
||||
if a and a.posted:
|
||||
continue # 已入账, 跳过避免重复
|
||||
# 校验源厂库存
|
||||
src_stock = stock_map(from_id).get(it.material_id, 0)
|
||||
if qty > src_stock:
|
||||
if qty is None:
|
||||
if a and a.posted:
|
||||
qty = a.alloc_qty # 已入账且无输入 = 维持, helper 判定 unchanged 跳过
|
||||
else:
|
||||
continue # 未入账且无输入 = 不处理
|
||||
if qty < 0:
|
||||
qty = 0
|
||||
st = _apply_alloc_change(o, it.material_id, qty, op,
|
||||
from_id=from_id, account_new=True)
|
||||
if st == "posted":
|
||||
n += 1
|
||||
elif st == "short":
|
||||
m = db.session.get(Material, it.material_id)
|
||||
shorts.append(f"{m.part_number}(源厂{ff.code}库存{src_stock:.2f}, 需{qty:.2f})")
|
||||
continue
|
||||
if a is None:
|
||||
a = ProdOrderAlloc(order_id=oid, material_id=it.material_id)
|
||||
db.session.add(a)
|
||||
a.alloc_qty = qty
|
||||
# 源厂出库减
|
||||
db.session.add(Outbound(
|
||||
material_id=it.material_id, factory_id=from_id, qty=qty,
|
||||
operator=op, purpose="生产订单调出", order_ref=o.order_no,
|
||||
occur_date=date.today(),
|
||||
remark=f"生产订单 {o.order_no} 调入(自 {ff.code})"))
|
||||
# 生产厂入库加
|
||||
db.session.add(Inbound(
|
||||
material_id=it.material_id, factory_id=o.factory_id, qty=qty,
|
||||
operator=op, source="生产订单调入", order_ref=o.order_no,
|
||||
occur_date=date.today(),
|
||||
remark=f"生产订单 {o.order_no} 调入(自 {ff.code})"))
|
||||
a.posted = True
|
||||
n += 1
|
||||
if a and a.posted:
|
||||
shorts.append(f"{m.part_number}(已入账调入修改, 源厂{ff.code}库存不足)")
|
||||
else:
|
||||
shorts.append(f"{m.part_number}(源厂{ff.code}库存不足)")
|
||||
db.session.commit()
|
||||
if n == 0 and not shorts:
|
||||
flash("没有新的调入数量待确认(数量需>0, 且不能重复入账)", "warn")
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ button:hover, .btn:hover { filter: brightness(.96); }
|
||||
.row-danger { background: #fff1f0; }
|
||||
.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.warn { background: #fff7e6; color: #ad6800; }
|
||||
.badge.danger { background: #fff1f0; color: #cf1322; }
|
||||
|
||||
@@ -8,38 +8,41 @@
|
||||
</div>
|
||||
<div class="card" style="margin-bottom:16px">
|
||||
<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>
|
||||
<p>BOM: <b>{{ bom.code if bom else '-' }}</b> ({{ bom.name if bom else '' }}) | 接单量: <b>{{ '%.2f'|format(o.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 %}
|
||||
<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>
|
||||
<h3>领料需求与调入 (单套用量 × 接单量){% 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) }}">
|
||||
<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><th>调入数量</th><th>状态</th><th>工厂库存</th><th>可替代料</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in reqs %}
|
||||
<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>{{ 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.gap) }}</td>
|
||||
<td class="num">
|
||||
{% 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="number" step="0.01" min="0"
|
||||
value="{{ '%.2f'|format(r.alloc) }}" style="width:96px">
|
||||
{% else %}
|
||||
<input name="alloc_{{ r.m.id }}" type="number" step="0.01" min="0"
|
||||
value="{{ '%.2f'|format(r.alloc) if r.alloc else '' }}"
|
||||
placeholder="{{ '%.2f'|format(r.need) }}" style="width:96px">
|
||||
{% endif %}
|
||||
</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 class="num">{{ '%.2f'|format(r.gap + r.alloc) }}</td>
|
||||
<td class="small">
|
||||
{% if r.subs %}
|
||||
{% for s in r.subs %}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% 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') }}"
|
||||
{% if o %}onsubmit="return confirm('保存将把该订单冲销回「刚下单」状态(撤销已调入/领料流水、清空调入计划),确认继续?');"{% endif %}>
|
||||
{% 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">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<td>{{ d.o.order_no }}</td>
|
||||
<td>{{ d.bom.code if d.bom else '-' }} ({{ d.bom.name if d.bom else '' }})</td>
|
||||
<td class="num">{{ '%.2f'|format(d.o.qty) }}</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>{{ d.o.operator or '-' }}</td>
|
||||
<td class="row-actions">
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
<label>所属工厂 *</label>
|
||||
<select name="factory_id" required>
|
||||
{% 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 %}
|
||||
</select>
|
||||
<label>批次号</label>
|
||||
|
||||
Reference in New Issue
Block a user