Files
Inventory-Management-System/templates/order_detail.html
T
star 099496dec9 feat: 生产订单新增导出 Excel 与打印功能
- 订单列表页「导出 Excel」(/orders/export): 导出全部生产订单(订单号/BOM/接单量/工厂/状态/经手人/下单时间/备注)

- 订单详情页「导出 Excel」(/order/<id>/export): 导出派工单, 含「订单信息」+「领料明细」双 sheet

- 订单详情页「打印」按钮: window.print() + @media print 样式(隐藏导航/按钮, 输入框转纯文本, 隐藏提示), 输出干净派工单
2026-10-22 14:02:04 +08:00

136 lines
7.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}订单 {{ o.order_no }}{% endblock %}
{% 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">
<h3 style="margin:0">生产订单 {{ o.order_no }}</h3>
<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_export', oid=o.id) }}">导出 Excel</a>
<button class="btn" type="button" onclick="window.print()">打印</button>
</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>{% 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 %}
<p class="small muted print-hide" style="margin-bottom:0">提示:点击「编辑订单」保存后,该订单会冲销已产生的调入/领料流水并清空调入计划,自动回到「刚下单」状态(待领料、未调拨),可重新规划。</p>
</div>
<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>
</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 %}
<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 %}
<input name="alloc_{{ r.m.id }}" type="text" inputmode="decimal"
value="{{ '%.2f'|format(r.alloc) if r.alloc else '' }}"
placeholder="{{ '%.2f'|format(r.need) }}" style="width:96px">
{% endif %}
</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 %}
<span class="{{ 'badge ok' if s.available else 'badge danger' }}">{{ s.pn }}·{{ s.stock }}{% if s.available %}✓{% else %}✗{% endif %}</span>
{% endfor %}
{% else %}<span class="muted">—</span>{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="8" class="muted">该 BOM 没有组成物料</td></tr>
{% endfor %}
</tbody>
</table>
<div class="row-actions" style="flex-wrap:wrap; align-items:flex-end">
<label style="display:flex; flex-direction:column; gap:4px; font-size:13px">
调入来源工厂
<select name="from_factory_id" style="padding:6px 8px; min-width:160px">
{% for f in factories %}
{% if f.id != (o.factory_id or 0) %}
<option value="{{ f.id }}" {{ 'selected' if f.code=='SELF' else '' }}>{{ f.code }}({{ f.name or '—' }})</option>
{% endif %}
{% endfor %}
</select>
</label>
<button class="primary" type="submit">保存调入数量</button>
<button class="secondary" type="submit"
formaction="{{ url_for('order_alloc_post', oid=o.id) }}"
onclick="return confirm('确认将待调入数量从上方所选来源工厂调拨到生产工厂? 总库存不变, 仅厂间转移。');">确认调入(厂间调拨)</button>
</div>
<p class="small muted print-hide">「确认调入」是一次厂间调拨:从来源工厂出库减、生产工厂入库加,系统总库存不变。</p>
</form>
<div class="card" style="margin:14px 0">
<b>调入总览:</b> 已调入 <b>{{ posted_count }}</b> 项,合计 <b>{{ '%.2f'|format(posted_total) }}</b> 件;
计划调入合计 <b>{{ '%.2f'|format(planned_total) }}</b> 件。
</div>
{% if o.status != 'picked' %}
<form method="post" action="{{ url_for('order_pick', oid=o.id) }}" onsubmit="return confirm('确认按 BOM 生成领料出库? 库存不足将记为负并在看板预警。');">
<div class="row-actions"><button class="primary">确认领料出库</button></div>
</form>
{% else %}
<p class="flash ok">该订单已领料出库。</p>
{% 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 %}