支持供应商简称维护和订单PDF下载命名
This commit is contained in:
@@ -42,6 +42,7 @@ Windows 7 SP1 服务器使用单独的 [Win7 离线打包说明](packaging/win7/
|
||||
## 主要能力
|
||||
|
||||
- 采购单和委外单,供应商/物料库、中文 XLS/XLSX 导入。
|
||||
- 供应商支持维护公司简称,PDF 下载文件名采用“单据类型_订单号_供应商简称.pdf”;未填写简称时使用供应商全名,已审批文件重新下载也适用。
|
||||
- 草稿可暂存,“保存并提交”保证当前修改一起进入审批。
|
||||
- 数量、单价四位精度;逐行四舍五入到分再合计,页面、数据库和 PDF 使用同一口径。
|
||||
- 制单与审批由不同账号完成,管理员也遵守分工;审批员一次确认即可通过或驳回。
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
本包用于在你的 Windows 7 服务电脑上,从 v4 源码生成 EXE。包括 Python 3.8.10 官方完整安装程序、32 位与 64 位离线依赖、构建脚本、环境与业务自检,以及现有 16 页使用升级手册。
|
||||
|
||||
2026-09-17 文件名更新:在“供应商管理”的新增或编辑窗口填写“公司简称”,PDF 文件名即为“采购单_订单号_简称.pdf”(委外单使用对应类型名称);未填写简称时使用供应商全名。已审批订单重新下载也使用新文件名,归档 PDF 内容不变,无需重新审批。Excel 模板新增末列“公司简称”,旧表缺少该列时保留原简称。本次启动会先备份数据库,再升级到数据库结构版本 5,新增简称字段。
|
||||
|
||||
2026-09-17 排版更新:采购专用章直径增加 30%,固定在右侧需方签章空位;供方和需方签章区之间留出 20 毫米间隔。采购单与委外订单均已调整,示例见 `samples/`。本次 PDF 模板版本为 `po-v4`。
|
||||
|
||||
升级后新审批生成的正式 PDF 使用新排版。已审批归档的 PDF 保留原版;需要采用新版的订单,请通过系统的“变更重审”流程重新审批。使用本包重新解压、安装环境并生成 EXE 后,按下文备份和替换服务程序。
|
||||
|
||||
+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] || '';
|
||||
|
||||
+39
-1
@@ -7,6 +7,44 @@ import pytest
|
||||
SCHEMA = Path(__file__).parent / 'fixtures' / 'legacy_schema.sql'
|
||||
|
||||
|
||||
def test_v4_supplier_alias_upgrade_preserves_orders_and_creates_one_backup(tmp_path):
|
||||
from models import Order, OrderVersion, Supplier, init_db
|
||||
path = tmp_path / 'po.db'
|
||||
engine, factory = init_db(str(path))
|
||||
archived = b'%PDF-1.4 immutable before alias update'
|
||||
with factory() as db:
|
||||
db.add(Supplier(name='旧供应商', code='S01', contact='原联系人'))
|
||||
db.add(Order(id=1, order_no='V4-ORDER', status='approved', revision=2,
|
||||
payment_status='partial', invoice_status='received', delivery_status='delivered'))
|
||||
db.flush()
|
||||
db.add(OrderVersion(order_id=1, revision=2, content_json='{}', pdf_data=archived,
|
||||
pdf_sha256='existing-digest', template_version='po-v4'))
|
||||
db.commit()
|
||||
engine.dispose()
|
||||
with sqlite3.connect(path) as con:
|
||||
con.execute('ALTER TABLE suppliers DROP COLUMN short_name')
|
||||
con.execute('PRAGMA user_version=4')
|
||||
old_orders = con.execute('SELECT * FROM orders').fetchall()
|
||||
old_archives = con.execute('SELECT * FROM order_versions').fetchall()
|
||||
engine, factory = init_db(str(path))
|
||||
with factory() as db:
|
||||
supplier = db.query(Supplier).one()
|
||||
assert (supplier.name, supplier.code, supplier.contact, supplier.short_name) == ('旧供应商', 'S01', '原联系人', '')
|
||||
engine.dispose()
|
||||
with sqlite3.connect(path) as con:
|
||||
assert con.execute('PRAGMA user_version').fetchone()[0] == 5
|
||||
assert con.execute('SELECT * FROM orders').fetchall() == old_orders
|
||||
assert con.execute('SELECT * FROM order_versions').fetchall() == old_archives
|
||||
backups = list(tmp_path.glob('*.bak'))
|
||||
assert len(backups) == 1
|
||||
with sqlite3.connect(backups[0]) as con:
|
||||
assert con.execute('PRAGMA user_version').fetchone()[0] == 4
|
||||
assert 'short_name' not in {row[1] for row in con.execute('PRAGMA table_info(suppliers)')}
|
||||
engine, _ = init_db(str(path))
|
||||
engine.dispose()
|
||||
assert len(list(tmp_path.glob('*.bak'))) == 1
|
||||
|
||||
|
||||
def test_v3_tracking_upgrade_preserves_existing_state_and_archives(tmp_path):
|
||||
import hashlib
|
||||
from models import Order, OrderVersion, User, init_db
|
||||
@@ -48,7 +86,7 @@ def test_v3_tracking_upgrade_preserves_existing_state_and_archives(tmp_path):
|
||||
assert con.execute('PRAGMA user_version').fetchone()[0] == 3
|
||||
assert 'payment_status' not in {row[1] for row in con.execute('PRAGMA table_info(orders)')}
|
||||
with sqlite3.connect(path) as con:
|
||||
assert con.execute('PRAGMA user_version').fetchone()[0] == 4
|
||||
assert con.execute('PRAGMA user_version').fetchone()[0] == 5
|
||||
assert con.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
|
||||
engine, _ = init_db(str(path))
|
||||
engine.dispose()
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import io
|
||||
import re
|
||||
|
||||
import openpyxl
|
||||
import pytest
|
||||
from werkzeug.http import parse_options_header
|
||||
|
||||
from conftest import post
|
||||
from test_workflow import action, new, order, save
|
||||
|
||||
|
||||
def download_name(response):
|
||||
return parse_options_header(response.headers['Content-Disposition'])[1]['filename']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('kind,title', [('purchase', '采购单'), ('outsourcing', '委外生产订单')])
|
||||
def test_supplier_alias_updates_filename_without_changing_archive(app, clients, kind, title):
|
||||
company = '苏州精锐五金制品有限公司'
|
||||
assert post(clients['maker'], '/suppliers', {'name': company, 'short_name': '精锐五金'}).status_code == 302
|
||||
with app.Session() as db:
|
||||
sid = db.query(app.Supplier).one().id
|
||||
oid = new(app, clients['maker'], kind)
|
||||
assert save(app, clients['maker'], oid, supplier_name=company, intent='submit').status_code == 302
|
||||
assert action(app, clients['approver'], oid, 'approve', stamped='1').status_code == 302
|
||||
with app.Session() as db:
|
||||
number = db.get(app.Order, oid).order_no
|
||||
path = '/orders/%s/pdf' % oid
|
||||
archive = clients['maker'].get(path).data
|
||||
original_version = order(app, oid)['version']
|
||||
for suffix, disposition in [('', 'inline'), ('?dl=1', 'attachment')]:
|
||||
response = clients['maker'].get(path + suffix)
|
||||
assert response.headers['Content-Disposition'].startswith(disposition + ';')
|
||||
assert download_name(response) == '%s_%s_精锐五金.pdf' % (title, number)
|
||||
assert response.data == archive
|
||||
assert post(clients['maker'], '/suppliers', {'id': sid, 'name': company, 'short_name': '精锐'}).status_code == 302
|
||||
response = clients['maker'].get(path + '?dl=1')
|
||||
assert download_name(response) == '%s_%s_精锐.pdf' % (title, number)
|
||||
assert response.data == archive and order(app, oid)['version'] == original_version
|
||||
assert clients['other'].get(path + '?dl=1').status_code == 404
|
||||
# A cleared alias or removed master-data entry still yields an identifiable name.
|
||||
assert post(clients['maker'], '/suppliers', {'id': sid, 'name': company, 'short_name': ''}).status_code == 302
|
||||
assert download_name(clients['maker'].get(path)) == '%s_%s_%s.pdf' % (title, number, company)
|
||||
assert post(clients['admin'], '/suppliers/%s/delete' % sid).status_code == 302
|
||||
response = clients['maker'].get(path)
|
||||
assert download_name(response) == '%s_%s_%s.pdf' % (title, number, company)
|
||||
assert response.data == archive
|
||||
|
||||
|
||||
def test_pdf_filename_handles_unsafe_characters_and_ambiguous_suppliers(app, clients):
|
||||
company = '测试同名公司'
|
||||
with app.Session() as db:
|
||||
db.add(app.Supplier(name=company, tax_no='ONE', short_name='精锐/五金:分部?\r\n<二>'))
|
||||
db.add(app.Supplier(name=company, tax_no='TWO', short_name='另一家'))
|
||||
db.commit()
|
||||
oid = new(app, clients['maker'])
|
||||
assert save(app, clients['maker'], oid, supplier_name=company, supplier_tax_no='ONE').status_code == 302
|
||||
response = clients['maker'].get('/orders/%s/pdf?dl=1' % oid)
|
||||
filename = download_name(response)
|
||||
assert response.status_code == 200
|
||||
assert '精锐_五金' in filename and '分部' in filename
|
||||
assert not re.search(r'[<>:"/\\|?*\x00-\x1f]', filename)
|
||||
assert len(filename.encode('utf-16-le')) // 2 <= 255
|
||||
# Without identifying tax information, do not pick an arbitrary company's alias.
|
||||
assert save(app, clients['maker'], oid, supplier_name=company, supplier_tax_no='').status_code == 302
|
||||
assert download_name(clients['maker'].get('/orders/%s/pdf' % oid)).endswith('_测试同名公司.pdf')
|
||||
assert save(app, clients['maker'], oid, supplier_name='公司' * 90).status_code == 302
|
||||
assert len(download_name(clients['maker'].get('/orders/%s/pdf' % oid)).encode('utf-16-le')) // 2 <= 255
|
||||
|
||||
|
||||
def test_supplier_import_alias_headers_legacy_preservation_and_blank_clear(app, clients):
|
||||
company = '苏州测试有限公司'
|
||||
|
||||
def upload(heads, row):
|
||||
workbook = openpyxl.Workbook()
|
||||
workbook.active.append(heads)
|
||||
workbook.active.append(row)
|
||||
stream = io.BytesIO()
|
||||
workbook.save(stream)
|
||||
stream.seek(0)
|
||||
response = post(clients['maker'], '/suppliers/import_excel', {'file': (stream, '供应商.xlsx')})
|
||||
assert response.status_code == 302
|
||||
|
||||
upload(['公司简称', '供应商名称', '供应商编号'], ['苏测', company, 'S001'])
|
||||
with app.Session() as db:
|
||||
supplier = db.query(app.Supplier).one()
|
||||
assert (supplier.code, supplier.name, supplier.short_name) == ('S001', company, '苏测')
|
||||
upload(['编号', '名称', '联系人'], ['S001', company, '新联系人'])
|
||||
with app.Session() as db:
|
||||
supplier = db.query(app.Supplier).one()
|
||||
assert (supplier.short_name, supplier.contact) == ('苏测', '新联系人')
|
||||
upload(['编号', '名称', '公司简称'], ['S001', company, ''])
|
||||
with app.Session() as db:
|
||||
assert db.query(app.Supplier).one().short_name == ''
|
||||
template = openpyxl.load_workbook(io.BytesIO(clients['maker'].get('/suppliers/import_template.xlsx').data))
|
||||
assert template.active.cell(1, 9).value == '公司简称'
|
||||
assert template.active.cell(2, 9).value == '精锐五金'
|
||||
template.close()
|
||||
assert post(clients['maker'], '/suppliers', {'name': '超长简称测试', 'short_name': '长' * 61}).status_code == 400
|
||||
+16
-1
@@ -70,6 +70,18 @@ def run(executable=None):
|
||||
admin.get_by_role('button',name='保存用户').click()
|
||||
admin.wait_for_url(base + '/users')
|
||||
admin.screenshot(path=str(RUN / 'users.png'),full_page=True)
|
||||
admin.goto(base + '/suppliers')
|
||||
supplier_form = admin.locator('.card').first.locator('form')
|
||||
supplier_form.locator('[name=name]').fill('测试供应商')
|
||||
supplier_form.locator('[name=short_name]').fill('测试简称')
|
||||
supplier_form.get_by_role('button', name='新增', exact=True).click()
|
||||
admin.wait_for_url(base + '/suppliers')
|
||||
admin.get_by_role('button', name='编辑', exact=True).click()
|
||||
assert admin.locator('#edit_short_name').input_value() == '测试简称'
|
||||
admin.locator('#edit_short_name').fill('测试供应商简称')
|
||||
admin.locator('#editModal').get_by_role('button', name='保存', exact=True).click()
|
||||
admin.wait_for_url(base + '/suppliers')
|
||||
admin.screenshot(path=str(RUN / 'supplier-short-name.png'), full_page=True)
|
||||
def sign_in(username, destination):
|
||||
tab=page(mobile=username != 'maker'); tab.goto(destination)
|
||||
assert '/login' in tab.url
|
||||
@@ -148,6 +160,9 @@ def run(executable=None):
|
||||
approver.screenshot(path=str(RUN / (kind+'-approved.png')),full_page=True)
|
||||
response=approver.request.get(view_url.replace('/view','/pdf'))
|
||||
assert response.status==200
|
||||
from werkzeug.http import parse_options_header
|
||||
filename = parse_options_header(response.headers['content-disposition'])[1]['filename']
|
||||
assert filename.endswith('_测试供应商简称.pdf')
|
||||
data=response.body();(RUN/(kind+'.pdf')).write_bytes(data)
|
||||
archives.append((view_url.replace('/view','/pdf'), hashlib.sha256(data).hexdigest()))
|
||||
document=pymupdf.open(stream=data,filetype='pdf')
|
||||
@@ -228,7 +243,7 @@ def run(executable=None):
|
||||
assert hashlib.sha256(response.body()).hexdigest() == expected
|
||||
with sqlite3.connect(RUN / 'instance' / 'po.db') as db:
|
||||
assert db.execute('PRAGMA integrity_check').fetchone()[0] == 'ok'
|
||||
assert db.execute('PRAGMA user_version').fetchone()[0] == 4
|
||||
assert db.execute('PRAGMA user_version').fetchone()[0] == 5
|
||||
assert db.execute('SELECT count(*) FROM orders WHERE status = ?', ('approved',)).fetchone()[0] == 2
|
||||
assert db.execute('SELECT count(*) FROM notifications').fetchone()[0] == 0
|
||||
assert db.execute("SELECT count(*) FROM orders WHERE payment_status='paid' AND delivery_status='pending'").fetchone()[0] == 1
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
4. 在运行系统的电脑上读取 `po_app/instance/setup.token`,把初始化口令填入页面,设置管理员账号和密码。不要发送该文件给无关人员。
|
||||
5. 管理员在“用户”页新增制单员、审批员,两个环节分配给不同人员。新增账号首次登录必须修改初始密码。
|
||||
6. 录入真实供应商和物料,再开始制单。新数据库不再预置演示账号、演示供应商或假银行信息。
|
||||
- 在“供应商管理”填写“公司简称”,用于 PDF 导出文件名,例如 `采购单_PO202609170001_精锐五金.pdf`。未填简称时使用订单上的供应商全名;按全名匹配供应商资料,同名有歧义时结合税号匹配,仍有歧义则使用全名。
|
||||
- 简称只用于下载命名,PDF 正文仍显示全名;已审批订单重新下载即可使用新简称,归档内容保持不变。文件名中的 Windows 不允许字符会被替换。
|
||||
- Excel 供应商模板末列支持“公司简称”。旧表没有该列时保留原简称;该列留空则清除原简称。此功能使用数据库结构版本 5,旧数据库启动时先备份再增量升级。
|
||||
|
||||
系统使用 Waitress 提供服务。源码和 EXE 均支持 `PO_DATA_DIR` 指定数据目录;未设置时分别使用 `po_app` 目录和 EXE 所在目录。`PO_HOST` 默认 `0.0.0.0`;端口已占用时启动程序会在 8120 之前尝试后续端口,以控制台打印地址为准。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user