init: 博特维采购订单系统 v1.0 源码

This commit is contained in:
star
2026-09-10 16:18:15 +08:00
commit 6327ef7641
35 changed files with 4029 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>{% block title %}采购订单管理系统{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
{% if session.get('user_id') %}
<div class="topbar">
<span class="brand">博特维 · 采购订单管理</span>
<nav style="display:flex;gap:18px;">
<a href="{{ url_for('index') }}">工作台</a>
<a href="{{ url_for('orders') }}">订单列表</a>
<a href="{{ url_for('order_new') }}">新建订单</a>
<a href="{{ url_for('suppliers') }}">供应商</a>
<a href="{{ url_for('materials') }}">物料库</a>
<a href="{{ url_for('stats') }}">统计</a>
{% if session.get('role') == 'admin' %}<a href="{{ url_for('users') }}">用户</a>{% endif %}
</nav>
<span class="spacer"></span>
<span class="me">{{ session.get('user_name') }}({{ ROLE_TEXT.get(session.get('role'), '') }})</span>
<a href="{{ url_for('logout') }}" style="color:#d8e6f7;">退出</a>
</div>
{% endif %}
<div class="wrap">
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, msg in msgs %}
<div class="flash {{ cat }}">{{ msg }}</div>
{% endfor %}
{% endwith %}
{% block body %}{% endblock %}
</div>
<footer class="foot">{{ COMPANY_NAME }} · 采购订单管理系统 v1.0</footer>
</body>
</html>
+50
View File
@@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">工作台</h1>
<div class="stat-row">
<div class="stat"><div class="v">{{ counts.get('pending_review', 0) }}</div><div class="k">待主管审核</div></div>
<div class="stat"><div class="v">{{ counts.get('pending_approval', 0) }}</div><div class="k">待经理批准</div></div>
<div class="stat"><div class="v">{{ counts.get('approved', 0) }}</div><div class="k">已批准</div></div>
<div class="stat"><div class="v">{{ counts.get('draft', 0) }}</div><div class="k">草稿</div></div>
<div class="stat"><div class="v">{{ '%.2f'|format(month_total) }}</div><div class="k">已批准订单总额(元)</div></div>
</div>
{% if todo %}
<div class="card">
<h2>待我处理({{ todo|length }})</h2>
<table class="grid">
<tr><th>单号</th><th>类型</th><th>供应商</th><th>金额</th><th>状态</th><th>提交时间</th><th>操作</th></tr>
{% for o in todo %}
<tr>
<td><a href="{{ url_for('order_view', oid=o.id) }}">{{ o.order_no }}</a></td>
<td>{{ o.doc_type_text }}</td>
<td>{{ o.supplier_name }}</td>
<td class="num">{{ '%.2f'|format(o.total_amount) }}</td>
<td><span class="badge {{ o.status }}">{{ o.status_text }}</span></td>
<td>{{ o.submitted_at.strftime('%m-%d %H:%M') if o.submitted_at else '-' }}</td>
<td><a class="btn sm primary" href="{{ url_for('order_view', oid=o.id) }}">去处理</a></td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
<div class="card">
<h2>我的订单</h2>
{% if my_orders %}
<table class="grid">
<tr><th>单号</th><th>类型</th><th>供应商</th><th>金额</th><th>状态</th><th>创建时间</th></tr>
{% for o in my_orders %}
<tr>
<td><a href="{{ url_for('order_view', oid=o.id) }}">{{ o.order_no }}</a></td>
<td>{{ o.doc_type_text }}</td>
<td>{{ o.supplier_name }}</td>
<td class="num">{{ '%.2f'|format(o.total_amount) }}</td>
<td><span class="badge {{ o.status }}">{{ o.status_text }}</span></td>
<td>{{ o.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</table>
{% else %}<p class="muted">暂无订单,点右上角「新建订单」开始。</p>{% endif %}
</div>
{% endblock %}
+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>登录 · 采购订单管理系统</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="login-box">
<h2>博特维采购订单管理系统</h2>
{% with msgs = get_flashed_messages(with_categories=true) %}
{% for cat, msg in msgs %}<div class="flash {{ cat }}">{{ msg }}</div>{% endfor %}
{% endwith %}
<form method="post">
<div class="field">
<label>用户名</label>
<input type="text" name="username" autofocus>
</div>
<div class="field">
<label>密码</label>
<input type="password" name="password">
</div>
<button class="btn primary" style="width:100%;">登 录</button>
</form>
<div class="hint" style="margin-top:14px;">
演示账号:admin / admin123(管理员),maker / 123456(制单员),
reviewer / 123456(主管审核),approver / 123456(经理批准)
</div>
</div>
</body>
</html>
+125
View File
@@ -0,0 +1,125 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">物料库</h1>
<div class="card">
<h2>新增物料</h2>
<form method="post">
<div class="form-grid">
<div><label>物料代码 *</label><input type="text" name="code" required placeholder="M-1004"></div>
<div><label>物料规格 / 名称</label><input type="text" name="spec"></div>
<div><label>单位</label><input type="text" name="unit" placeholder="件"></div>
<div><label>默认含税单价</label><input type="number" step="0.01" name="price" value="0"></div>
</div>
<div style="margin-top:12px;"><button class="btn primary">新增</button></div>
</form>
</div>
<div class="card">
<div class="card-head">
<h2>物料列表</h2>
<button type="button" class="btn primary sm" onclick="document.getElementById('matImpModal').style.display='flex'">批量导入 Excel</button>
</div>
<table class="grid">
<tr><th>物料代码</th><th>物料规格</th><th>单位</th><th class="num">默认含税单价</th><th>操作</th></tr>
{% for m in rows %}
<tr class="{% if not m.active %}row-off{% endif %}">
<td>{{ m.code }}</td><td>{{ m.spec }}</td><td class="center">{{ m.unit }}</td>
<td class="num">{{ '%.2f'|format(m.price) }}</td>
<td class="nowrap">
<button type="button" class="btn sm" onclick="openEdit(this)"
data-id="{{ m.id }}" data-code="{{ m.code }}"
data-spec="{{ m.spec or '' }}" data-unit="{{ m.unit or '' }}"
data-price="{{ m.price or 0 }}">编辑</button>
<form method="post" action="{{ url_for('material_toggle', mid=m.id) }}" style="display:inline;">
<button class="btn sm {% if m.active %}danger{% endif %}"
onclick="return confirm('确定{{ '停用' if m.active else '启用' }}该物料?')">
{{ '停用' if m.active else '启用' }}
</button>
</form>
{% if user.role == 'admin' %}
<form method="post" action="{{ url_for('material_delete', mid=m.id) }}" style="display:inline;">
<button class="btn sm danger"
onclick="return confirm('确定彻底删除该物料?删除后不可恢复!')">删除</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div id="matImpModal" class="modal-mask" style="display:none;" onclick="if(event.target===this) this.style.display='none'">
<div class="modal-box">
<div class="modal-head">
<span>批量导入物料</span>
<span class="modal-close" onclick="document.getElementById('matImpModal').style.display='none'">&times;</span>
</div>
<form method="post" action="{{ url_for('material_import_excel') }}" enctype="multipart/form-data">
<div class="modal-body">
<div class="hint">先下载模板按格式填好再导入。<b>物料代码</b>必填;已存在的代码按下方方式处理。</div>
<div class="field">
<label>1. 下载模板</label>
<a href="{{ url_for('material_import_template') }}">物料导入模板.xlsx</a>
</div>
<div class="field">
<label>2. 选择 Excel 文件(.xls / .xlsx)</label>
<input type="file" name="file" accept=".xls,.xlsx,.xlsm" required>
</div>
<div class="field">
<label>3. 遇到已存在的物料代码</label>
<select name="mode">
<option value="update">更新该物料的规格 / 单位 / 单价</option>
<option value="skip">跳过,不改动已有数据</option>
</select>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn" onclick="document.getElementById('matImpModal').style.display='none'">取消</button>
<button type="submit" class="btn primary">开始导入</button>
</div>
</form>
</div>
</div>
<div id="editModal" class="modal-mask" style="display:none;" onclick="if(event.target===this) this.style.display='none'">
<div class="modal-box">
<div class="modal-head">
<span>编辑物料</span>
<span class="modal-close" onclick="document.getElementById('editModal').style.display='none'">&times;</span>
</div>
<form method="post" action="{{ url_for('materials') }}">
<input type="hidden" name="id" id="edit_id">
<div class="modal-body">
<div class="field">
<label>物料代码 *</label>
<input type="text" name="code" id="edit_code" required>
</div>
<div class="field">
<label>物料规格 / 名称</label>
<input type="text" name="spec" id="edit_spec">
</div>
<div class="form-grid two">
<div><label>单位</label><input type="text" name="unit" id="edit_unit"></div>
<div><label>默认含税单价</label><input type="number" step="0.01" name="price" id="edit_price" value="0"></div>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn" onclick="document.getElementById('editModal').style.display='none'">取消</button>
<button type="submit" class="btn primary">保存</button>
</div>
</form>
</div>
</div>
<script>
function openEdit(btn){
const d = btn.dataset;
document.getElementById('edit_id').value = d.id || '';
document.getElementById('edit_code').value = d.code || '';
document.getElementById('edit_spec').value = d.spec || '';
document.getElementById('edit_unit').value = d.unit || '';
document.getElementById('edit_price').value = d.price || 0;
document.getElementById('editModal').style.display = 'flex';
}
</script>
{% endblock %}
+201
View File
@@ -0,0 +1,201 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">编辑订单 · {{ o.order_no }}({{ o.doc_type_text }})</h1>
<div class="card">
<h2>单据信息</h2>
<form method="post" id="orderForm">
<div class="form-grid">
<div><label>采购单号</label><input type="text" value="{{ o.order_no }}" disabled></div>
<div><label>采购日期</label><input type="date" name="order_date" value="{{ o.order_date or '' }}"></div>
<div><label>税点</label><input type="text" name="tax_rate" value="{{ o.tax_rate or '' }}" placeholder="如 13%"></div>
</div>
<h3>供方(供应商)</h3>
<div class="form-grid">
<div>
<label>供应商</label>
<input type="text" name="supplier_name" id="supplier_name" value="{{ o.supplier_name or '' }}">
<select id="supplierPick" style="margin-top:4px;">
<option value="">-- 从供应商库带入 --</option>
{% for s in suppliers %}<option value="{{ s.id }}">{{ s.name }}</option>{% endfor %}
</select>
</div>
<div><label>联系人</label><input type="text" name="supplier_contact" id="supplier_contact" value="{{ o.supplier_contact or '' }}"></div>
<div><label>电话</label><input type="text" name="supplier_phone" id="supplier_phone" value="{{ o.supplier_phone or '' }}"></div>
<div><label>付款方式</label><input type="text" name="payment_terms" id="payment_terms" value="{{ o.payment_terms or '' }}"></div>
<div><label>税号</label><input type="text" name="supplier_tax_no" id="supplier_tax_no" value="{{ o.supplier_tax_no or '' }}"></div>
<div><label>银行帐号</label><input type="text" name="supplier_bank_account" id="supplier_bank_account" value="{{ o.supplier_bank_account or '' }}"></div>
<div><label>开户银行</label><input type="text" name="supplier_bank_name" id="supplier_bank_name" value="{{ o.supplier_bank_name or '' }}"></div>
</div>
<h3>需方(本公司)</h3>
<div class="form-grid">
<div><label>需方名称</label><input type="text" name="our_company" value="{{ o.our_company or '' }}"></div>
<div><label>联系人</label><input type="text" name="our_contact" value="{{ o.our_contact or '' }}"></div>
<div><label>电话</label><input type="text" name="our_phone" value="{{ o.our_phone or '' }}"></div>
<div><label>税号</label><input type="text" name="our_tax_no" value="{{ o.our_tax_no or '' }}"></div>
<div style="grid-column: span 2;">
<label>交货地点</label>
<input type="text" name="delivery_place" value="{{ o.delivery_place or '' }}">
</div>
</div>
<h3>物料明细</h3>
<table class="grid" id="itemTable">
<tr>
<th style="width:38px;">序</th>
<th style="width:150px;">物料代码</th>
<th>物料规格</th>
<th style="width:62px;">单位</th>
<th style="width:78px;">数量</th>
<th style="width:92px;">含税单价</th>
<th style="width:96px;">金额</th>
<th style="width:118px;">交期</th>
<th style="width:130px;">备注</th>
<th style="width:44px;">删</th>
</tr>
{% for it in o.items %}
<tr>
<td class="center seq">{{ loop.index }}</td>
<td><input type="text" name="item_code" value="{{ it.material_code or '' }}">
<select class="matpick" style="margin-top:3px;">
<option value="">-- 物料库 --</option>
{% for m in materials %}<option value="{{ m.id }}" data-code="{{ m.code }}" data-spec="{{ m.spec }}" data-unit="{{ m.unit }}" data-price="{{ m.price }}">{{ m.code }} {{ m.spec }}</option>{% endfor %}
</select>
</td>
<td><input type="text" name="item_spec" value="{{ it.material_spec or '' }}"></td>
<td><input type="text" name="item_unit" value="{{ it.unit or '' }}"></td>
<td><input type="number" step="0.0001" name="item_qty" class="qty" value="{{ it.qty or 0 }}"></td>
<td><input type="number" step="0.0001" name="item_price" class="price" value="{{ it.price or 0 }}"></td>
<td><input type="text" class="amt" value="{{ '%.2f'|format(it.amount or 0) }}" disabled></td>
<td><input type="date" name="item_date" value="{{ it.delivery_date or '' }}"></td>
<td><input type="text" name="item_remark" value="{{ it.remark or '' }}"></td>
<td class="center"><button type="button" class="btn sm danger" onclick="delRow(this)">×</button></td>
</tr>
{% endfor %}
</table>
<template id="rowTpl">
<tr>
<td class="center seq">1</td>
<td><input type="text" name="item_code" value="">
<select class="matpick" style="margin-top:3px;">
<option value="">-- 物料库 --</option>
{% for m in materials %}<option value="{{ m.id }}" data-code="{{ m.code }}" data-spec="{{ m.spec }}" data-unit="{{ m.unit }}" data-price="{{ m.price }}">{{ m.code }} {{ m.spec }}</option>{% endfor %}
</select>
</td>
<td><input type="text" name="item_spec" value=""></td>
<td><input type="text" name="item_unit" value=""></td>
<td><input type="number" step="0.0001" name="item_qty" class="qty" value="0"></td>
<td><input type="number" step="0.0001" name="item_price" class="price" value="0"></td>
<td><input type="text" class="amt" value="0.00" disabled></td>
<td><input type="date" name="item_date" value=""></td>
<td><input type="text" name="item_remark" value=""></td>
<td class="center"><button type="button" class="btn sm danger" onclick="delRow(this)">×</button></td>
</tr>
</template>
<div class="toolbar" style="margin-top:10px;">
<button type="button" class="btn" onclick="addRow()">+ 添加一行</button>
<span class="grow"></span>
<span class="total-bar">合计:<span class="amt" id="totalAmt">{{ '%.2f'|format(o.total_amount) }}</span> 元
<span class="muted" id="totalCn">{{ total_cn }}</span></span>
</div>
<h3>补充说明</h3>
<textarea name="note" placeholder="选填,会打印在 PDF 备注区">{{ o.note or '' }}</textarea>
<div style="margin-top:16px;">
<button class="btn primary" type="submit">保存</button>
<a class="btn" href="{{ url_for('order_view', oid=o.id) }}">查看订单</a>
<a class="btn" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">预览 PDF</a>
</div>
</form>
<form method="post" action="{{ url_for('order_submit', oid=o.id) }}" style="margin-top:10px;">
<div class="toolbar">
<input type="text" name="comment" placeholder="提交说明(选填)" style="width:260px;">
<button class="btn ok">提交主管审核</button>
</div>
</form>
</div>
<div class="card">
<h2>从 Excel 导入明细</h2>
<div class="hint">
支持 .xlsx / .xls。系统自动识别表头(物料代码、物料规格、单位、数量、含税单价、交期、备注),
没有表头则按前 7 列顺序读取。行数超出模板也没关系,PDF 会自动分页。
<a href="{{ url_for('import_template') }}">下载导入模板</a>
</div>
<form method="post" action="{{ url_for('import_excel', oid=o.id) }}" enctype="multipart/form-data" class="toolbar">
<input type="file" name="file" accept=".xls,.xlsx" required style="width:280px;">
<select name="mode" style="width:140px;">
<option value="append">追加到现有明细</option>
<option value="replace">替换全部明细</option>
</select>
<button class="btn primary">导入</button>
</form>
</div>
<script>
const MATERIALS = {{ materials|map(attribute='id')|list|tojson }};
function bindRow(tr){
tr.querySelectorAll('.qty, .price').forEach(i => i.addEventListener('input', calc));
const pick = tr.querySelector('.matpick');
if (pick) pick.addEventListener('change', function(){
const opt = this.selectedOptions[0];
if (!opt || !opt.value) return;
tr.querySelector('[name=item_code]').value = opt.dataset.code || '';
tr.querySelector('[name=item_spec]').value = opt.dataset.spec || '';
tr.querySelector('[name=item_unit]').value = opt.dataset.unit || '';
tr.querySelector('.price').value = opt.dataset.price || 0;
calc();
});
}
function calc(){
let total = 0;
document.querySelectorAll('#itemTable tr').forEach((tr, idx) => {
const q = parseFloat(tr.querySelector('.qty')?.value || 0);
const p = parseFloat(tr.querySelector('.price')?.value || 0);
const a = (isNaN(q) ? 0 : q) * (isNaN(p) ? 0 : p);
const amt = tr.querySelector('.amt');
if (amt) amt.value = a.toFixed(2);
total += a;
const seq = tr.querySelector('.seq');
if (seq) seq.textContent = idx;
});
document.getElementById('totalAmt').textContent = total.toFixed(2);
}
function addRow(){
// 从 <template> 克隆,避免"明细为空时没有可复制的行"导致点击无反应
const t = document.getElementById('itemTable');
const tr = document.getElementById('rowTpl').content.firstElementChild.cloneNode(true);
tr.querySelectorAll('input').forEach(i => { if (i.type !== 'date') i.value = ''; });
const q = tr.querySelector('.qty'); if (q) q.value = 0;
const p = tr.querySelector('.price'); if (p) p.value = 0;
const a = tr.querySelector('.amt'); if (a) a.value = '0.00';
const sel = tr.querySelector('.matpick'); if (sel) sel.selectedIndex = 0;
t.appendChild(tr);
bindRow(tr); calc();
}
function delRow(btn){
btn.closest('tr').remove(); calc();
}
document.querySelectorAll('#itemTable tr').forEach(bindRow);
calc();
document.getElementById('supplierPick').addEventListener('change', async function(){
if (!this.value) return;
const r = await fetch('/api/suppliers').then(r => r.json());
const s = r.find(x => String(x.id) === this.value);
if (!s) return;
document.getElementById('supplier_name').value = s.name || '';
document.getElementById('supplier_contact').value = s.contact || '';
document.getElementById('supplier_phone').value = s.phone || '';
document.getElementById('supplier_tax_no').value = s.tax_no || '';
document.getElementById('supplier_bank_name').value = s.bank_name || '';
document.getElementById('supplier_bank_account').value = s.bank_account || '';
document.getElementById('payment_terms').value = s.payment_terms || '';
});
</script>
{% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">新建订单</h1>
<div class="card">
<div class="hint">先选单据类型与供应商,系统会带出供应商资料并生成单号,下一步填写物料明细。</div>
<form method="post">
<div class="form-grid two">
<div>
<label>单据类型</label>
<select name="doc_type">
{% for k, v in DOC_TYPES.items() %}<option value="{{ k }}">{{ v }}</option>{% endfor %}
</select>
</div>
<div>
<label>供应商(留空则手工填写)</label>
<select name="supplier_id">
<option value="">-- 手工填写 --</option>
{% for s in suppliers %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div style="margin-top:16px;">
<button class="btn primary">创建并填写明细</button>
<a class="btn" href="{{ url_for('orders') }}">取消</a>
</div>
</form>
</div>
{% endblock %}
+203
View File
@@ -0,0 +1,203 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">{{ o.doc_type_text }} · {{ o.order_no }}
<span class="badge {{ o.status }}" style="font-size:13px;vertical-align:middle;">{{ o.status_text }}</span>
</h1>
<div class="toolbar">
{% if editable %}<a class="btn primary" href="{{ url_for('order_edit', oid=o.id) }}">编辑</a>{% endif %}
<a class="btn" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">导出 / 预览 PDF</a>
<a class="btn" href="{{ url_for('order_pdf', oid=o.id) }}?dl=1" download>下载 PDF</a>
<form method="post" action="{{ url_for('order_copy', oid=o.id) }}" style="display:inline;">
<button class="btn">复制为新订单</button>
</form>
<span class="grow"></span>
<a class="btn" href="{{ url_for('orders') }}">返回列表</a>
</div>
<div class="card">
<h2>单据信息</h2>
<table class="kv-table" style="width:100%;">
<tr>
<td class="k">供应商</td><td>{{ o.supplier_name }}</td>
<td class="k">需方</td><td>{{ o.our_company }}</td>
</tr>
<tr>
<td class="k">联系人</td><td>{{ o.supplier_contact }}</td>
<td class="k">联系人</td><td>{{ o.our_contact }}</td>
</tr>
<tr>
<td class="k">电话</td><td>{{ o.supplier_phone }}</td>
<td class="k">电话</td><td>{{ o.our_phone }}</td>
</tr>
<tr>
<td class="k">付款方式</td><td>{{ o.payment_terms }}</td>
<td class="k">税点</td><td>{{ o.tax_rate }}</td>
</tr>
<tr>
<td class="k">税号</td><td>{{ o.supplier_tax_no }}</td>
<td class="k">税号</td><td>{{ o.our_tax_no }}</td>
</tr>
<tr>
<td class="k">银行帐号</td><td>{{ o.supplier_bank_account }}</td>
<td class="k">采购单号</td><td>{{ o.order_no }}</td>
</tr>
<tr>
<td class="k">开户银行</td><td>{{ o.supplier_bank_name }}</td>
<td class="k">采购日期</td><td>{{ o.order_date }}</td>
</tr>
<tr><td class="k">交货地点</td><td colspan="3">{{ o.delivery_place }}</td></tr>
</table>
<h3>物料明细</h3>
<table class="grid">
<tr><th>序号</th><th>物料代码</th><th>物料规格</th><th>单位</th><th class="num">数量</th>
<th class="num">含税单价</th><th class="num">金额</th><th>交期</th><th>备注</th></tr>
{% for it in o.items %}
<tr>
<td class="center">{{ loop.index }}</td>
<td>{{ it.material_code }}</td>
<td>{{ it.material_spec }}</td>
<td class="center">{{ it.unit }}</td>
<td class="num">{{ it.qty }}</td>
<td class="num">{{ '%.2f'|format(it.price) }}</td>
<td class="num">{{ '%.2f'|format(it.amount) }}</td>
<td>{{ it.delivery_date }}</td>
<td>{{ it.remark }}</td>
</tr>
{% else %}
<tr><td colspan="9" class="center muted">暂无明细</td></tr>
{% endfor %}
</table>
<div class="total-bar">
<span>合计(小写):<span class="amt">{{ '%.2f'|format(o.total_amount) }}</span> 元</span>
<span>大写:<b>{{ total_cn }}</b></span>
</div>
{% if o.note %}<p class="muted">补充说明:{{ o.note }}</p>{% endif %}
</div>
<div class="card">
<h2>审核流程</h2>
<table class="grid">
<tr><th>环节</th><th>操作人</th><th>时间</th></tr>
<tr><td>制单</td><td>{{ o.created_by_name }}</td><td>{{ o.created_at.strftime('%Y-%m-%d %H:%M') if o.created_at else '-' }}</td></tr>
<tr><td>主管审核</td><td>{{ o.reviewed_by or '—' }}</td><td>{{ o.reviewed_at.strftime('%Y-%m-%d %H:%M') if o.reviewed_at else '—' }}</td></tr>
<tr><td>经理批准</td><td>{{ o.approved_by or '—' }}</td><td>{{ o.approved_at.strftime('%Y-%m-%d %H:%M') if o.approved_at else '—' }}</td></tr>
</table>
{% if user.role in ['reviewer','admin'] and o.status == 'pending_review' %}
<div style="margin-top:12px;">
<div id="stampBox" style="display:none; margin-bottom:10px;">
<div class="seal-wrap">
<div class="seal-tip">已加盖主管审核章</div>
<div class="seal-circle">
<span class="co">{{ o.our_company or COMPANY_NAME }}</span>
<span class="txt">审核专用</span>
<span class="who">{{ user.name }}<br><span id="stampTime"></span></span>
</div>
</div>
</div>
<form method="post" action="{{ url_for('order_review', oid=o.id) }}" class="toolbar"
onsubmit="return checkStamp()">
<input type="hidden" name="stamped" id="stampedInput" value="0">
<input type="text" name="comment" placeholder="审核意见(选填)" style="width:280px;">
<button type="button" class="btn" id="stampBtn" onclick="applyStamp()">加盖审核章</button>
<button type="submit" class="btn ok" id="confirmBtn" disabled
title="请先加盖审核章">确认审核通过 → 提交经理批准</button>
</form>
<div class="muted" style="margin-top:6px;">流程:先点「加盖审核章」,确认无误后再点「确认审核通过」。</div>
</div>
<script>
function applyStamp(){
document.getElementById('stampedInput').value = '1';
document.getElementById('stampBox').style.display = 'block';
const d = new Date(), p = n => String(n).padStart(2, '0');
document.getElementById('stampTime').textContent =
d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
' ' + p(d.getHours()) + ':' + p(d.getMinutes());
const c = document.getElementById('confirmBtn');
c.disabled = false;
const s = document.getElementById('stampBtn');
s.disabled = true;
s.textContent = '已加章';
}
function checkStamp(){
if (document.getElementById('stampedInput').value !== '1'){
alert('请先点击「加盖审核章」,再确认审核。');
return false;
}
return confirm('确认审核通过并提交经理批准?');
}
</script>
{% endif %}
{% if user.role in ['approver','admin'] and o.status == 'pending_approval' %}
<div style="margin-top:12px;">
<div id="apprStampBox" style="display:none; margin-bottom:10px;">
<div class="seal-wrap">
<div class="seal-tip">已加盖经理批准章</div>
<div class="seal-circle">
<span class="co">{{ o.our_company or COMPANY_NAME }}</span>
<span class="txt">批准专用</span>
<span class="who">{{ user.name }}<br><span id="apprStampTime"></span></span>
</div>
</div>
</div>
<form method="post" action="{{ url_for('order_approve', oid=o.id) }}" class="toolbar"
onsubmit="return checkStampA()">
<input type="hidden" name="stamped" id="apprStampedInput" value="0">
<input type="text" name="comment" placeholder="批准意见(选填)" style="width:280px;">
<button type="button" class="btn" id="apprStampBtn" onclick="applyStampA()">加盖批准章</button>
<button type="submit" class="btn ok" id="apprConfirmBtn" disabled
title="请先加盖批准章">确认批准通过</button>
</form>
<div class="muted" style="margin-top:6px;">流程:先点「加盖批准章」,确认无误后再点「确认批准通过」。</div>
</div>
<script>
function applyStampA(){
document.getElementById('apprStampedInput').value = '1';
document.getElementById('apprStampBox').style.display = 'block';
const d = new Date(), p = n => String(n).padStart(2, '0');
document.getElementById('apprStampTime').textContent =
d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) +
' ' + p(d.getHours()) + ':' + p(d.getMinutes());
document.getElementById('apprConfirmBtn').disabled = false;
const s = document.getElementById('apprStampBtn');
s.disabled = true;
s.textContent = '已加章';
}
function checkStampA(){
if (document.getElementById('apprStampedInput').value !== '1'){
alert('请先点击「加盖批准章」,再确认批准。');
return false;
}
return confirm('确认批准通过?批准后即可导出正式 PDF。');
}
</script>
{% endif %}
{% if user.role in ['reviewer','approver','admin'] and o.status in ['pending_review','pending_approval'] %}
<form method="post" action="{{ url_for('order_reject', oid=o.id) }}" class="toolbar" style="margin-top:8px;">
<input type="text" name="comment" placeholder="驳回原因(必填)" style="width:280px;" required>
<button class="btn danger">驳回给制单人</button>
</form>
{% endif %}
{% if editable and o.status in ['draft','rejected'] %}
<form method="post" action="{{ url_for('order_submit', oid=o.id) }}" class="toolbar" style="margin-top:8px;">
<input type="text" name="comment" placeholder="提交说明(选填)" style="width:280px;">
<button class="btn primary">提交审核</button>
</form>
{% endif %}
{% if o.status == 'rejected' and o.reject_reason %}
<div class="flash error">驳回原因:{{ o.reject_reason }}</div>
{% endif %}
<h3>审核记录</h3>
<ul class="timeline">
{% for lg in o.logs %}
<li>{{ lg.created_at.strftime('%Y-%m-%d %H:%M') }} · {{ lg.user_name }} · {{ lg.action }}
{% if lg.comment %}({{ lg.comment }}){% endif %}</li>
{% else %}
<li class="muted">暂无记录</li>
{% endfor %}
</ul>
</div>
{% endblock %}
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">订单列表</h1>
<div class="card">
<form class="toolbar" method="get">
<input type="text" name="kw" placeholder="单号 / 供应商" value="{{ args.get('kw','') }}" style="width:180px;">
<select name="status" style="width:140px;">
<option value="">全部状态</option>
{% for k, v in STATUS_TEXT.items() %}
<option value="{{ k }}" {% if args.get('status')==k %}selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
<select name="doc_type" style="width:150px;">
<option value="">全部类型</option>
{% for k, v in DOC_TYPES.items() %}
<option value="{{ k }}" {% if args.get('doc_type')==k %}selected{% endif %}>{{ v }}</option>
{% endfor %}
</select>
<input type="date" name="date_from" value="{{ args.get('date_from','') }}" style="width:150px;">
<span>~</span>
<input type="date" name="date_to" value="{{ args.get('date_to','') }}" style="width:150px;">
<button class="btn primary">查询</button>
<span class="grow"></span>
<a class="btn" href="{{ url_for('export_ledger') }}">导出 Excel 台账</a>
<a class="btn primary" href="{{ url_for('order_new') }}">新建订单</a>
</form>
<table class="grid">
<tr>
<th>单号</th><th>类型</th><th>供应商</th><th>采购日期</th><th class="num">金额</th>
<th>状态</th><th>制单</th><th>审核</th><th>批准</th><th class="nowrap">操作</th>
</tr>
{% for o in rows %}
<tr>
<td class="nowrap">{{ o.order_no }}</td>
<td>{{ o.doc_type_text }}</td>
<td>{{ o.supplier_name }}</td>
<td class="nowrap">{{ o.order_date }}</td>
<td class="num">{{ '%.2f'|format(o.total_amount) }}</td>
<td><span class="badge {{ o.status }}">{{ o.status_text }}</span></td>
<td>{{ o.created_by_name }}</td>
<td>{{ o.reviewed_by or '-' }}</td>
<td>{{ o.approved_by or '-' }}</td>
<td class="nowrap">
<a class="btn sm" href="{{ url_for('order_view', oid=o.id) }}">查看</a>
<a class="btn sm" href="{{ url_for('order_pdf', oid=o.id) }}" target="_blank">PDF</a>
</td>
</tr>
{% else %}
<tr><td colspan="10" class="center muted">没有符合条件的订单</td></tr>
{% endfor %}
</table>
</div>
{% endblock %}
+40
View File
@@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">统计</h1>
<div class="card">
<h2>按状态</h2>
<table class="grid">
<tr><th>状态</th><th class="num">订单数</th><th class="num">金额合计(元)</th></tr>
{% for st, cnt, amt in by_status %}
<tr>
<td>{{ status_text.get(st, st) }}</td>
<td class="num">{{ cnt }}</td>
<td class="num">{{ '%.2f'|format(amt or 0) }}</td>
</tr>
{% endfor %}
</table>
</div>
<div class="card">
<h2>按供应商(仅已批准)</h2>
<table class="grid">
<tr><th>供应商</th><th class="num">订单数</th><th class="num">金额合计(元)</th></tr>
{% for name, cnt, amt in by_supplier %}
<tr><td>{{ name }}</td><td class="num">{{ cnt }}</td><td class="num">{{ '%.2f'|format(amt or 0) }}</td></tr>
{% else %}<tr><td colspan="3" class="center muted">暂无数据</td></tr>{% endfor %}
</table>
</div>
<div class="card">
<h2>按月份</h2>
<table class="grid">
<tr><th>月份</th><th class="num">订单数</th><th class="num">金额合计(元)</th></tr>
{% for m, cnt, amt in by_month %}
<tr><td>{{ m }}</td><td class="num">{{ cnt }}</td><td class="num">{{ '%.2f'|format(amt or 0) }}</td></tr>
{% endfor %}
</table>
<div style="margin-top:12px;">
<a class="btn" href="{{ url_for('export_ledger') }}">导出 Excel 台账</a>
</div>
</div>
{% endblock %}
+132
View File
@@ -0,0 +1,132 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">供应商管理</h1>
<div class="card">
<h2>新增供应商</h2>
<form method="post">
<div class="form-grid">
<div><label>编号</label><input type="text" name="code" placeholder="S003"></div>
<div><label>名称 *</label><input type="text" name="name" required></div>
<div><label>联系人</label><input type="text" name="contact"></div>
<div><label>电话</label><input type="text" name="phone"></div>
<div><label>付款方式</label><input type="text" name="payment_terms" placeholder="月结30天"></div>
<div><label>税号</label><input type="text" name="tax_no"></div>
<div><label>开户银行</label><input type="text" name="bank_name"></div>
<div><label>银行帐号</label><input type="text" name="bank_account"></div>
</div>
<div style="margin-top:12px;"><button class="btn primary">新增</button></div>
</form>
</div>
<div class="card">
<div class="card-head">
<h2>供应商列表</h2>
<button type="button" class="btn primary sm" onclick="document.getElementById('supImpModal').style.display='flex'">批量导入 Excel</button>
</div>
<table class="grid">
<tr><th>编号</th><th>名称</th><th>联系人</th><th>电话</th><th>付款方式</th><th>税号</th><th>开户银行</th><th>银行帐号</th><th>操作</th></tr>
{% for s in rows %}
<tr class="{% if not s.active %}row-off{% endif %}">
<td>{{ s.code }}</td><td>{{ s.name }}</td><td>{{ s.contact }}</td><td>{{ s.phone }}</td>
<td>{{ s.payment_terms }}</td><td>{{ s.tax_no }}</td><td>{{ s.bank_name }}</td>
<td>{{ s.bank_account }}</td>
<td class="nowrap">
<button type="button" class="btn sm" onclick="openEdit(this)"
data-id="{{ s.id }}" data-code="{{ s.code or '' }}"
data-name="{{ s.name or '' }}" data-contact="{{ s.contact or '' }}"
data-phone="{{ s.phone or '' }}"
data-payment_terms="{{ s.payment_terms or '' }}"
data-tax_no="{{ s.tax_no or '' }}"
data-bank_name="{{ s.bank_name or '' }}"
data-bank_account="{{ s.bank_account or '' }}">编辑</button>
<form method="post" action="{{ url_for('supplier_toggle', sid=s.id) }}" style="display:inline;">
<button class="btn sm {% if s.active %}danger{% endif %}"
onclick="return confirm('确定{{ '停用' if s.active else '启用' }}该供应商?')">
{{ '停用' if s.active else '启用' }}
</button>
</form>
{% if user.role == 'admin' %}
<form method="post" action="{{ url_for('supplier_delete', sid=s.id) }}" style="display:inline;">
<button class="btn sm danger"
onclick="return confirm('确定彻底删除该供应商?删除后不可恢复!')">删除</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
<div id="supImpModal" class="modal-mask" style="display:none;" onclick="if(event.target===this) this.style.display='none'">
<div class="modal-box">
<div class="modal-head">
<span>批量导入供应商</span>
<span class="modal-close" onclick="document.getElementById('supImpModal').style.display='none'">&times;</span>
</div>
<form method="post" action="{{ url_for('supplier_import_excel') }}" enctype="multipart/form-data">
<div class="modal-body">
<div class="hint">先下载模板按格式填好再导入。<b>名称</b>必填;已存在的按下方方式处理(优先按编号匹配,无编号则按名称)。</div>
<div class="field">
<label>1. 下载模板</label>
<a href="{{ url_for('supplier_import_template') }}">供应商导入模板.xlsx</a>
</div>
<div class="field">
<label>2. 选择 Excel 文件(.xls / .xlsx)</label>
<input type="file" name="file" accept=".xls,.xlsx,.xlsm" required>
</div>
<div class="field">
<label>3. 遇到已存在的供应商</label>
<select name="mode">
<option value="update">更新该供应商的资料</option>
<option value="skip">跳过,不改动已有数据</option>
</select>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn" onclick="document.getElementById('supImpModal').style.display='none'">取消</button>
<button type="submit" class="btn primary">开始导入</button>
</div>
</form>
</div>
</div>
<div id="editModal" class="modal-mask" style="display:none;" onclick="if(event.target===this) this.style.display='none'">
<div class="modal-box">
<div class="modal-head">
<span>编辑供应商</span>
<span class="modal-close" onclick="document.getElementById('editModal').style.display='none'">&times;</span>
</div>
<form method="post" action="{{ url_for('suppliers') }}">
<input type="hidden" name="id" id="edit_id">
<div class="modal-body">
<div class="form-grid two">
<div><label>编号</label><input type="text" name="code" id="edit_code"></div>
<div><label>名称 *</label><input type="text" name="name" id="edit_name" required></div>
<div><label>联系人</label><input type="text" name="contact" id="edit_contact"></div>
<div><label>电话</label><input type="text" name="phone" id="edit_phone"></div>
<div><label>付款方式</label><input type="text" name="payment_terms" id="edit_payment_terms"></div>
<div><label>税号</label><input type="text" name="tax_no" id="edit_tax_no"></div>
<div><label>开户银行</label><input type="text" name="bank_name" id="edit_bank_name"></div>
<div><label>银行帐号</label><input type="text" name="bank_account" id="edit_bank_account"></div>
</div>
</div>
<div class="modal-foot">
<button type="button" class="btn" onclick="document.getElementById('editModal').style.display='none'">取消</button>
<button type="submit" class="btn primary">保存</button>
</div>
</form>
</div>
</div>
<script>
function openEdit(btn){
const d = btn.dataset;
['id','code','name','contact','phone','payment_terms','tax_no','bank_name','bank_account']
.forEach(k => {
const el = document.getElementById('edit_' + k);
if (el) el.value = d[k] || '';
});
document.getElementById('editModal').style.display = 'flex';
}
</script>
{% endblock %}
+39
View File
@@ -0,0 +1,39 @@
{% extends "base.html" %}
{% block body %}
<h1 class="page">用户与权限</h1>
<div class="card">
<h2>新增用户</h2>
<form method="post">
<div class="form-grid">
<div><label>登录名 *</label><input type="text" name="username" required></div>
<div><label>姓名</label><input type="text" name="name"></div>
<div><label>角色</label>
<select name="role">
{% for k, v in roles.items() %}<option value="{{ k }}">{{ v }}</option>{% endfor %}
</select>
</div>
<div><label>初始密码(默认 123456)</label><input type="text" name="password" placeholder="123456"></div>
</div>
<div style="margin-top:12px;"><button class="btn primary">新增</button></div>
</form>
</div>
<div class="card">
<h2>用户列表</h2>
<table class="grid">
<tr><th>登录名</th><th>姓名</th><th>角色</th><th>状态</th><th>创建时间</th></tr>
{% for t in rows %}
<tr>
<td>{{ t.username }}</td><td>{{ t.name }}</td>
<td>{{ roles.get(t.role, t.role) }}</td>
<td>{{ '启用' if t.active else '停用' }}</td>
<td>{{ t.created_at.strftime('%Y-%m-%d %H:%M') if t.created_at else '-' }}</td>
</tr>
{% endfor %}
</table>
<div class="hint" style="margin-top:12px;">
制单员:新建 / 编辑 / 提交;主管审核:审核通过或驳回;经理批准:批准后订单生效并可导出正式 PDF;
管理员:全部权限,含用户、供应商、物料维护。
</div>
</div>
{% endblock %}