"""Link non-Beijing-Tongrentang quotations to existing contract opportunities."""
|
import sys,json
|
from pathlib import Path
|
from datetime import datetime
|
from decimal import Decimal
|
P=Path(__file__).parent
|
sys.stdout.reconfigure(encoding='utf8')
|
sys.path.insert(0,str(P.parent/'iqc-import-20261012/deps'))
|
import pymysql
|
assert not (P/'result.json').exists(),'Already processed; verify instead of repeating.'
|
minimum=datetime(2026,9,17)
|
c=pymysql.connect(host='36.213.150.237',port=10000,user='root',password=sys.stdin.readline().strip(),database='mom-hm',charset='utf8mb4',autocommit=False,cursorclass=pymysql.cursors.DictCursor)
|
result={'quotes':[],'new_business_product_ids':[],'new_permission_ids':[],'business_headers_created':0,'excluded_customer_keyword':'北京同仁堂'}
|
try:
|
with c.cursor() as q:
|
q.execute("SET time_zone='+08:00'")
|
q.execute('SELECT DATABASE() db,NOW() now,GET_LOCK(%s,10) locked',('quotation-opportunities-20261014',));state=q.fetchone();assert state['db']=='mom-hm' and state['locked']==1;now=state['now']
|
tabs=['crm_customer','crm_business','crm_business_product','crm_sale_quotation','crm_sale_quotation_product','crm_contract','crm_contract_product','erp_sale_order','erp_sale_order_items','crm_permission','crm_invoice','crm_receivable','crm_receivable_plan']
|
before={};data={};schemas={}
|
for t in tabs:
|
q.execute('SELECT * FROM '+t+' WHERE deleted=0 ORDER BY id FOR UPDATE');rs=q.fetchall();before[t]=rs;data[t]={x['id']:x for x in rs}
|
q.execute('SHOW COLUMNS FROM '+t);schemas[t]={x['Field'] for x in q.fetchall()}
|
q.execute('SELECT status FROM system_users WHERE id=291 AND deleted=0');assert q.fetchone()['status']==0
|
backup=P/('locked-before-'+now.strftime('%Y%m%d-%H%M%S')+'.json')
|
backup.write_text(json.dumps({'state':state,'tables':before},ensure_ascii=False,default=str,indent=2),encoding='utf8')
|
allowed={};expected={}
|
def insert(t,v,day):
|
v={**v,'creator':'291','updater':'291','create_time':day,'update_time':day,'deleted':0};assert set(v)<=schemas[t]
|
q.execute('INSERT INTO '+t+' ('+','.join('`'+k+'`' for k in v)+') VALUES ('+','.join(['%s']*len(v))+')',list(v.values()));return q.lastrowid
|
def update(t,id,v,day):
|
v={**v,'updater':'291','update_time':day};assert set(v)<=schemas[t]
|
q.execute('UPDATE '+t+' SET '+','.join('`'+k+'`=%s' for k in v)+' WHERE deleted=0 AND id=%s',[*v.values(),id]);assert q.rowcount==1
|
allowed.setdefault((t,id),set()).update(v);expected.setdefault((t,id),{}).update(v)
|
quotes=[x for x in before['crm_sale_quotation'] if '北京同仁堂' not in data['crm_customer'][x['customer_id']]['name']]
|
seen=set()
|
for qt in quotes:
|
ct=data['crm_contract'][qt['contract_id']];assert ct['customer_id']==qt['customer_id'] and ct['total_price']==qt['total_price']
|
b=data['crm_business'].get(ct['business_id']);assert b and b['customer_id']==qt['customer_id'] and b['total_price']==qt['total_price']
|
assert qt['business_id'] in (None,b['id']) and b['id'] not in seen;seen.add(b['id'])
|
order=data['erp_sale_order'][ct['order_id']];assert order['contract_id']==ct['id'] and order['customer_id']==qt['customer_id']
|
assert b['order_id'] in (None,order['id']) and b['order_no'] in (None,'',order['no'])
|
day=max(qt['quotation_time'],qt['create_time'],minimum);assert day<=now
|
update('crm_sale_quotation',qt['id'],{'business_id':b['id'],'quotation_time':day,'create_time':day},max(qt['update_time'],day))
|
quote_lines=[x for x in before['crm_sale_quotation_product'] if x['quotation_id']==qt['id']]
|
contract_lines=[x for x in before['crm_contract_product'] if x['contract_id']==ct['id']];assert contract_lines
|
assert sorted((x['item_id'],x['item_unit_id'],x['count']) for x in quote_lines)==sorted((x['item_id'],x['item_unit_id'],x['count']) for x in contract_lines)
|
for l in quote_lines:
|
if l['create_time']!=day:update('crm_sale_quotation_product',l['id'],{'create_time':day},max(l['update_time'],day))
|
existing_products=[x for x in before['crm_business_product'] if x['business_id']==b['id']]
|
assert not existing_products,'Existing business product rows require separate reconciliation'
|
total=sum(x['total_price'] for x in contract_lines);assert total==ct['total_product_price']==ct['total_price'] and ct['discount_percent']==0
|
for l in contract_lines:
|
id=insert('crm_business_product',{'business_id':b['id'],'item_id':l['item_id'],'item_unit_id':l['item_unit_id'],'item_price':l['item_price'],'business_price':l['contract_price'],'count':l['count'],'total_price':l['total_price']},day);result['new_business_product_ids'].append(id)
|
update('crm_business',b['id'],{'order_id':order['id'],'order_no':order['no'],'total_product_price':total,'discount_percent':Decimal(0)},now)
|
permissions=[x for x in before['crm_permission'] if x['biz_type']==4 and x['biz_id']==b['id'] and x['user_id']==b['owner_user_id']]
|
if not permissions:result['new_permission_ids'].append(insert('crm_permission',{'biz_type':4,'biz_id':b['id'],'user_id':b['owner_user_id'],'level':1},day))
|
else:assert any(x['level']==1 for x in permissions),'Business owner permission is incomplete'
|
result['quotes'].append({'id':qt['id'],'no':qt['no'],'customer':data['crm_customer'][qt['customer_id']]['name'],'business_id':b['id'],'contract_id':ct['id'],'order_id':order['id'],'date':str(day),'old_date':str(qt['quotation_time']),'date_changed':day!=qt['quotation_time'] or day!=qt['create_time'],'product_lines':len(contract_lines)})
|
assert len(quotes)==16 and len(result['new_business_product_ids'])==34
|
after={}
|
for t,rows in before.items():
|
q.execute('SELECT * FROM '+t+' WHERE deleted=0 ORDER BY id');rs=q.fetchall();after[t]=rs;byid={x['id']:x for x in rs}
|
for old in rows:
|
new=byid[old['id']];diff={k for k in old if old[k]!=new[k]};assert diff<=allowed.get((t,old['id']),set()),(t,old['id'],diff)
|
for k,v in expected.get((t,old['id']),{}).items():assert new[k]==v
|
newids=set(result['new_business_product_ids']) if t=='crm_business_product' else set(result['new_permission_ids']) if t=='crm_permission' else set()
|
assert set(byid)-{x['id'] for x in rows}==newids
|
for x in result['quotes']:
|
qt=next(r for r in after['crm_sale_quotation'] if r['id']==x['id']);b=next(r for r in after['crm_business'] if r['id']==x['business_id'])
|
assert minimum<=qt['quotation_time']==qt['create_time']<=now and qt['business_id']==b['id'] and qt['customer_id']==b['customer_id']
|
assert sum(r['total_price'] for r in after['crm_business_product'] if r['business_id']==b['id'])==b['total_product_price']==b['total_price']
|
c.commit()
|
for t,rs in after.items():
|
q.execute('SELECT * FROM '+t+' WHERE deleted=0 ORDER BY id');assert q.fetchall()==rs
|
result.update(committed=True,verified=True,at=str(now),quotes_linked=len(quotes),quote_dates_changed=sum(x['date_changed'] for x in result['quotes']),business_products_added=len(result['new_business_product_ids']),backup=backup.name,beijing_tongrentang_data_unchanged=True,contract_invoice_receipt_order_amounts_dates_statuses_unchanged=True)
|
(P/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf8')
|
print(json.dumps({k:v for k,v in result.items() if k not in ['quotes','new_business_product_ids','new_permission_ids']},ensure_ascii=False))
|
except Exception:c.rollback();raise
|
finally:
|
with c.cursor() as q:q.execute('SELECT RELEASE_LOCK(%s)',('quotation-opportunities-20261014',))
|
c.close()
|