"""Correct sales order grouping, without duplicating contracts, invoices or payments.""" import sys,json,copy from pathlib import Path from decimal import Decimal,ROUND_HALF_UP P=Path(__file__).parent sys.path.insert(0,str(P.parent/'iqc-import-20261012/deps')) import pymysql D=Decimal money=lambda v:D(v).quantize(D('.01'),rounding=ROUND_HALF_UP) plan=json.loads((P/'import-plan.json').read_text(encoding='utf8')) old=json.loads((P/'import-result.json').read_text(encoding='utf8')) assert not (P/'source-order-regroup-result.json').exists(),'Already corrected; verify instead of rerunning.' 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={'groups':[],'orders':[],'rule':'相同原单据号一个订单;合同和合并开票保留;正合并金额内的退货原单独立保留;负合并金额两组仍排除'} try: with c.cursor() as q: q.execute('SELECT GET_LOCK(%s,10) locked,NOW() now,DATABASE() db',('sales-ledger-202609',));state=q.fetchone();assert state['locked']==1 and state['db']=='mom-hm';now=state['now'] backup={'state':state,'tables':{}} protected=['crm_sale_quotation','crm_sale_quotation_product','crm_contract_product','crm_invoice','crm_receivable','crm_receivable_plan','crm_permission','mdm_item','mes_md_item','crm_customer'] for t in ['erp_sale_order','erp_sale_order_items','crm_contract']+protected: q.execute('SELECT * FROM `'+t+'` ORDER BY id');backup['tables'][t]=q.fetchall() (P/'source-order-regroup-before.json').write_text(json.dumps(backup,ensure_ascii=False,default=str,indent=2),encoding='utf8') orders={x['id']:x for x in backup['tables']['erp_sale_order']};items={x['id']:x for x in backup['tables']['erp_sale_order_items']};contracts={x['id']:x for x in backup['tables']['crm_contract']} original_ids={g['order_id'] for g in old['groups']};all_source=set() for g in plan['groups']: assert not (all_source & set(g['source_numbers'])),'A source number crosses invoice groups; needs global consolidation.' all_source.update(g['source_numbers']) conflicts=[x for x in orders.values() if x['no'] in all_source and x['id'] not in original_ids];assert not conflicts,[(x['id'],x['no']) for x in conflicts] for g,prior in zip(plan['groups'],old['groups']): original=orders[prior['order_id']];contract=contracts[prior['contract_id']] assert original['no']=='XSDD-'+g['key'] and original['total_price']==D(g['gross']) and original['out_count']==0 and original['production_status']==0 assert contract['order_id']==original['id'] and contract['total_price']==D(g['gross']) for a,l in zip(g['lines'],prior['lines']): line=items[l['order_item_id']];assert line['order_id']==original['id'] and line['count']==D(a['quantity']) and line['total_price']==D(a['net']) and line['tax_price']==D(a['tax']) and line['out_count']==0 and not line['mps_id'] buckets=[] for no in g['source_numbers']: ls=[(a,l) for a,l in zip(g['lines'],prior['lines']) if a['source_no']==no] buckets.append({'source_no':no,'lines':ls,'gross':money(sum(D(a['gross_raw']) for a,l in ls))}) # Preserve the original invoice-group amount; document the one-cent allocation explicitly. delta=D(g['gross'])-sum(b['gross'] for b in buckets) recipient=next(b for b in reversed(buckets) if b['gross']>0);recipient['gross']+=delta changed=[] for i,b in enumerate(buckets): no=b['source_no'];net=sum(D(a['net']) for a,l in b['lines']);tax=sum(D(a['tax']) for a,l in b['lines']);count=sum(D(a['quantity']) for a,l in b['lines']);gross=b['gross'] marker='退货原单,负数量负金额保留;' if gross<0 else '' remark=f'九月台账{g["key"]};原单据号{no};同号明细归同一订单;{marker}关联合并合同{contract["no"]},开票组含税合计{g["gross"]}元;本单净税额与含税金额尾差{net+tax-gross}元;合并组分配尾差{delta if b is recipient else 0}元。' fields={'no':no,'total_count':count,'total_price':gross,'total_product_price':net,'total_tax_price':tax,'discount_price':net+tax-gross,'remark':remark[:255],'updater':'291','update_time':now} if i==0: oid=original['id'];q.execute('UPDATE erp_sale_order SET '+','.join('`'+k+'`=%s' for k in fields)+' WHERE id=%s',[*fields.values(),oid]) else: fields={**original,**fields};fields.pop('id');fields['creator']='291';fields['create_time']=now q.execute('INSERT INTO erp_sale_order ('+','.join('`'+k+'`' for k in fields)+') VALUES ('+','.join(['%s']*len(fields))+')',list(fields.values()));oid=q.lastrowid ids=[l['order_item_id'] for a,l in b['lines']] q.execute('UPDATE erp_sale_order_items SET order_id=%s,updater=%s,update_time=%s WHERE id IN ('+','.join(['%s']*len(ids))+')',[oid,'291',now,*ids]) entry={'source_no':no,'order_id':oid,'contract_id':contract['id'],'customer_id':original['customer_id'],'gross':str(gross),'net':str(net),'tax':str(tax),'count':str(count),'order_item_ids':[l['order_item_id'] for a,l in b['lines']],'source_rows':[a['row'] for a,l in b['lines']]};changed.append(entry);result['orders'].append(entry) q.execute('UPDATE crm_contract SET order_no=%s,updater=%s,update_time=%s WHERE id=%s',(buckets[0]['source_no'],'291',now,contract['id'])) result['groups'].append({'key':g['key'],'primary_order_id':original['id'],'contract_id':contract['id'],'orders':changed,'invoice_id':prior['invoice_id'],'receivable_id':prior['receivable_id'],'plan_id':prior['plan_id']}) # Validate all balances and line partitioning before commit. assert len(result['orders'])==24 and len({o['source_no'] for o in result['orders']})==24 seen=set();total=D(0) for o in result['orders']: q.execute('SELECT * FROM erp_sale_order WHERE id=%s',(o['order_id'],));actual=q.fetchone() assert actual['no']==o['source_no'] and actual['total_price']==D(o['gross']) and actual['total_product_price']+actual['total_tax_price']-actual['discount_price']==actual['total_price'] q.execute('SELECT * FROM erp_sale_order_items WHERE order_id=%s AND deleted=0',(o['order_id'],));ls=q.fetchall();ids={x['id'] for x in ls};assert ids==set(o['order_item_ids']) and not ids & seen;seen|=ids assert sum(x['count'] for x in ls)==actual['total_count'] and sum(x['total_price'] for x in ls)==actual['total_product_price'] and sum(x['tax_price'] for x in ls)==actual['total_tax_price'] total+=actual['total_price'] assert len(seen)==76 and total==D(plan['totals']['gross']) for group,g in zip(result['groups'],plan['groups']):assert sum(D(x['gross']) for x in group['orders'])==D(g['gross']) for t in protected: q.execute('SELECT * FROM `'+t+'` ORDER BY id');assert q.fetchall()==backup['tables'][t],('Unexpected changes',t) c.commit();result.update(committed=True,at=str(now),order_count=24,line_count=76,total=str(total)) (P/'source-order-regroup-result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),encoding='utf8') print(json.dumps({'committed':True,'orders':24,'lines':76,'total':str(total),'split_orders':[o for o in result['orders'] if o['source_no'] in ['26000393','26000406','26000369']]},ensure_ascii=False)) except Exception:c.rollback();raise finally: with c.cursor() as q:q.execute('SELECT RELEASE_LOCK(%s)',('sales-ledger-202609',)) c.close()