feat(pl): 购买证运输证改为客户品类额度并支持齐备完善。
按客户与证用品类自动选证,未配置生效时间拦住安排;齐备仅放行安排,归档前须完善挂证,并补充迁移与测试造数脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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())
|
||||
Reference in new issue
Block a user