完善采购审批流程、企业微信通知和采购专用章

This commit is contained in:
reddaniel110
2026-09-16 11:45:21 +08:00
parent 4876d8805d
commit bf952196fa
58 changed files with 3451 additions and 1909 deletions
+29
View File
@@ -0,0 +1,29 @@
"""Create a consistent SQLite backup without copying a live database file."""
import argparse
import os
import sqlite3
from datetime import datetime
from pathlib import Path
def backup_database(data_dir):
instance = Path(data_dir).resolve() / 'instance'
source = instance / 'po.db'
if not source.is_file():
raise FileNotFoundError('未找到数据库:%s' % source)
destination_dir = instance / 'backups'
destination_dir.mkdir(exist_ok=True)
destination = destination_dir / ('po-' + datetime.now().strftime('%Y%m%d-%H%M%S-%f') + '.db')
with sqlite3.connect(source.as_uri() + '?mode=ro', uri=True) as original:
with sqlite3.connect(destination) as copy:
original.backup(copy)
if copy.execute('PRAGMA integrity_check').fetchone()[0] != 'ok':
raise RuntimeError('备份完整性检查未通过:%s' % destination)
return destination
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--data-dir', default=os.environ.get('PO_DATA_DIR', str(Path(__file__).resolve().parents[1] / 'po_app')))
args = parser.parse_args()
print(backup_database(args.data_dir))
+177
View File
@@ -0,0 +1,177 @@
"""Headless Edge checks against a temporary local server and temporary data only."""
import io
import json
import os
import re
import sys
import tempfile
import threading
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / 'tools' / 'out'
OUT.mkdir(exist_ok=True)
RUN = Path(tempfile.mkdtemp(prefix='ui-check-', dir=OUT))
os.environ['PO_DATA_DIR'] = str(RUN)
sys.path.insert(0, str(ROOT / 'po_app'))
from app import app, Session
from waitress.server import create_server
from playwright.sync_api import sync_playwright
import pymupdf
import openpyxl
from datetime import datetime, timedelta
INITIAL = 'UI-test-initial-2026'
PERSONAL = 'UI-test-personal-2026'
def run():
server = create_server(app, host='127.0.0.1', port=0)
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
base = 'http://127.0.0.1:%s' % server.effective_port
errors = []
summary = []
try:
with sync_playwright() as p:
browser = p.chromium.launch(channel='msedge', headless=True)
def page(mobile=False):
context = browser.new_context(viewport={'width':390,'height':844} if mobile else {'width':1440,'height':1080},
is_mobile=mobile, has_touch=mobile)
tab = context.new_page()
tab.on('pageerror', lambda e: errors.append(str(e)))
tab.on('dialog', lambda d: d.accept())
return tab
admin = page()
admin.goto(base + '/setup')
admin.locator('[name=setup_token]').fill((RUN / 'instance' / 'setup.token').read_text().strip())
admin.locator('[name=username]').fill('admin')
admin.locator('[name=name]').fill('管理员')
admin.locator('[name=password]').fill(PERSONAL)
admin.get_by_role('button',name='创建管理员').click()
admin.wait_for_url(base + '/')
for username, role in [('maker','maker'),('approver','approver')]:
admin.goto(base + '/users')
admin.locator('[name=username]').fill(username)
admin.locator('[name=name]').fill({'maker':'采购员','approver':'审批员'}[username])
admin.locator('[name=role]').select_option(role)
admin.locator('[name=password]').fill(INITIAL)
admin.get_by_role('button',name='保存用户').click()
admin.wait_for_url(base + '/users')
admin.screenshot(path=str(RUN / 'users.png'),full_page=True)
def sign_in(username, destination):
tab=page(mobile=username != 'maker'); tab.goto(destination)
assert '/login' in tab.url
if username != 'maker':
tab.screenshot(path=str(RUN / (username+'-mobile-login.png')))
tab.locator('[name=username]').fill(username)
tab.locator('[name=password]').fill(INITIAL)
tab.get_by_role('button',name='登 录').click()
tab.wait_for_url(re.compile(r'/account/password(?:\?|$)'))
tab.locator('[name=old_password]').fill(INITIAL)
tab.locator('[name=password]').fill(PERSONAL)
tab.locator('[name=confirm_password]').fill(PERSONAL)
tab.get_by_role('button',name='更新密码').click()
tab.wait_for_url(destination)
return tab
accounts = {'maker': sign_in('maker', base + '/')}
# A fake bot URL only: importing the app does not start the sender.
admin.goto(base + '/settings/notifications')
admin.locator('[name=base_url]').fill('http://192.168.1.10:8080')
for channel in ('approval','result'):
admin.locator('[name='+channel+'_webhook]').fill(
'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=TEST-ONLY-NOT-A-REAL-KEY-'+channel)
admin.locator('[name=enabled]').check()
admin.get_by_role('button',name='保存通知配置').click()
admin.wait_for_load_state('networkidle')
assert admin.locator('[name=approval_webhook]').input_value() == ''
admin.get_by_role('button',name='测试审批通知群',exact=True).click()
admin.wait_for_load_state('networkidle')
admin.screenshot(path=str(RUN/'notifications.png'), full_page=True)
maker=accounts['maker']
for kind,count in [('purchase',2),('outsourcing',30)]:
maker.goto(base + '/orders/new')
maker.locator('[name=doc_type]').select_option(kind)
maker.get_by_role('button',name='创建并填写明细').click()
maker.wait_for_url('**/edit')
edit_url=maker.url
maker.locator('[name=supplier_name]').fill('测试供应商')
maker.locator('[name=payment_terms]').fill('月结30天')
for i in range(count):
maker.get_by_role('button',name='+ 添加一行').click()
maker.evaluate('''count => {
document.querySelectorAll('#itemTable tr').forEach((tr,i) => {
if (!i) return;
const vals={code:'BROWSER-'+i,spec:'测试物料 A&B <规格>',unit:'件',qty:'1',price:'0.334',date:'2026-10-01',remark:''};
for (const [k,v] of Object.entries(vals)) tr.querySelector('[name=item_'+k+']').value=v;
});
calc();
}''',count)
assert maker.locator('#totalAmt').inner_text() == ('0.66' if count==2 else '9.90')
maker.screenshot(path=str(RUN / (kind+'-edit.png')),full_page=True)
maker.get_by_role('button',name='保存并提交审批').click()
maker.wait_for_url('**/view')
view_url=maker.url
assert maker.locator('h1').inner_text().endswith('待审批')
if 'approver' not in accounts:
accounts['approver'] = sign_in('approver', view_url)
approver=accounts['approver'];approver.goto(view_url)
assert approver.evaluate('document.documentElement.scrollWidth <= innerWidth')
approver.get_by_role('button',name='加盖采购专用章',exact=True).click()
approver.locator('#apprConfirmBtn').scroll_into_view_if_needed()
approver.screenshot(path=str(RUN / (kind+'-mobile-approve.png')))
approver.get_by_role('button',name='确认审批通过',exact=True).click()
approver.wait_for_load_state('networkidle')
assert '审批通过' in approver.locator('h1').inner_text()
approver.screenshot(path=str(RUN / (kind+'-approved.png')),full_page=True)
response=approver.request.get(view_url.replace('/view','/pdf'))
assert response.status==200
data=response.body();(RUN/(kind+'.pdf')).write_bytes(data)
document=pymupdf.open(stream=data,filetype='pdf')
text='\n'.join(p.get_text() for p in document)
assert '0.334' in text
assert '待审批' not in text
assert text.count('采购专用章') == 1
assert '审核专用章' not in text and '批准专用章' not in text
for i,pdf_page in enumerate(document):
pdf_page.get_pixmap(dpi=110).save(RUN/('%s-%s.png'%(kind,i+1)))
for width in (320,390,768):
approver.set_viewport_size({'width':width, 'height':844})
assert approver.evaluate('document.documentElement.scrollWidth <= innerWidth'), width
for route in ('/orders', '/'):
approver.goto(base + route)
assert approver.evaluate('document.documentElement.scrollWidth <= innerWidth'), (width, route)
approver.goto(view_url)
approver.set_viewport_size({'width':390,'height':844})
summary.append({'kind':kind,'rows':count,'pdf_pages':len(document),'mobile_approval':True})
document.close()
# Native Excel date is visible in the browser date input after upload.
maker.goto(base + '/orders/new');maker.get_by_role('button',name='创建并填写明细').click()
maker.wait_for_url('**/edit')
workbook=openpyxl.Workbook();workbook.active.append(['物料代码','物料规格','单位','数量','单价','交期','备注'])
workbook.active.append(['DATE','日期测试','件',1,2,datetime(2026,10,1),''])
out=io.BytesIO();workbook.save(out)
maker.locator('[name=file]').set_input_files({'name':'采购明细.xlsx','mimeType':'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet','buffer':out.getvalue()})
maker.get_by_role('button',name='导入',exact=True).click()
maker.wait_for_load_state('networkidle')
assert maker.locator('[name=item_date]').input_value()=='2026-10-01'
from notifications import dispatch_once
from models import Notification
delivered=[]
for i in range(15):
dispatch_once(Session, lambda *args: delivered.append(args), datetime.utcnow()+timedelta(seconds=i*5))
with Session() as db:
assert db.query(Notification).filter_by(status='sent').count()==3 # Test + two results.
assert db.query(Notification).filter_by(status='cancelled').count()==2 # Superseded tasks.
assert len(delivered)==3
assert not errors, errors
browser.close()
(RUN/'summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'result':'passed','scenarios':summary,'output':str(RUN)},ensure_ascii=False))
finally:
server.close()
if __name__ == '__main__':
run()
+3 -97
View File
@@ -1,99 +1,5 @@
# -*- coding: utf-8 -*-
"""端到端冒烟:制单 → 提交 → 主管审核 → 经理批准 → 导出 PDF。"""
import http.cookiejar
import os
import re
import urllib.parse
import urllib.request
BASE = "http://127.0.0.1:8080"
OUT = r"D:\2026-09-09-task-18\tools\out"
os.makedirs(OUT, exist_ok=True)
def new_session():
cj = http.cookiejar.CookieJar()
return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def post(op, path, data):
body = urllib.parse.urlencode(data).encode()
req = urllib.request.Request(BASE + path, data=body)
r = op.open(req, timeout=30)
return r.geturl(), r.read().decode("utf-8", "replace")
def get(op, path):
r = op.open(BASE + path, timeout=30)
return r.geturl(), r.read()
def login(user, pwd):
op = new_session()
url, html = post(op, "/login", {"username": user, "password": pwd})
assert "/login" not in url, "%s 登录失败" % user
print(" login %s ok" % user)
return op
def make_order(op, doc_type, supplier_id, n_items):
url, _ = post(op, "/orders/new", {"doc_type": doc_type, "supplier_id": supplier_id})
m = re.search(r"/orders/(\d+)", url)
oid = int(m.group(1))
form = [
("order_date", "2026-09-09"), ("tax_rate", "13%"),
("supplier_name", "苏州精锐五金制品有限公司"),
("supplier_contact", "周经理"), ("supplier_phone", "13800000001"),
("payment_terms", "月结30天"), ("supplier_tax_no", "91320500MA1XXXXXXX"),
("supplier_bank_account", "4562 1088 0012 3456"),
("supplier_bank_name", "中国银行苏州工业园区支行"),
("our_company", "苏州博特维智能科技有限公司"),
("our_contact", "胡乃琛"), ("our_phone", "13913172516"),
("our_tax_no", "91320594MA1YYYYYYY"),
("delivery_place", "苏州市工业园区星汉街5号腾飞新苏工业坊B幢5楼05-06单元 胡乃琛 13913172516"),
("note", "本单为系统测试单,含 & <特殊> 字符校验"),
]
for i in range(n_items):
form += [
("item_code", "M-10%02d" % (i + 1)),
("item_spec", "测试物料 规格 A&B <%d>" % (i + 1)),
("item_unit", "件"), ("item_qty", "12.5"), ("item_price", "38.5"),
("item_date", "2026-09-20"), ("item_remark", "行%d" % (i + 1)),
]
post(op, "/orders/%d" % oid, form)
return oid
def run(tag, doc_type, n_items):
print("[%s] doc=%s items=%d" % (tag, doc_type, n_items))
maker = login("maker", "123456")
oid = make_order(maker, doc_type, 1, n_items)
print(" 制单 ok, id=%d" % oid)
post(maker, "/orders/%d/submit" % oid, {"comment": "请审核"})
rv = login("reviewer", "123456")
post(rv, "/orders/%d/review" % oid, {"comment": "同意"})
ap = login("approver", "123456")
post(ap, "/orders/%d/approve" % oid, {"comment": "批准"})
print(" 审核流程 ok")
_, data = get(ap, "/orders/%d/pdf" % oid)
path = os.path.join(OUT, "%s.pdf" % tag)
with open(path, "wb") as f:
f.write(data)
print(" PDF -> %s (%d bytes)" % (path, len(data)))
# 草稿水印版
oid2 = make_order(maker, doc_type, 1, 2)
_, data2 = get(maker, "/orders/%d/pdf" % oid2)
p2 = os.path.join(OUT, "%s_draft.pdf" % tag)
with open(p2, "wb") as f:
f.write(data2)
print(" 草稿 PDF -> %s (%d bytes)" % (p2, len(data2)))
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
run("purchase", "purchase", 3)
run("outsourcing_30rows", "outsourcing", 30)
print("ALL DONE")
raise SystemExit(main(()))
+3 -1
View File
@@ -5,7 +5,8 @@ import sys
import pymupdf
OUT = r"D:\2026-09-09-task-18\tools\out"
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'out')
os.makedirs(OUT, exist_ok=True)
def render(pdf, pages=(0,), dpi=110, prefix=None):
@@ -20,6 +21,7 @@ def render(pdf, pages=(0,), dpi=110, prefix=None):
pix.save(path)
print(path)
print("pages:", len(doc))
doc.close()
if __name__ == "__main__":
+4 -12
View File
@@ -1,13 +1,5 @@
# -*- coding: utf-8 -*-
import subprocess
import sys
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
PY = r"C:\Users\wps18\.workbuddy\binaries\python\envs\default\Scripts\python.exe"
CWD = r"D:\2026-09-09-task-18\po_app"
p = subprocess.run([PY, "-c", "import app; print('IMPORT OK')"], cwd=CWD,
capture_output=True, text=True, encoding="utf-8", errors="replace",
timeout=60)
print("RC", p.returncode)
print("STDOUT:", p.stdout[-4000:])
print("STDERR:", p.stderr[-4000:])
if __name__ == "__main__":
raise SystemExit(main(('-k', 'pages')))
+13
View File
@@ -0,0 +1,13 @@
"""Portable isolated regression suite. No running server or live database needed."""
import subprocess
import sys
from pathlib import Path
def main(extra=()):
root = Path(__file__).resolve().parents[1]
return subprocess.call([sys.executable, '-m', 'pytest', str(root / 'tests'), '-q', *extra], cwd=root)
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))
+3 -98
View File
@@ -1,100 +1,5 @@
# -*- coding: utf-8 -*-
"""模拟:经理批准也需先加章;批准通过后 PDF 上审核章与批准章并排。
用法:python test_approve_stamp.py 8099
"""
import os
import re
import sqlite3
import sys
import requests
BASE = "http://127.0.0.1:%s" % (sys.argv[1] if len(sys.argv) > 1 else "8099")
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "out")
os.makedirs(OUT, exist_ok=True)
DB = os.path.join(os.path.dirname(HERE), "po_app", "instance", "po.db")
FAILS = []
def check(label, cond, extra=""):
print((" OK " if cond else " FAIL ") + label + (" " + str(extra) if extra else ""))
if not cond:
FAILS.append(label)
def sess(u, p):
s = requests.Session()
s.post(BASE + "/login", data={"username": u, "password": p})
return s
def flash_of(h):
return " | ".join(re.sub(r"<[^>]+>", "", m.group(1)).strip()
for m in re.finditer(r'class="flash[^"]*"[^>]*>(.*?)</div>', h, re.S))
def dbq(sql, a=()):
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute(sql, a)
r = cur.fetchone()
con.close()
return r
def main():
admin = sess("admin", "admin123")
r = admin.post(BASE + "/orders/new", data={"doc_type": "purchase"})
oid = int(re.search(r"/orders/(\d+)/edit", r.url).group(1))
admin.post(BASE + "/orders/%d" % oid, data={
"supplier_name": u"测试供应商",
"item_code": ["M-1001"], "item_spec": [u"测试物料"], "item_unit": [u"件"],
"item_qty": ["10"], "item_price": ["12.5"], "item_date": [""], "item_remark": [""],
})
admin.post(BASE + "/orders/%d/submit" % oid, data={"comment": u"模拟"})
print("\n[1] 主管审核(加章)")
rv = sess("reviewer", "123456")
rv.post(BASE + "/orders/%d/review" % oid, data={"comment": u"ok", "stamped": "1"})
check("审核通过", dbq("SELECT status FROM orders WHERE id=?", (oid,))[0] == "pending_approval")
print("\n[2] 经理批准:未加章应被拒绝")
ap = sess("approver", "123456")
h = ap.post(BASE + "/orders/%d/approve" % oid, data={"comment": u"直接批", "stamped": "0"}).text
check("未加章 → 提示", u"请先加盖批准章" in flash_of(h), flash_of(h))
check("状态未变", dbq("SELECT status FROM orders WHERE id=?", (oid,))[0] == "pending_approval")
print("\n[3] 加章后批准")
ap.post(BASE + "/orders/%d/approve" % oid, data={"comment": u"同意", "stamped": "1"})
row = dbq("SELECT status, approved_by FROM orders WHERE id=?", (oid,))
check("已批准", row[0] == "approved", row)
print("\n[4] 导出 PDF(应含两枚章)")
b = admin.get(BASE + "/orders/%d/pdf" % oid).content
p = os.path.join(OUT, "seal_both.pdf")
open(p, "wb").write(b)
check("PDF 生成", len(b) > 10000, "%d bytes" % len(b))
try:
import pymupdf
doc = pymupdf.open(p)
doc[-1].get_pixmap(dpi=130).save(os.path.join(OUT, "seal_both.png"))
print(" 预览:", os.path.join(OUT, "seal_both.png"), "%d 页" % doc.page_count)
doc.close()
except Exception as e:
print(" 渲染失败:", e)
print("\n[5] 清理")
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DELETE FROM order_items WHERE order_id=?", (oid,))
cur.execute("DELETE FROM orders WHERE id=?", (oid,))
con.commit()
con.close()
print(" 已删除订单 %d" % oid)
print("\n" + ("全部通过" if not FAILS else "失败: " + "; ".join(FAILS)))
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'full_flow')))
+3 -206
View File
@@ -1,208 +1,5 @@
# -*- coding: utf-8 -*-
"""批量导入 + 需方预置 + PO 号规则 E2E 测试。
用法:先启动服务(python app.py 8099),再运行本脚本。
覆盖:物料/供应商导入的 新增 / 更新 / 跳过 / 失败 四条路径,
模板下载,PO 号 4 位规则,以及旧格式单号的兼容解析。
"""
import datetime
import os
import re
import sqlite3
import sys
import openpyxl
import requests
PORT = sys.argv[1] if len(sys.argv) > 1 else "8099"
BASE = "http://127.0.0.1:%s" % PORT
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "out")
os.makedirs(OUT, exist_ok=True)
DB = os.path.join(os.path.dirname(HERE), "po_app", "instance", "po.db")
s = requests.Session()
FAILS = []
def check(label, cond, extra=""):
print((" OK " if cond else " FAIL ") + label + (" " + str(extra) if extra else ""))
if not cond:
FAILS.append(label)
def login():
r = s.post(BASE + "/login",
data={"username": "admin", "password": "admin123"},
allow_redirects=False)
return r.status_code in (200, 302)
def make_material_xlsx(path):
wb = openpyxl.Workbook()
ws = wb.active
ws.append([u"物料代码", u"物料规格", u"单位", u"默认含税单价"])
ws.append(["M-9001", u"测试新增物料A", u"件", 12.5]) # 新增
ws.append(["M-9002", u"测试新增物料B", u"台", 88.0]) # 新增
ws.append(["M-1001", u"铝合金支架(已存在-应被更新)", u"件", 99.9]) # 已存在
ws.append(["", u"缺代码应失败", u"件", 1.0]) # 失败
wb.save(path)
def make_supplier_xlsx(path):
wb = openpyxl.Workbook()
ws = wb.active
ws.append([u"编号", u"名称", u"联系人", u"电话", u"付款方式", u"税号",
u"开户银行", u"银行帐号"])
ws.append(["S901", u"测试新增供应商A", u"王经理", "13700000001",
u"月结30天", "91320500TEST001", u"工商银行苏州分行", "1111 2222 3333"])
ws.append(["S001", u"苏州精锐五金制品有限公司", u"周经理(已更新)", "13800000001",
u"月结60天", "91320500MA1XXXXXXX", u"中国银行苏州工业园区支行",
"4562 1088 0012 3456"]) # 已存在
ws.append(["S902", u"", u"缺名称", "13900000009", u"", u"", u"", u""]) # 失败
wb.save(path)
def post_import(url, path, mode):
with open(path, "rb") as f:
return s.post(url,
files={"file": (os.path.basename(path), f,
"application/vnd.openxmlformats-officedocument."
"spreadsheetml.sheet")},
data={"mode": mode})
def flash_texts(html):
out = []
for m in re.finditer(r'class="flash[^"]*"[^>]*>(.*?)</div>', html, re.S):
out.append(re.sub(r"<[^>]+>", "", m.group(1)).strip())
return out
def main():
print("登录:", "OK" if login() else "FAIL")
# 预清理:让脚本可重复运行(上次残留的测试数据不影响本次断言)
try:
today0 = datetime.datetime.now().strftime("%Y%m%d")
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("UPDATE materials SET spec=?, unit=?, price=? WHERE code=?",
(u"铝合金支架 6061-T6 200×80×10mm", u"件", 38.5, "M-1001"))
cur.execute("UPDATE suppliers SET contact=?, payment_terms=? WHERE code=?",
(u"周经理", u"月结30天", "S001"))
cur.execute("DELETE FROM materials WHERE code IN ('M-9001','M-9002')")
cur.execute("DELETE FROM suppliers WHERE code IN ('S901','S902')")
for p in ("PO", "WO"):
cur.execute("DELETE FROM order_items WHERE order_id IN "
"(SELECT id FROM orders WHERE order_no LIKE ?)",
(p + today0 + "%",))
cur.execute("DELETE FROM orders WHERE order_no LIKE ?", (p + today0 + "%",))
con.commit()
con.close()
except Exception as e:
print("预清理失败:", e)
mp = os.path.join(OUT, "materials_test.xlsx")
sp = os.path.join(OUT, "suppliers_test.xlsx")
make_material_xlsx(mp)
make_supplier_xlsx(sp)
print("\n[1] 物料导入 update 模式")
r = post_import(BASE + "/materials/import_excel", mp, "update")
t = " | ".join(flash_texts(r.text))
print(" flash:", t)
check("新增2 / 更新1 / 失败1", u"新增 2" in t and u"更新 1" in t and u"失败 1" in t)
print("\n[2] 供应商导入 update 模式")
r = post_import(BASE + "/suppliers/import_excel", sp, "update")
t = " | ".join(flash_texts(r.text))
print(" flash:", t)
check("新增1 / 更新1 / 失败1", u"新增 1" in t and u"更新 1" in t and u"失败 1" in t)
print("\n[3] 再导一次,skip 模式(应全部跳过)")
r = post_import(BASE + "/materials/import_excel", mp, "skip")
t = " | ".join(flash_texts(r.text))
print(" 物料 flash:", t)
check("物料跳过3(M-9001/M-9002/M-1001 此时都已存在)", u"跳过 3" in t)
r = post_import(BASE + "/suppliers/import_excel", sp, "skip")
t = " | ".join(flash_texts(r.text))
print(" 供应商 flash:", t)
check("供应商跳过2(S901/S001 此时都已存在)", u"跳过 2" in t)
print("\n[4] 列表核对")
html = s.get(BASE + "/materials").text
check("M-9001 已新增", "M-9001" in html)
check("M-1001 单价已更新为 99.90", "99.90" in html)
html = s.get(BASE + "/suppliers").text
check("S901 已新增", "S901" in html)
check("联系人已更新为 周经理(已更新)", u"周经理(已更新)" in html)
print("\n[5] 模板下载")
for u in ("/materials/import_template.xlsx", "/suppliers/import_template.xlsx"):
rr = s.get(BASE + u)
check(u, rr.status_code == 200 and len(rr.content) > 3000,
"%s bytes" % len(rr.content))
print("\n[6] 新建采购单:PO 号规则 + 需方预置")
created = []
r = s.post(BASE + "/orders/new", data={"doc_type": "purchase"})
m = re.search(r"PO\d{12}", r.text)
no = m.group(0) if m else ""
created.append(no)
print(" 单号:", no)
today = datetime.datetime.now().strftime("%Y%m%d")
check("格式为 PO+年月日+4位", bool(re.match(r"^PO\d{8}\d{4}$", no)))
check("日期为今天 " + today, no.startswith("PO" + today))
for k, v in (("our_contact", u"胡乃琛"), ("our_phone", u"13913172516"),
("our_tax_no", u"91320507MA1WW8D15D"), ("tax_rate", u"13%")):
check("预置 %s = %s" % (k, v), v in r.text)
print("\n[7] 旧格式单号兼容(插入带横杠的 PO%s-0007)" % today)
legacy = "PO%s-0007" % today
try:
con = sqlite3.connect(DB)
con.execute("INSERT INTO orders (order_no, doc_type, status, order_date) "
"VALUES (?,?,?,?)",
(legacy, "purchase", "draft",
datetime.datetime.now().strftime("%Y-%m-%d")))
con.commit()
con.close()
r = s.post(BASE + "/orders/new", data={"doc_type": "purchase"})
m2 = re.search(r"PO\d{12}", r.text)
got = m2.group(0) if m2 else ""
print(" 新单号:", got, " 期望: PO%s0008" % today)
check("旧格式未导致序号重置", got == "PO%s0008" % today)
created.extend([got, legacy])
except Exception as e:
print(" 跳过,原因:", e)
print("\n[8] 清理测试数据(还原被更新的既有记录)")
try:
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("UPDATE materials SET spec=?, unit=?, price=? WHERE code=?",
(u"铝合金支架 6061-T6 200×80×10mm", u"件", 38.5, "M-1001"))
cur.execute("UPDATE suppliers SET contact=?, payment_terms=? WHERE code=?",
(u"周经理", u"月结30天", "S001"))
cur.execute("DELETE FROM materials WHERE code IN ('M-9001','M-9002')")
cur.execute("DELETE FROM suppliers WHERE code IN ('S901','S902')")
for no_ in [x for x in created if x]:
cur.execute("DELETE FROM order_items WHERE order_id IN "
"(SELECT id FROM orders WHERE order_no=?)", (no_,))
cur.execute("DELETE FROM orders WHERE order_no=?", (no_,))
con.commit()
cur.execute("SELECT COUNT(*) FROM materials WHERE code LIKE 'M-900%'")
n1 = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM suppliers WHERE code IN ('S901','S902')")
n2 = cur.fetchone()[0]
con.close()
check("测试物料已清除", n1 == 0)
check("测试供应商已清除", n2 == 0)
except Exception as e:
print(" 清理失败:", e)
print("\n" + ("全部通过" if not FAILS else "失败项: " + "; ".join(FAILS)))
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'xls_native')))
+3 -117
View File
@@ -1,119 +1,5 @@
# -*- coding: utf-8 -*-
"""编辑功能 + 明细行模板 冒烟测试。
覆盖:
1) 新建订单(0 条明细,正是"点添加一行没反应"的场景)页面必须带 rowTpl 模板行
2) 物料列表编辑:POST /materials 带 id 能改库
3) 供应商列表编辑:POST /suppliers 带 id 能改库
用法:先启动服务(python app.py 8099),再 python test_edit_row.py 8099
"""
import os
import re
import sqlite3
import sys
import requests
PORT = sys.argv[1] if len(sys.argv) > 1 else "8099"
BASE = "http://127.0.0.1:%s" % PORT
HERE = os.path.dirname(os.path.abspath(__file__))
DB = os.path.join(os.path.dirname(HERE), "po_app", "instance", "po.db")
s = requests.Session()
FAILS = []
def check(label, cond, extra=""):
print((" OK " if cond else " FAIL ") + label + (" " + str(extra) if extra else ""))
if not cond:
FAILS.append(label)
def main():
r = s.post(BASE + "/login", data={"username": "admin", "password": "admin123"})
check("登录", r.status_code == 200, r.status_code)
print("\n[1] 新建订单(0 明细)—— 明细行模板")
r = s.post(BASE + "/orders/new", data={"doc_type": "purchase"})
html = r.text
no = ""
m = re.search(r"PO\d{12}", html)
if m:
no = m.group(0)
check("新建订单页含 rowTpl 模板", 'id="rowTpl"' in html)
tpl = re.search(r'<template id="rowTpl">(.*?)</template>', html, re.S)
check("模板内含 tr 行", bool(tpl) and "<tr>" in tpl.group(1))
check("模板含 item_code 输入框", bool(tpl) and 'name="item_code"' in tpl.group(1))
check("模板含 matpick 下拉", bool(tpl) and "matpick" in tpl.group(1))
n_code = html.count('name="item_code"')
check("当前确实 0 条明细(模板是唯一来源)", n_code == 1,
"item_code 出现 %d 次" % n_code)
print("\n[2] 物料列表编辑")
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("SELECT id FROM materials WHERE code='M-1002'")
row = cur.fetchone()
con.close()
if not row:
check("找到物料 M-1002", False)
else:
mid = row[0]
r = s.post(BASE + "/materials",
data={"id": mid, "code": "M-1002", "spec": u"步进电机(已编辑)",
"unit": u"台", "price": "299.5"})
check("物料编辑请求成功", r.status_code == 200, r.status_code)
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("SELECT spec, unit, price FROM materials WHERE id=?", (mid,))
got = cur.fetchone()
check("物料已更新", got == (u"步进电机(已编辑)", u"台", 299.5), got)
cur.execute("UPDATE materials SET spec=?, price=? WHERE id=?",
(u"步进电机 57BYG250 2相", 265.0, mid))
con.commit()
con.close()
print(" 已还原 M-1002")
print("\n[3] 供应商列表编辑")
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("SELECT id FROM suppliers WHERE code='S002'")
row = cur.fetchone()
con.close()
if not row:
check("找到供应商 S002", False)
else:
sid = row[0]
r = s.post(BASE + "/suppliers",
data={"id": sid, "code": "S002", "name": u"昆山宏远电子科技有限公司",
"contact": u"陈主管(已编辑)", "phone": "13900000002",
"payment_terms": u"货到付款", "tax_no": "91320583MA2XXXXXXX",
"bank_name": u"建设银行昆山分行",
"bank_account": "3220 1988 2233 4455"})
check("供应商编辑请求成功", r.status_code == 200, r.status_code)
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("SELECT contact, payment_terms FROM suppliers WHERE id=?", (sid,))
got = cur.fetchone()
check("供应商已更新", got == (u"陈主管(已编辑)", u"货到付款"), got)
cur.execute("UPDATE suppliers SET contact=? WHERE id=?", (u"陈主管", sid))
con.commit()
con.close()
print(" 已还原 S002")
print("\n[4] 清理测试订单 " + no)
if no:
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DELETE FROM order_items WHERE order_id IN "
"(SELECT id FROM orders WHERE order_no=?)", (no,))
cur.execute("DELETE FROM orders WHERE order_no=?", (no,))
con.commit()
con.close()
print("\n" + ("全部通过" if not FAILS else "失败: " + "; ".join(FAILS)))
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'money_and_save')))
+3 -71
View File
@@ -1,73 +1,5 @@
# -*- coding: utf-8 -*-
"""测试 Excel 明细导入接口(multipart 上传)。"""
import io
import os
import re
import urllib.parse
import urllib.request
import openpyxl
BASE = "http://127.0.0.1:8080"
OUT = r"D:\2026-09-09-task-18\tools\out"
os.makedirs(OUT, exist_ok=True)
def new_session():
import http.cookiejar
cj = http.cookiejar.CookieJar()
return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
def post_form(op, path, data):
body = urllib.parse.urlencode(data).encode()
r = op.open(urllib.request.Request(BASE + path, data=body), timeout=30)
return r.geturl(), r.read()
def multipart_upload(op, path, field, filename, content, extra=None):
boundary = "----PoTestBoundary1234"
buf = io.BytesIO()
for k, v in (extra or {}).items():
buf.write(("--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n%s\r\n"
% (boundary, k, v)).encode("utf-8"))
buf.write(("--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\n"
"Content-Type: application/octet-stream\r\n\r\n" % (boundary, field, filename))
.encode("utf-8"))
buf.write(content)
buf.write(("\r\n--%s--\r\n" % boundary).encode())
req = urllib.request.Request(BASE + path, data=buf.getvalue(), method="POST")
req.add_header("Content-Type", "multipart/form-data; boundary=%s" % boundary)
r = op.open(req, timeout=30)
return r.geturl(), r.read().decode("utf-8", "replace")
def main():
op = new_session()
post_form(op, "/login", {"username": "maker", "password": "123456"})
wb = openpyxl.Workbook()
ws = wb.active
ws.append([u"物料代码", u"物料规格", u"单位", u"数量", u"含税单价", u"交期", u"备注"])
ws.append([u"X-001", u"导入测试件 & <一>", u"件", 10, 99.9, "2026-10-01", u"首件"])
ws.append([u"X-002", u"导入测试件 二", u"套", 3, 128.5, "2026-10-05", u""])
buf = io.BytesIO()
wb.save(buf)
xlsx = buf.getvalue()
url, _ = post_form(op, "/orders/new", {"doc_type": "purchase", "supplier_id": ""})
oid = int(re.search(r"/orders/(\d+)", url).group(1))
print("order id", oid)
url, html = multipart_upload(op, "/orders/%d/import_excel" % oid, "file",
"test.xlsx", xlsx, {"mode": "replace"})
ok = "已导入 2 行明细" in html
print("import rows ok:", ok)
ok2 = "99.90" in html or "99.9" in html
print("price present:", ok2)
assert ok and ok2
print("IMPORT TEST PASSED")
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'chinese_filename')))
+3 -26
View File
@@ -1,28 +1,5 @@
# -*- coding: utf-8 -*-
"""快速检查统计页与台账导出。"""
import http.cookiejar
import urllib.parse
import urllib.request
BASE = "http://127.0.0.1:8080"
def main():
cj = http.cookiejar.CookieJar()
op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
op.open(urllib.request.Request(
BASE + "/login",
data=urllib.parse.urlencode({"username": "admin", "password": "admin123"}).encode()),
timeout=30)
for path, expect in [("/stats", b"200"), ("/export/ledger", b"xlsx"),
("/orders", b"200"), ("/suppliers", b"200"),
("/materials", b"200"), ("/users", b"200"), ("/", b"200")]:
r = op.open(BASE + path, timeout=30)
data = r.read()
print(path, r.status, len(data))
assert r.status == 200, path
print("PAGES OK")
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'pages')))
+3 -154
View File
@@ -1,156 +1,5 @@
# -*- coding: utf-8 -*-
"""模拟完整流程:停用/启用/删除 + 主管审核加章,并导出带章 PDF 渲染成 PNG。
用法:先启动服务(python app.py 8099),再 python test_stamp_toggle.py 8099
"""
import os
import re
import sqlite3
import sys
import requests
PORT = sys.argv[1] if len(sys.argv) > 1 else "8099"
BASE = "http://127.0.0.1:%s" % PORT
HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "out")
os.makedirs(OUT, exist_ok=True)
DB = os.path.join(os.path.dirname(HERE), "po_app", "instance", "po.db")
FAILS = []
def check(label, cond, extra=""):
print((" OK " if cond else " FAIL ") + label + (" " + str(extra) if extra else ""))
if not cond:
FAILS.append(label)
def new_sess(u, p):
s = requests.Session()
r = s.post(BASE + "/login", data={"username": u, "password": p})
assert r.status_code == 200, r.status_code
return s
def flash_of(html):
return " | ".join(re.sub(r"<[^>]+>", "", m.group(1)).strip()
for m in re.finditer(r'class="flash[^"]*"[^>]*>(.*?)</div>',
html, re.S))
def row_html(html, key):
"""取出包含指定关键字的那个 <tr>...</tr>。"""
for m in re.finditer(r"<tr[^>]*>(.*?)</tr>", html, re.S):
if key in m.group(1):
return m.group(0)
return ""
def dbq(sql, args=()):
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute(sql, args)
r = cur.fetchone()
con.close()
return r
def main():
admin = new_sess("admin", "admin123")
print("\n[1] 供应商:停用 → 整行置灰 → 启用 → 彻底删除")
admin.post(BASE + "/suppliers",
data={"code": "S-T1", "name": u"测试供应商T1", "contact": u"测试联系人"})
sid = dbq("SELECT id FROM suppliers WHERE code='S-T1'")[0]
h = admin.get(BASE + "/suppliers").text
check("S-T1 已创建", "S-T1" in h)
check("初始为启用态(未置灰)", "row-off" not in row_html(h, "S-T1"))
admin.post(BASE + "/suppliers/%d/toggle" % sid)
h = admin.get(BASE + "/suppliers").text
check("停用后整行置灰", "row-off" in row_html(h, "S-T1"))
check("停用后按钮变为『启用』", u"启用" in row_html(h, "S-T1"))
check("停用后仍可编辑(保留入口)", "openEdit" in row_html(h, "S-T1"))
admin.post(BASE + "/suppliers/%d/toggle" % sid)
h = admin.get(BASE + "/suppliers").text
check("启用后恢复(不再置灰)", "row-off" not in row_html(h, "S-T1"))
admin.post(BASE + "/suppliers/%d/delete" % sid)
h = admin.get(BASE + "/suppliers").text
check("删除后从列表消失", "S-T1" not in h)
print("\n[2] 物料:停用 → 整行置灰 → 启用 → 彻底删除")
admin.post(BASE + "/materials",
data={"code": "M-T1", "spec": u"测试物料T1", "unit": u"件", "price": "9.9"})
mid = dbq("SELECT id FROM materials WHERE code='M-T1'")[0]
h = admin.get(BASE + "/materials").text
check("M-T1 已创建", "M-T1" in h)
check("初始为启用态(未置灰)", "row-off" not in row_html(h, "M-T1"))
admin.post(BASE + "/materials/%d/toggle" % mid)
h = admin.get(BASE + "/materials").text
check("停用后整行置灰", "row-off" in row_html(h, "M-T1"))
admin.post(BASE + "/materials/%d/toggle" % mid)
h = admin.get(BASE + "/materials").text
check("启用后恢复", "row-off" not in row_html(h, "M-T1"))
admin.post(BASE + "/materials/%d/delete" % mid)
h = admin.get(BASE + "/materials").text
check("删除后从列表消失", "M-T1" not in h)
print("\n[3] 主管审核:确认前必须先加章")
r = admin.post(BASE + "/orders/new", data={"doc_type": "purchase"})
m = re.search(r"/orders/(\d+)/edit", r.url)
oid = int(m.group(1))
admin.post(BASE + "/orders/%d" % oid, data={
"supplier_name": u"测试供应商",
"item_code": ["M-1001"], "item_spec": [u"测试物料"],
"item_unit": [u"件"], "item_qty": ["10"], "item_price": ["12.5"],
"item_date": [""], "item_remark": [""],
})
admin.post(BASE + "/orders/%d/submit" % oid, data={"comment": u"模拟提交"})
st = dbq("SELECT status FROM orders WHERE id=?", (oid,))[0]
check("订单已提交,状态待审核", st == "pending_review", st)
rv = new_sess("reviewer", "123456")
r = rv.post(BASE + "/orders/%d/review" % oid,
data={"comment": u"直接点确认试试", "stamped": "0"})
check("未加章 → 被拒绝并提示", u"请先加盖审核章" in flash_of(r.text),
flash_of(r.text))
st = dbq("SELECT status FROM orders WHERE id=?", (oid,))[0]
check("未加章时状态不变", st == "pending_review", st)
r = rv.post(BASE + "/orders/%d/review" % oid,
data={"comment": u"已核对无误", "stamped": "1"})
row = dbq("SELECT status, reviewed_by FROM orders WHERE id=?", (oid,))
check("加章后审核通过", row[0] == "pending_approval", row)
check("记录了审核人", bool(row[1]), row[1])
print("\n[4] 导出带章 PDF 并渲染预览")
pdf_bytes = admin.get(BASE + "/orders/%d/pdf" % oid).content
pdf_path = os.path.join(OUT, "seal_review.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_bytes)
check("PDF 已生成", len(pdf_bytes) > 10000, "%d bytes" % len(pdf_bytes))
try:
import fitz
doc = fitz.open(pdf_path)
pix = doc[-1].get_pixmap(dpi=130)
png_path = os.path.join(OUT, "seal_review.png")
pix.save(png_path)
print(" 渲染预览:", png_path, "共 %d 页" % doc.page_count)
doc.close()
except Exception as e:
print(" 渲染预览失败:", e)
print("\n[5] 清理测试订单")
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DELETE FROM order_items WHERE order_id=?", (oid,))
cur.execute("DELETE FROM orders WHERE id=?", (oid,))
con.commit()
con.close()
print(" 已删除订单 %d" % oid)
print("\n" + ("模拟全部通过" if not FAILS else "失败: " + "; ".join(FAILS)))
"""Compatibility entry point; tests only use temporary databases."""
from run_tests import main
if __name__ == "__main__":
main()
raise SystemExit(main(('-k', 'full_flow')))
+37 -75
View File
@@ -1,82 +1,44 @@
# -*- coding: utf-8 -*-
"""打包后 EXE 冒烟测试。
确认这些确实进了包:批量导入入口与模板、列表编辑入口、弹窗样式,
以及「新建订单明细为空时仍能添加行」所依赖的 rowTpl 模板。
用法:python verify_exe.py 8080 [数据库路径]
不传数据库路径时默认 po_app/dist/instance/po.db
"""
import os
"""Read-only page/asset checks for a running packaged app; never writes order data."""
import argparse
import getpass
import http.cookiejar
import re
import sqlite3
import sys
import requests
BASE = "http://127.0.0.1:%s" % (sys.argv[1] if len(sys.argv) > 1 else "8080")
HERE = os.path.dirname(os.path.abspath(__file__))
DB = sys.argv[2] if len(sys.argv) > 2 else os.path.join(
os.path.dirname(HERE), "po_app", "dist", "instance", "po.db")
s = requests.Session()
FAILS = []
def check(label, cond, extra=""):
print((" OK " if cond else " FAIL ") + label + (" " + str(extra) if extra else ""))
if not cond:
FAILS.append(label)
import urllib.parse
import urllib.request
def main():
r = s.post(BASE + "/login", data={"username": "admin", "password": "admin123"})
check("登录", r.status_code == 200, r.status_code)
# 注意:url_for() 会被 Jinja 渲染成实际路径,页面里不会出现函数名
for path, marks in (
("/materials", [u"批量导入 Excel", "matImpModal", "/materials/import_excel",
"/materials/import_template.xlsx", "openEdit", "editModal",
"/toggle", "/delete", u"停用", u"删除"]),
("/suppliers", [u"批量导入 Excel", "supImpModal", "/suppliers/import_excel",
"/suppliers/import_template.xlsx", "openEdit", "editModal",
"/toggle", "/delete", u"停用", u"删除"]),
):
h = s.get(BASE + path).text
missing = [m for m in marks if m not in h]
check("%s 含导入/编辑/停用删除/置灰" % path, not missing, missing)
css = s.get(BASE + "/static/style.css").text
check("style.css 含弹窗样式", "modal-mask" in css and "modal-box" in css)
for u in ("/materials/import_template.xlsx", "/suppliers/import_template.xlsx"):
r = s.get(BASE + u)
check("模板下载 %s" % u, r.status_code == 200 and len(r.content) > 3000,
"%s bytes" % len(r.content))
# 新建订单:明细为空时必须仍能添加行(依赖 rowTpl 模板存在)
r = s.post(BASE + "/orders/new", data={"doc_type": "purchase"})
h = r.text
no = ""
m = re.search(r"PO\d{12}", h)
if m:
no = m.group(0)
check("新建订单页含 rowTpl 明细行模板", 'id="rowTpl"' in h)
tpl = re.search(r'<template id="rowTpl">(.*?)</template>', h, re.S)
check("rowTpl 内含完整空行", bool(tpl) and 'name="item_code"' in tpl.group(1))
if no and os.path.exists(DB):
con = sqlite3.connect(DB)
cur = con.cursor()
cur.execute("DELETE FROM order_items WHERE order_id IN "
"(SELECT id FROM orders WHERE order_no=?)", (no,))
cur.execute("DELETE FROM orders WHERE order_no=?", (no,))
con.commit()
con.close()
print(" 已清理测试订单 %s" % no)
print("\n" + ("打包版冒烟全部通过" if not FAILS else "失败: " + "; ".join(FAILS)))
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--url',default='http://127.0.0.1:8080')
parser.add_argument('--username',required=True,help='Use an initialized admin account')
args=parser.parse_args()
base=args.url.rstrip('/')
opener=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
def get(path):
with opener.open(base+path,timeout=30) as response:
return response.geturl(),response.read()
_,body=get('/login')
match=re.search(r'name="csrf_token" value="([^"]+)"',body.decode('utf-8'))
if not match:
raise SystemExit('Complete first-time setup before running this check.')
fields={'username':args.username,'password':getpass.getpass('Password: '),'csrf_token':match[1]}
with opener.open(urllib.request.Request(base+'/login',data=urllib.parse.urlencode(fields).encode()),timeout=30) as response:
if response.geturl().rstrip('/') != base:
raise SystemExit('Login failed or password change is required.')
response.read()
for path,marker in [('/orders/new',b'request_key'),('/users',b'username'),('/materials',b'matImpModal'),
('/suppliers',b'supImpModal'),('/static/money.js',b'lineCents'),('/static/style.css',b'modal-mask')]:
url,body=get(path)
if url != base+path or marker not in body:
raise SystemExit('Failed page/asset check: '+path)
print('PASS',path)
for path in ['/materials/import_template.xlsx','/suppliers/import_template.xlsx','/import_template.xlsx']:
url,body=get(path)
if url != base+path or not body.startswith(b'PK'):
raise SystemExit('Failed workbook check: '+path)
print('PASS',path)
print('Packaged page/asset checks passed; order data was not modified.')
if __name__ == "__main__":
if __name__=='__main__':
main()