Files
shanxia2.0/backend/scripts/seed_pl_customer_cert_test_data.py
T
wuzilongandCursor cd94ebc56d feat(pl): 购买证运输证改为客户品类额度并支持齐备完善。
按客户与证用品类自动选证,未配置生效时间拦住安排;齐备仅放行安排,归档前须完善挂证,并补充迁移与测试造数脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-14 23:31:01 +08:00

344 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())