支持供应商简称维护和订单PDF下载命名

This commit is contained in:
reddaniel110
2026-09-17 10:52:36 +08:00
parent 62cd117379
commit a86f0b0d5b
12 changed files with 227 additions and 21 deletions
+17 -6
View File
@@ -147,6 +147,11 @@ def suppliers():
setattr(s, f, request.form.get(f, "").strip())
if not s.name:
raise ValueError('供应商名称必填')
if 'short_name' in request.form:
short_name = request.form['short_name'].strip()
if len(short_name) > 60:
raise ValueError('供应商简称不能超过60个字符')
s.short_name = short_name
if not sid:
db.add(s)
db.commit()
@@ -208,12 +213,16 @@ def supplier_import_excel():
if not any(r):
continue
(code, name, contact, phone, payment_terms, tax_no,
bank_name, bank_account) = (list(r) + [""] * 8)[:8]
bank_name, bank_account, short_name) = (list(r) + [None] * 9)[:9]
code, name = s(code), s(name)
if not name:
failed += 1
errs.append(u"第%d行:供应商名称为空" % i)
continue
if short_name is not None and len(s(short_name)) > 60:
failed += 1
errs.append(u"第%d行:供应商简称不能超过60个字符" % i)
continue
obj = db.query(Supplier).filter_by(code=code).first() if code else None
if not obj:
obj = db.query(Supplier).filter_by(name=name).first()
@@ -229,13 +238,15 @@ def supplier_import_excel():
obj.tax_no = s(tax_no)
obj.bank_name = s(bank_name)
obj.bank_account = s(bank_account)
if short_name is not None:
obj.short_name = s(short_name)
obj.active = True
updated += 1
else:
db.add(Supplier(code=code, name=name, contact=s(contact),
phone=s(phone), payment_terms=s(payment_terms),
tax_no=s(tax_no), bank_name=s(bank_name),
bank_account=s(bank_account), active=True))
bank_account=s(bank_account), short_name=s(short_name), active=True))
added += 1
db.commit()
msg = u"导入完成:新增 %d,更新 %d,跳过 %d,失败 %d" % (added, updated, skipped, failed)
@@ -254,11 +265,11 @@ def supplier_import_template():
wb = openpyxl.Workbook()
ws = wb.active
ws.append([u"编号", u"名称", u"联系人", u"电话", u"付款方式", u"税号",
u"开户银行", u"银行帐号"])
u"开户银行", u"银行帐号", u"公司简称"])
ws.append(["S003", u"苏州精锐五金制品有限公司", u"周经理", "13800000001",
u"月结30天", "91320500MA1XXXXXXX", u"中国银行苏州工业园区支行",
"4562 1088 0012 3456"])
for col, w in zip("ABCDEFGH", (10, 30, 12, 16, 14, 22, 26, 24)):
"4562 1088 0012 3456", u"精锐五金"])
for col, w in zip("ABCDEFGHI", (10, 30, 12, 16, 14, 22, 26, 24, 18)):
ws.column_dimensions[col].width = w
buf = io.BytesIO()
wb.save(buf)
@@ -406,7 +417,7 @@ def api_materials():
@login_required
def api_suppliers():
rows = db.query(Supplier).filter_by(active=True).all()
return jsonify([{"id": s.id, "name": s.name, "contact": s.contact,
return jsonify([{"id": s.id, "name": s.name, "short_name": s.short_name, "contact": s.contact,
"phone": s.phone, "tax_no": s.tax_no, "bank_name": s.bank_name,
"bank_account": s.bank_account,
"payment_terms": s.payment_terms} for s in rows])
+6 -5
View File
@@ -66,7 +66,7 @@ def read_rows(path):
return out
def read_table(path, keys, order):
def read_table(path, keys, order, missing=""):
"""通用表格读取:按关键词映射表头,返回按 order 排列的二维列表。
与 read_rows() 的区别是表头判定更宽松(命中任意关键词即可),
@@ -110,7 +110,7 @@ def read_table(path, keys, order):
vals = []
for k in order:
i = mapping.get(k)
vals.append(row[i] if (i is not None and i < len(row)) else "")
vals.append(row[i] if (i is not None and i < len(row)) else missing)
out.append(vals)
return out
@@ -124,6 +124,7 @@ MATERIAL_KEYS = {
MATERIAL_ORDER = ["code", "spec", "unit", "price"]
SUPPLIER_KEYS = {
"short_name": [u"简称", u"别名"],
"code": [u"编号", u"供应商编号", u"代码"],
"name": [u"名称", u"供应商名称", u"公司"],
"contact": [u"联系人"],
@@ -134,7 +135,7 @@ SUPPLIER_KEYS = {
"bank_account": [u"银行帐号", u"银行账号", u"帐号", u"账号"],
}
SUPPLIER_ORDER = ["code", "name", "contact", "phone", "payment_terms",
"tax_no", "bank_name", "bank_account"]
"tax_no", "bank_name", "bank_account", "short_name"]
def read_material_rows(path):
@@ -143,5 +144,5 @@ def read_material_rows(path):
def read_supplier_rows(path):
"""读取供应商表,返回 [编号,名称,联系人,电话,付款方式,税号,开户银行,银行帐号]。"""
return read_table(path, SUPPLIER_KEYS, SUPPLIER_ORDER)
"""简称追加在末列;旧表缺少该列时以 None 表示,更新时保留已有简称。"""
return read_table(path, SUPPLIER_KEYS, SUPPLIER_ORDER, missing=None)
+8 -3
View File
@@ -14,7 +14,7 @@ def upgrade_database(path):
if 'orders' not in tables:
return
version = con.execute('PRAGMA user_version').fetchone()[0]
if version >= 4:
if version >= 5:
return
duplicates = con.execute('SELECT order_no, COUNT(*) FROM orders GROUP BY order_no HAVING COUNT(*) > 1').fetchall()
if duplicates:
@@ -28,8 +28,13 @@ def upgrade_database(path):
upgrade_fixed_amounts(con)
if version < 3:
merge_approval(con, tables)
add_tracking(con)
con.execute('PRAGMA user_version=4')
if version < 4:
add_tracking(con)
if 'suppliers' in tables:
columns = {row[1] for row in con.execute('PRAGMA table_info(suppliers)')}
if 'short_name' not in columns:
con.execute("ALTER TABLE suppliers ADD COLUMN short_name VARCHAR(60) NOT NULL DEFAULT ''")
con.execute('PRAGMA user_version=5')
con.commit()
except Exception:
con.rollback()
+2 -1
View File
@@ -81,6 +81,7 @@ class Supplier(Base):
id = Column(Integer, primary_key=True)
code = Column(String(50))
name = Column(String(200), nullable=False)
short_name = Column(String(60), nullable=False, default='')
contact = Column(String(50))
phone = Column(String(50))
tax_no = Column(String(80))
@@ -277,5 +278,5 @@ def init_db(db_path):
Base.metadata.create_all(engine)
if not existed:
with engine.connect() as con:
con.exec_driver_sql('PRAGMA user_version=4')
con.exec_driver_sql('PRAGMA user_version=5')
return engine, sessionmaker(bind=engine)
+29 -1
View File
@@ -1,6 +1,8 @@
import hashlib
import io
import json
import re
import unicodedata
from datetime import datetime
from flask import abort, flash, redirect, render_template, request, send_file, url_for
@@ -24,6 +26,32 @@ def pdf_bytes(order):
return buf.getvalue()
def pdf_download_name(order):
# Use current master-data aliases without regenerating an approved PDF.
company = (order.supplier_name or '').strip()
matches = db.query(Supplier.short_name, Supplier.tax_no).filter(Supplier.name == company).all()
tax_no = (order.supplier_tax_no or '').strip()
if tax_no:
same_tax = [row for row in matches if (row.tax_no or '').strip() == tax_no]
if same_tax:
matches = same_tax
aliases = {(row.short_name or '').strip() or company for row in matches}
# Ambiguous duplicate names fall back to the company recorded on the order.
supplier = next(iter(aliases)) if len(aliases) == 1 else company
def part(value, fallback, limit):
value = ''.join(c for c in str(value or '') if not unicodedata.category(c).startswith('C'))
value = re.sub(r'[<>:"/\\|?*]', '_', value)
value = re.sub(r'\s+', ' ', value).strip(' .')
# Windows counts UTF-16 units; retain Chinese and bound the full filename.
value = value.encode('utf-16-le')[:limit * 2].decode('utf-16-le', errors='ignore').rstrip(' .')
return value or fallback
return '%s_%s_%s.pdf' % (part(order.doc_type_text, '订单', 32),
part(order.order_no, '未编号', 64),
part(supplier, '未填写供应商', 80))
def register_order_routes(app):
@app.route('/orders/new', methods=['GET', 'POST'])
@login_required
@@ -254,7 +282,7 @@ def register_order_routes(app):
else:
data = pdf_bytes(order)
response = send_file(io.BytesIO(data), mimetype='application/pdf', as_attachment=request.args.get('dl') == '1',
download_name='%s %s.pdf' % (order.doc_type_text, order.order_no))
download_name=pdf_download_name(order))
response.headers['Cache-Control'] = 'private, no-store'
return response
+6 -3
View File
@@ -8,6 +8,7 @@
<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>公司简称(用于 PDF 文件名)</label><input type="text" name="short_name" maxlength="60" placeholder="如:精锐五金;留空使用全名"></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>
@@ -28,13 +29,14 @@
<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.code }}</td><td>{{ s.name }}{% if s.short_name %}<div class="hint">简称:{{ s.short_name }}</div>{% endif %}</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-short_name="{{ s.short_name or '' }}"
data-phone="{{ s.phone or '' }}"
data-payment_terms="{{ s.payment_terms or '' }}"
data-tax_no="{{ s.tax_no or '' }}"
@@ -69,7 +71,7 @@
<form method="post" action="{{ url_for('supplier_import_excel') }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="modal-body">
<div class="hint">先下载模板按格式填好再导入。<b>名称</b>必填;已存在的按下方方式处理(优先按编号匹配,无编号则按名称)。</div>
<div class="hint">先下载模板按格式填好再导入。<b>名称</b>必填;已存在的按下方方式处理(优先按编号匹配,无编号则按名称)。公司简称用于 PDF 文件名;旧表没有简称列时保留原简称,简称列留空则清除原简称。</div>
<div class="field">
<label>1. 下载模板</label>
<a href="{{ url_for('supplier_import_template') }}">供应商导入模板.xlsx</a>
@@ -107,6 +109,7 @@
<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>公司简称(用于 PDF 文件名)</label><input type="text" name="short_name" id="edit_short_name" maxlength="60" placeholder="留空使用全名"></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>
@@ -126,7 +129,7 @@
<script>
function openEdit(btn){
const d = btn.dataset;
['id','code','name','contact','phone','payment_terms','tax_no','bank_name','bank_account']
['id','code','name','short_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] || '';