feat(pl): 购买证运输证改为客户品类额度并支持齐备完善。

按客户与证用品类自动选证,未配置生效时间拦住安排;齐备仅放行安排,归档前须完善挂证,并补充迁移与测试造数脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-14 23:31:01 +08:00
1 parent aad74505c0
commit cd94ebc56d
31 files changed
+2835 -39

No files matched your search

@@ -0,0 +1,302 @@
#!/usr/bin/env python3
"""回填购买证/运输证的客户与证用品类(客户+品类额度改造)。
对已有证件:
1. 按证面品名 product_name_on_cert 去重创建 pl_cert_category(已存在同名则复用)
2. 购买证 category_id ← 与证面品名同名的品类
3. 购买证 customer_id ← buyer_name 精确匹配 crm_customer.customer_name
4. 运输证 customer_id ← 优先非内部客户的收货方 consignee_name,否则 shipper_name
5. 可选:品类关联系统产品(仅 product_name 与品类名完全一致)
不自动:
- 模糊归并「硫酸」与「98% 工业硫酸」为同一品类(需人工合并)
- 模糊挂接相近产品名(默认只精确匹配;可用 --link-exact-products)
用法:
cd backend && source venv/bin/activate
PYTHONPATH=. python scripts/backfill_pl_cert_customer_category.py --dry-run
PYTHONPATH=. python scripts/backfill_pl_cert_customer_category.py --apply
PYTHONPATH=. python scripts/backfill_pl_cert_customer_category.py --apply --link-exact-products
环境:读取 backend/.env 的 DATABASE_URL(可用 ENV_FILE 覆盖路径)。
"""
from __future__ import annotations
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional, Tuple
from urllib.parse import unquote, urlparse
from uuid import uuid4
BACKEND_ROOT = Path(__file__).resolve().parents[1]
if str(BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(BACKEND_ROOT))
INTERNAL_HINTS = ('山峡', '云栋')
def load_database_url() -> str:
env_file = os.environ.get('ENV_FILE') or str(BACKEND_ROOT / '.env')
text = Path(env_file).read_text(encoding='utf-8')
for line in text.splitlines():
line = line.strip()
if not line or line.startswith('#'):
continue
if line.startswith('DATABASE_URL='):
return line.split('=', 1)[1].strip().strip('"').strip("'")
raise SystemExit(f'DATABASE_URL not found in {env_file}')
def connect():
import pymysql
raw = load_database_url().replace('mysql+pymysql://', 'mysql://', 1)
p = urlparse(raw)
return pymysql.connect(
host=p.hostname or '127.0.0.1',
port=p.port or 3306,
user=unquote(p.username or ''),
password=unquote(p.password or ''),
database=(p.path or '/').lstrip('/'),
charset='utf8mb4',
autocommit=False,
)
def is_internal_name(name: Optional[str]) -> bool:
text = (name or '').strip()
return any(h in text for h in INTERNAL_HINTS)
def normalize_name(value: Optional[str]) -> str:
return (value or '').strip()
def main() -> int:
parser = argparse.ArgumentParser(description='Backfill cert customer_id / category_id')
mode = parser.add_mutually_exclusive_group(required=True)
mode.add_argument('--dry-run', action='store_true', help='只打印计划,不写库')
mode.add_argument('--apply', action='store_true', help='写入数据库')
parser.add_argument('--tenant', default='shanxia2.0', help='租户编号')
parser.add_argument(
'--link-exact-products',
action='store_true',
help='把品类名与 CRM 产品名完全一致的产品写入 pl_cert_category_product',
)
args = parser.parse_args()
apply = bool(args.apply)
tenant = args.tenant
now = datetime.now().replace(microsecond=0)
conn = connect()
cur = conn.cursor()
try:
# --- load customers ---
cur.execute(
"SELECT id, customer_name FROM crm_customer WHERE tenant_no=%s AND del_flag=0",
(tenant,),
)
customer_by_name: Dict[str, str] = {}
dup_names = set()
for cid, cname in cur.fetchall():
key = normalize_name(cname)
if not key:
continue
if key in customer_by_name and customer_by_name[key] != cid:
dup_names.add(key)
else:
customer_by_name[key] = cid
for key in dup_names:
customer_by_name.pop(key, None)
# --- existing categories ---
cur.execute(
"SELECT id, category_name FROM pl_cert_category WHERE tenant_no=%s AND del_flag=0",
(tenant,),
)
category_by_name = {normalize_name(n): i for i, n in cur.fetchall()}
# --- distinct cert face names ---
cur.execute(
"""
SELECT DISTINCT product_name_on_cert FROM (
SELECT product_name_on_cert FROM pl_purchase_cert
WHERE tenant_no=%s AND del_flag=0
UNION
SELECT product_name_on_cert FROM pl_transport_cert
WHERE tenant_no=%s AND del_flag=0
) t WHERE product_name_on_cert IS NOT NULL AND TRIM(product_name_on_cert) <> ''
""",
(tenant, tenant),
)
face_names = [normalize_name(r[0]) for r in cur.fetchall() if normalize_name(r[0])]
print(f'tenant={tenant} apply={apply} face_names={face_names}')
print(f'crm_customers={len(customer_by_name)} ambiguous_names={sorted(dup_names)[:10]}')
created_categories = []
for name in face_names:
if name in category_by_name:
continue
cid = str(uuid4())
created_categories.append((cid, name))
category_by_name[name] = cid
print(f' [category+] {name} -> {cid}')
if apply:
cur.execute(
"""
INSERT INTO pl_cert_category
(id, category_name, status, tenant_no, create_dept_id, create_time, update_time, del_flag)
VALUES (%s,%s,'1',%s,'',%s,%s,0)
""",
(cid, name, tenant, now, now),
)
# --- purchase backfill ---
cur.execute(
"""
SELECT id, cert_no, product_name_on_cert, buyer_name, customer_id, category_id, status
FROM pl_purchase_cert WHERE tenant_no=%s AND del_flag=0
""",
(tenant,),
)
purchase_rows = cur.fetchall()
purchase_ok = purchase_miss_customer = purchase_miss_category = 0
for pid, cert_no, face, buyer, cust_id, cat_id, status in purchase_rows:
face_n = normalize_name(face)
buyer_n = normalize_name(buyer)
new_cat = category_by_name.get(face_n)
new_cust = customer_by_name.get(buyer_n)
updates = {}
if not cat_id and new_cat:
updates['category_id'] = new_cat
if not cust_id and new_cust:
updates['customer_id'] = new_cust
if updates:
print(
f' [purchase] {cert_no} status={status} face={face_n!r} buyer={buyer_n!r} -> {updates}'
)
if apply:
sets = ', '.join(f'{k}=%s' for k in updates)
cur.execute(
f"UPDATE pl_purchase_cert SET {sets}, update_time=%s WHERE id=%s",
(*updates.values(), now, pid),
)
purchase_ok += 1
if not (cust_id or updates.get('customer_id')):
purchase_miss_customer += 1
print(f' [purchase!] {cert_no} 无法匹配客户 buyer={buyer_n!r}')
if not (cat_id or updates.get('category_id')):
purchase_miss_category += 1
# --- transport backfill ---
cur.execute(
"""
SELECT id, cert_no, product_name_on_cert, shipper_name, consignee_name, customer_id, status
FROM pl_transport_cert WHERE tenant_no=%s AND del_flag=0
""",
(tenant,),
)
transport_ok = transport_miss = 0
for tid, cert_no, face, shipper, consignee, cust_id, status in cur.fetchall():
candidates = []
consignee_n = normalize_name(consignee)
shipper_n = normalize_name(shipper)
if consignee_n and not is_internal_name(consignee_n):
candidates.append(('consignee', consignee_n))
if shipper_n and not is_internal_name(shipper_n):
candidates.append(('shipper', shipper_n))
if not candidates:
if consignee_n:
candidates.append(('consignee', consignee_n))
elif shipper_n:
candidates.append(('shipper', shipper_n))
new_cust = None
picked = None
for role, name in candidates:
hit = customer_by_name.get(name)
if hit:
new_cust = hit
picked = (role, name)
break
if not cust_id and new_cust:
print(
f' [transport] {cert_no} status={status} via {picked} -> customer_id={new_cust}'
)
if apply:
cur.execute(
"UPDATE pl_transport_cert SET customer_id=%s, update_time=%s WHERE id=%s",
(new_cust, now, tid),
)
transport_ok += 1
elif not cust_id:
transport_miss += 1
print(
f' [transport!] {cert_no} 无法匹配客户 consignee={consignee_n!r} shipper={shipper_n!r}'
)
# --- optional exact product links ---
product_links = 0
if args.link_exact_products:
cur.execute(
"SELECT id, product_name FROM crm_product WHERE tenant_no=%s AND del_flag=0",
(tenant,),
)
products_by_name: Dict[str, list] = {}
for prid, pname in cur.fetchall():
key = normalize_name(pname)
products_by_name.setdefault(key, []).append(prid)
for cname, category_id in category_by_name.items():
ids = products_by_name.get(cname) or []
if len(ids) != 1:
if ids:
print(f' [product?] 品类 {cname!r} 精确匹配到 {len(ids)} 个产品,跳过')
continue
product_id = ids[0]
cur.execute(
"""
SELECT id FROM pl_cert_category_product
WHERE tenant_no=%s AND category_id=%s AND product_id=%s AND del_flag=0
""",
(tenant, category_id, product_id),
)
if cur.fetchone():
continue
print(f' [product+] 品类 {cname!r} <-> product {product_id}')
product_links += 1
if apply:
cur.execute(
"""
INSERT INTO pl_cert_category_product
(id, category_id, product_id, scope_status, tenant_no, create_dept_id,
create_time, update_time, del_flag)
VALUES (%s,%s,%s,'active',%s,'',%s,%s,0)
""",
(str(uuid4()), category_id, product_id, tenant, now, now),
)
print('--- summary ---')
print(f'categories_created={len(created_categories)}')
print(f'purchase_updated={purchase_ok} miss_customer={purchase_miss_customer} miss_category={purchase_miss_category}')
print(f'transport_updated={transport_ok} miss_customer={transport_miss}')
print(f'product_links={product_links}')
if apply:
conn.commit()
print('COMMITTED')
else:
conn.rollback()
print('DRY-RUN (rolled back)')
return 0
except Exception:
conn.rollback()
raise
finally:
cur.close()
conn.close()
if __name__ == '__main__':
raise SystemExit(main())
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""为客户+品类证件额度联调造数(测试库)。
造:客户、报价(accepted)、合同(executing)+ 合同产品、
2 张购买证(10吨早到期 / 100吨晚到期)、2 张运输证(引用对应购买证)、期初 opening。
不造:品类↔系统产品关联(由用户在界面配置)。
用法:
cd backend && source venv/bin/activate
PYTHONPATH=. python scripts/seed_pl_customer_cert_test_data.py --dry-run
PYTHONPATH=. python scripts/seed_pl_customer_cert_test_data.py --apply
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from datetime import date, datetime, timedelta
from decimal import Decimal
from pathlib import Path
from urllib.parse import unquote, urlparse
from uuid import uuid4
BACKEND_ROOT = Path(__file__).resolve().parents[1]
TENANT = 'shanxia2.0'
# 固定前缀,便于识别/清理
TAG = 'PL-CERT-SEED-20261114'
CUSTOMER_NAME = 'PL证件额度测试客户'
PRODUCT_ID = '339E64E02E7C41F3A04AACCE71E699BE' # 98% 工业硫酸 is_toxic=1 on
OWNER_USER_ID = '02584218205246EEB5F2AC96DF465A9D'
OWNER_NAME = '方金强'
SELLER = '安庆市山峡化工有限公司'
VEHICLE = '皖A9TEST1'
def load_url():
env_file = os.environ.get('ENV_FILE') or str(BACKEND_ROOT / '.env')
for line in Path(env_file).read_text(encoding='utf-8').splitlines():
if line.startswith('DATABASE_URL='):
return line.split('=', 1)[1].strip().strip('"').strip("'")
raise SystemExit('DATABASE_URL missing')
def connect():
import pymysql
raw = load_url().replace('mysql+pymysql://', 'mysql://', 1)
p = urlparse(raw)
return pymysql.connect(
host=p.hostname, port=p.port or 3306,
user=unquote(p.username or ''), password=unquote(p.password or ''),
database=(p.path or '/').lstrip('/'), charset='utf8mb4', autocommit=False,
)
def uid(suffix: str) -> str:
# 稳定 ID,重复执行可先删再插
return f'{TAG}-{suffix}'
def main() -> int:
parser = argparse.ArgumentParser()
g = parser.add_mutually_exclusive_group(required=True)
g.add_argument('--dry-run', action='store_true')
g.add_argument('--apply', action='store_true')
args = parser.parse_args()
apply = args.apply
now = datetime.now().replace(microsecond=0)
today = date.today()
ids = {
'customer': uid('customer'),
'quote': uid('quote'),
'quote_product': uid('quote-prod'),
'contract': uid('contract'),
'contract_product': uid('contract-prod'),
'purchase_early': uid('purchase-10t'),
'purchase_late': uid('purchase-100t'),
'transport_early': uid('transport-10t'),
'transport_late': uid('transport-100t'),
'ref_early': uid('ref-10t'),
'ref_late': uid('ref-100t'),
'open_pe': uid('open-p-10t'),
'open_pl': uid('open-p-100t'),
'open_te': uid('open-t-10t'),
'open_tl': uid('open-t-100t'),
}
conn = connect()
cur = conn.cursor()
try:
# category for 98% 工业硫酸
cur.execute(
"SELECT id FROM pl_cert_category WHERE tenant_no=%s AND del_flag=0 AND category_name=%s",
(TENANT, '98% 工业硫酸'),
)
row = cur.fetchone()
if not row:
raise SystemExit('缺少品类「98% 工业硫酸」,请先跑 backfill 或手工创建')
category_id = row[0]
cur.execute(
"SELECT product_no, product_name, unit, COALESCE(price,1000) FROM crm_product WHERE id=%s AND del_flag=0",
(PRODUCT_ID,),
)
prod = cur.fetchone()
if not prod:
raise SystemExit(f'产品不存在: {PRODUCT_ID}')
product_no, product_name, unit, price = prod
price = Decimal(price)
qty = Decimal('100')
subtotal = (price * qty).quantize(Decimal('0.01'))
print('=== seed plan ===')
print(f'customer={CUSTOMER_NAME} id={ids["customer"]}')
print(f'product={product_name} ({PRODUCT_ID}) toxic unit={unit}')
print(f'category_id={category_id}')
print(f'vehicle={VEHICLE}')
print(f'purchase early 10 / late 100 ; transport pair each')
# cleanup previous seed with same ids
cleanup_tables = [
('pl_cert_qty_log', 'id', [ids['open_pe'], ids['open_pl'], ids['open_te'], ids['open_tl']]),
('pl_transport_cert_purchase_ref', 'id', [ids['ref_early'], ids['ref_late']]),
('pl_transport_cert', 'id', [ids['transport_early'], ids['transport_late']]),
('pl_purchase_cert', 'id', [ids['purchase_early'], ids['purchase_late']]),
('crm_contract_product', 'id', [ids['contract_product']]),
('crm_contract', 'id', [ids['contract']]),
('crm_quote_product', 'id', [ids['quote_product']]),
('crm_quote', 'id', [ids['quote']]),
('crm_customer', 'id', [ids['customer']]),
]
for table, col, values in cleanup_tables:
for vid in values:
cur.execute(f"DELETE FROM {table} WHERE {col}=%s", (vid,))
# customer
cur.execute(
"""
INSERT INTO crm_customer
(id, customer_no, customer_name, status, deal_status, owner_user_id, owner_user_name,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag)
VALUES (%s,%s,%s,'normal','undeal',%s,%s,%s,'',%s,%s,%s,0)
""",
(ids['customer'], f'C-{TAG}', CUSTOMER_NAME, OWNER_USER_ID, OWNER_NAME,
TENANT, OWNER_USER_ID, now, now),
)
print('+ customer')
# quote
quote_no = f'BJ-{TAG}'
cur.execute(
"""
INSERT INTO crm_quote
(id, quote_no, quote_title, quote_type, customer_id, customer_name,
owner_user_id, version, is_latest, total_amount, final_amount,
quote_date, expire_date, status, tenant_no, create_dept_id, create_by,
create_time, update_time, del_flag)
VALUES (%s,%s,%s,'sales',%s,%s,%s,1,1,%s,%s,%s,%s,'accepted',%s,'',%s,%s,%s,0)
""",
(ids['quote'], quote_no, f'{CUSTOMER_NAME}-硫酸报价', ids['customer'], CUSTOMER_NAME,
OWNER_USER_ID, subtotal, subtotal, today, today + timedelta(days=30),
TENANT, OWNER_USER_ID, now, now),
)
cur.execute(
"""
INSERT INTO crm_quote_product
(id, quote_id, product_id, product_name, product_no, unit, quantity, price, discount,
subtotal, sort, tenant_no, create_dept_id, create_by, create_time, update_time, del_flag)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,100,%s,1,%s,'',%s,%s,%s,0)
""",
(ids['quote_product'], ids['quote'], PRODUCT_ID, product_name, product_no, unit,
qty, price, subtotal, TENANT, OWNER_USER_ID, now, now),
)
print('+ quote', quote_no)
# contract
contract_no = f'HT-{TAG}'
cur.execute(
"""
INSERT INTO crm_contract
(id, contract_no, contract_name, customer_id, customer_name, quote_id,
contract_type, amount, currency, tax_rate, status, supplier_id,
party_a, party_b, start_date, end_date, sign_date,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag)
VALUES (%s,%s,%s,%s,%s,%s,'sale',%s,'CNY',13,'executing','1',%s,%s,%s,%s,%s,
%s,'',%s,%s,%s,0)
""",
(ids['contract'], contract_no, f'{CUSTOMER_NAME}-购销合同', ids['customer'], CUSTOMER_NAME,
ids['quote'], subtotal, SELLER, CUSTOMER_NAME, today, today + timedelta(days=365), today,
TENANT, OWNER_USER_ID, now, now),
)
cur.execute(
"""
INSERT INTO crm_contract_product
(id, contract_id, product_id, product_name, product_no, unit, quantity, price,
tax_rate, subtotal, delivery_status, tenant_no, create_dept_id, create_by,
create_time, update_time, del_flag)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,13,%s,'pending',%s,'',%s,%s,%s,0)
""",
(ids['contract_product'], ids['contract'], PRODUCT_ID, product_name, product_no, unit,
qty, price, subtotal, TENANT, OWNER_USER_ID, now, now),
)
print('+ contract', contract_no, 'product_row', ids['contract_product'])
def insert_purchase(pid, cert_no, total, valid_to, face='98% 工业硫酸'):
cur.execute(
"""
INSERT INTO pl_purchase_cert
(id, cert_no, issue_date, product_name_on_cert, total_qty, quantity_unit,
valid_from, valid_to, valid_use_text, entry_mode, status,
verified_by, verified_time, reserved_qty, used_qty, opening_single_use_consumed,
revision, buyer_name, seller_name, customer_id, category_id,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag)
VALUES (%s,%s,%s,%s,%s,'dun',%s,%s,'多次有效','manual','active',
%s,%s,0,0,0,1,%s,%s,%s,%s,%s,'',%s,%s,%s,0)
""",
(pid, cert_no, today, face, Decimal(total), today, valid_to,
OWNER_USER_ID, now, CUSTOMER_NAME, SELLER, ids['customer'], category_id,
TENANT, OWNER_USER_ID, now, now),
)
op = str(uuid4())
cur.execute(
"""
INSERT INTO pl_cert_qty_log
(id, operation_id, request_hash, cert_type, cert_id, event_type, quantity_unit,
total_delta, reserved_delta, used_delta,
before_total_qty, after_total_qty, before_reserved_qty, after_reserved_qty,
before_used_qty, after_used_qty, reason, tenant_no, create_dept_id, create_by,
create_time, update_time, del_flag)
VALUES (%s,%s,%s,'purchase',%s,'opening','dun',
0,0,0,%s,%s,0,0,0,0,'seed opening',%s,'',%s,%s,%s,0)
""",
(str(uuid4()) if pid.endswith('10t') else str(uuid4()), op, op, pid,
Decimal(total), Decimal(total), TENANT, OWNER_USER_ID, now, now),
)
# fix opening ids to stable ones
def insert_opening(log_id, cert_type, cert_id, total):
op = log_id
cur.execute(
"""
INSERT INTO pl_cert_qty_log
(id, operation_id, request_hash, cert_type, cert_id, event_type, quantity_unit,
total_delta, reserved_delta, used_delta,
before_total_qty, after_total_qty, before_reserved_qty, after_reserved_qty,
before_used_qty, after_used_qty, reason, tenant_no, create_dept_id, create_by,
create_time, update_time, del_flag)
VALUES (%s,%s,%s,%s,%s,'opening','dun',
0,0,0,%s,%s,0,0,0,0,'seed opening',%s,'',%s,%s,%s,0)
""",
(log_id, op, op, cert_type, cert_id, Decimal(total), Decimal(total),
TENANT, OWNER_USER_ID, now, now),
)
# rewrite purchase insert without nested opening
def insert_purchase2(pid, cert_no, total, valid_to, open_id):
cur.execute(
"""
INSERT INTO pl_purchase_cert
(id, cert_no, issue_date, product_name_on_cert, total_qty, quantity_unit,
valid_from, valid_to, valid_use_text, entry_mode, status,
verified_by, verified_time, reserved_qty, used_qty, opening_single_use_consumed,
revision, buyer_name, seller_name, customer_id, category_id,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag, remark)
VALUES (%s,%s,%s,'98%% 工业硫酸',%s,'dun',%s,%s,'多次有效','manual','active',
%s,%s,0,0,0,1,%s,%s,%s,%s,%s,'',%s,%s,%s,0,%s)
""",
(pid, cert_no, today, Decimal(total), today, valid_to,
OWNER_USER_ID, now, CUSTOMER_NAME, SELLER, ids['customer'], category_id,
TENANT, OWNER_USER_ID, now, now, TAG),
)
insert_opening(open_id, 'purchase', pid, total)
def insert_transport2(tid, cert_no, total, valid_to, open_id, purchase_id, ref_id):
plates = json.dumps([VEHICLE], ensure_ascii=False)
cur.execute(
"""
INSERT INTO pl_transport_cert
(id, cert_no, issue_date, product_name_on_cert, total_qty, quantity_unit,
valid_from, valid_to, valid_use_text, entry_mode, status,
verified_by, verified_time, reserved_qty, used_qty, opening_single_use_consumed,
revision, shipper_name, consignee_name, vehicle_numbers_json, customer_id,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag, remark)
VALUES (%s,%s,%s,'98%% 工业硫酸',%s,'dun',%s,%s,'多次有效','manual','active',
%s,%s,0,0,0,1,%s,%s,%s,%s,%s,'',%s,%s,%s,0,%s)
""",
(tid, cert_no, today, Decimal(total), today, valid_to,
OWNER_USER_ID, now, SELLER, CUSTOMER_NAME, plates, ids['customer'],
TENANT, OWNER_USER_ID, now, now, TAG),
)
insert_opening(open_id, 'transport', tid, total)
cur.execute(
"""
INSERT INTO pl_transport_cert_purchase_ref
(id, transport_cert_id, reference_type, reference_no, purchase_cert_id,
tenant_no, create_dept_id, create_by, create_time, update_time, del_flag)
VALUES (%s,%s,'purchase_permit',%s,%s,%s,'',%s,%s,%s,0)
""",
(ref_id, tid, cert_no.replace('Y-', 'G-'), purchase_id, TENANT, OWNER_USER_ID, now, now),
)
early_to = today + timedelta(days=30)
late_to = today + timedelta(days=180)
insert_purchase2(ids['purchase_early'], f'G-{TAG}-10T', 10, early_to, ids['open_pe'])
insert_purchase2(ids['purchase_late'], f'G-{TAG}-100T', 100, late_to, ids['open_pl'])
insert_transport2(
ids['transport_early'], f'Y-{TAG}-10T', 10, early_to, ids['open_te'],
ids['purchase_early'], ids['ref_early'],
)
insert_transport2(
ids['transport_late'], f'Y-{TAG}-100T', 100, late_to, ids['open_tl'],
ids['purchase_late'], ids['ref_late'],
)
print('+ purchase x2 + transport x2 + opening + refs')
print('--- ids ---')
for k, v in ids.items():
print(f'{k}: {v}')
print('--- how to test ---')
print(f'1. 证用品类「98% 工业硫酸」关联产品 {PRODUCT_ID}({product_name})')
print(f'2. 新建运输计划:客户={CUSTOMER_NAME},产品用上述合同行')
print(f'3. 安排车辆请用车号 {VEHICLE}(或把运输证车号改成你的车)')
print('4. 测规则A:计划量 50 吨时,应跳过 10 吨早到期证,选用 100 吨证')
if apply:
conn.commit()
print('COMMITTED')
else:
conn.rollback()
print('DRY-RUN rolled back')
return 0
except Exception:
conn.rollback()
raise
finally:
cur.close()
conn.close()
if __name__ == '__main__':
raise SystemExit(main())