支持供应商简称维护和订单PDF下载命名
This commit is contained in:
+17
-6
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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] || '';
|
||||
|
||||
Reference in New Issue
Block a user