feat: 生产订单新增导出 Excel 与打印功能
- 订单列表页「导出 Excel」(/orders/export): 导出全部生产订单(订单号/BOM/接单量/工厂/状态/经手人/下单时间/备注) - 订单详情页「导出 Excel」(/order/<id>/export): 导出派工单, 含「订单信息」+「领料明细」双 sheet - 订单详情页「打印」按钮: window.print() + @media print 样式(隐藏导航/按钮, 输入框转纯文本, 隐藏提示), 输出干净派工单
This commit is contained in:
@@ -1468,6 +1468,23 @@ def orders():
|
|||||||
return render_template("orders.html", data=data)
|
return render_template("orders.html", data=data)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/orders/export")
|
||||||
|
@login_required
|
||||||
|
@role_required("admin", "manager")
|
||||||
|
def orders_export():
|
||||||
|
"""导出全部生产订单为 Excel。"""
|
||||||
|
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)
|
||||||
|
fac = db.session.get(Factory, o.factory_id) if o.factory_id else None
|
||||||
|
data.append({"o": o, "bom": bom, "factory": fac})
|
||||||
|
from excel_io import export_orders
|
||||||
|
path = os.path.join(EXE_DIR, f"生产订单_{date.today().isoformat()}.xlsx")
|
||||||
|
export_orders(path, data)
|
||||||
|
return send_file(path, as_attachment=True, download_name=os.path.basename(path))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/order/new", methods=["GET", "POST"])
|
@app.route("/order/new", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@role_required("admin", "manager")
|
@role_required("admin", "manager")
|
||||||
@@ -1540,6 +1557,44 @@ def order_detail(oid):
|
|||||||
posted_count=posted_count)
|
posted_count=posted_count)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/order/<int:oid>/export")
|
||||||
|
@login_required
|
||||||
|
@role_required("admin", "manager")
|
||||||
|
def order_export(oid):
|
||||||
|
"""导出单个生产订单详情(派工单)为 Excel。"""
|
||||||
|
o = db.session.get(ProdOrder, oid)
|
||||||
|
if not o:
|
||||||
|
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_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(
|
||||||
|
db.select(ProdOrderAlloc).where(ProdOrderAlloc.order_id == oid)).all()
|
||||||
|
alloc_map = {a.material_id: a for a in allocs}
|
||||||
|
reqs = []
|
||||||
|
for it in items:
|
||||||
|
m = db.session.get(Material, it.material_id)
|
||||||
|
need = it.qty_per_unit * o.qty
|
||||||
|
have = sm.get(m.id, 0)
|
||||||
|
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, None)]
|
||||||
|
reqs.append({"m": m, "need": need, "have": have,
|
||||||
|
"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)
|
||||||
|
from excel_io import export_order_detail
|
||||||
|
path = os.path.join(EXE_DIR, f"生产订单_{o.order_no}.xlsx")
|
||||||
|
export_order_detail(path, o, bom, reqs, fac, posted_total, planned_total, posted_count)
|
||||||
|
return send_file(path, as_attachment=True, download_name=os.path.basename(path))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/order/<int:oid>/pick", methods=["POST"])
|
@app.route("/order/<int:oid>/pick", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@role_required("admin", "manager")
|
@role_required("admin", "manager")
|
||||||
|
|||||||
+93
@@ -327,6 +327,99 @@ def export_ledger(target, rows):
|
|||||||
wb.save(target)
|
wb.save(target)
|
||||||
|
|
||||||
|
|
||||||
|
def export_orders(target, data):
|
||||||
|
"""导出生产订单列表。
|
||||||
|
|
||||||
|
data: [{o, bom, factory}] (与 orders() 返回一致)。
|
||||||
|
"""
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "生产订单"
|
||||||
|
head = ["订单号", "BOM型号", "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"]
|
||||||
|
bom = d["bom"]
|
||||||
|
fac = d["factory"]
|
||||||
|
status = "已领料" if o.status == "picked" else "待领料"
|
||||||
|
ws.append([
|
||||||
|
o.order_no,
|
||||||
|
bom.code if bom else "",
|
||||||
|
bom.name if bom else "",
|
||||||
|
o.qty,
|
||||||
|
(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 "",
|
||||||
|
])
|
||||||
|
if o.status == "picked":
|
||||||
|
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="C6EFCE")
|
||||||
|
else:
|
||||||
|
ws.cell(row=ws.max_row, column=6).fill = PatternFill("solid", fgColor="FFE699")
|
||||||
|
ws.freeze_panes = "A2"
|
||||||
|
wb.save(target)
|
||||||
|
|
||||||
|
|
||||||
|
def export_order_detail(target, o, bom, reqs, factory,
|
||||||
|
posted_total, planned_total, posted_count):
|
||||||
|
"""导出单个生产订单详情(派工单):订单信息 sheet + 领料明细 sheet。"""
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "订单信息"
|
||||||
|
ws.append(["生产订单派工单"])
|
||||||
|
ws["A1"].font = Font(bold=True, size=14)
|
||||||
|
info = [
|
||||||
|
("订单号", o.order_no),
|
||||||
|
("BOM型号", (bom.code if bom else "-") + (f" · {bom.name}" if bom and bom.name else "")),
|
||||||
|
("接单量", o.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)
|
||||||
|
ws.cell(row=r, column=2, value=v)
|
||||||
|
r += 1
|
||||||
|
|
||||||
|
ws2 = wb.create_sheet("领料明细")
|
||||||
|
head = ["物料编号", "型号", "下单前库存", "需求数量", "缺口",
|
||||||
|
"调入数量", "状态", "工厂库存", "可替代料"]
|
||||||
|
ws2.append(head)
|
||||||
|
for c in ws2[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 "—"
|
||||||
|
ws2.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"]:
|
||||||
|
ws2.cell(row=ws2.max_row, column=7).fill = PatternFill("solid", fgColor="FFC7CE")
|
||||||
|
else:
|
||||||
|
ws2.cell(row=ws2.max_row, column=7).fill = PatternFill("solid", fgColor="C6EFCE")
|
||||||
|
ws2.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/
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
{% 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 class="small muted" style="margin-top:0"><b>下单信息(按下单时记录)</b></p>
|
<p class="small muted" style="margin-top:0"><b>下单信息(按下单时记录)</b></p>
|
||||||
<p>BOM: <b>{{ bom.code if bom else '-' }}</b>{% if bom and bom.name %} · {{ bom.name }}{% endif %} | 接单量: <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>
|
<p>BOM: <b>{{ bom.code if bom else '-' }}</b>{% if bom and bom.name %} · {{ bom.name }}{% endif %} | 接单量: <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 %}
|
{% if o.remark %}<p class="small muted">备注: {{ o.remark }}</p>{% endif %}
|
||||||
<p class="small muted" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
|
<p class="small muted print-hide" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3>领料需求与调入 (单套用量 × 接单量){% if factory %} | 按下单前库存核算{% endif %}</h3>
|
<h3>领料需求与调入 (单套用量 × 接单量){% if factory %} | 按下单前库存核算{% endif %}</h3>
|
||||||
@@ -73,7 +93,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">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<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>
|
||||||
|
|||||||
Reference in New Issue
Block a user