gaoluyang
11 小时以前 e40a608ef9bf51db294d8eefada6a8e2e80cebca
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
"""Independent graph/timestamp checks against original and proposed full rows."""
import json
from pathlib import Path
from datetime import datetime,date
from decimal import Decimal
P=Path(__file__).parent
def validate(before,after,plan,now):
 changes={(x['table'],x['id']):x['after'] for x in plan['changes']}
 ids=lambda t:{r['id']:r for r in after[t]}
 for t,rows in before.items():
  new=ids(t);assert set(new)=={r['id'] for r in rows},('Rows added or removed',t)
  for old in rows:
   proposed={**old,**changes.get((t,old['id']),{})}
   # Database datetime objects vs serializable saved plan strings.
   canon=lambda x:json.loads(json.dumps(x,default=str))
   assert canon(new[old['id']])==canon(proposed),('Unexpected change',t,old['id'])
 hol={date(2026,9,d) for d in range(25,28)}|{date(2026,10,d) for d in range(1,8)}
 dt=lambda x:datetime.fromisoformat(str(x))
 for change in plan['changes']:
  for k,v in change['after'].items():
   if v is None:continue
   d=dt(v);assert d<=now and d.weekday()<5 and d.date() not in hol,(change['table'],change['id'],k,d)
  row=ids(change['table'])[change['id']];assert dt(row['create_time'])<=dt(row['update_time'])
 selected={x['order_id'] for x in plan['chains']}
 actual={r['order_id'] for r in after['erp_sale_order_items'] if r['need_production'] in (1,True,b'\x01',"b'\\x01'")}
 assert selected==actual
 for chain in plan['chains']:
  oid=chain['order_id'];order=ids('erp_sale_order')[oid];start=dt(order['order_time']);out=dt(chain['outbound_time']);production=dt(order['production_finish_time']);q=Decimal(str(order['total_count']))
  n=len(chain['workdays']);assert (1<=n<=2 if q<=30 else 3<=n<=5 if q<=1000 else n>=6 and (out-start).days>=7)
  assert start<production<out<=now
  for mid in chain['mps_ids']:
   m=ids('mes_pro_mps')[mid];assert m['sale_order_id']==oid and start<=dt(m['create_time'])<=dt(m['issue_date'])<=production
   assert dt(m['finish_date'])==production
  for wid in chain['work_order_ids']:
   w=ids('mes_pro_work_order')[wid];assert start<=dt(w['create_time'])<=dt(w['finish_date'])==production
   ts=sorted([r for r in after['mes_pro_task'] if r['work_order_id']==wid],key=lambda r:dt(r['start_time']))
   assert ts
   for i,t in enumerate(ts):
    assert dt(w['create_time'])<=dt(t['start_time'])<dt(t['end_time'])<=dt(t['finish_date'])<=production
    if i:assert dt(ts[i-1]['finish_date'])<=dt(t['start_time'])
    for f in after['mes_pro_feedback']:
     if f['task_id']==t['id']:assert dt(t['start_time'])<=dt(f['feedback_time'])<=dt(t['finish_date'])
  for sale in after['mes_wm_product_sales']:
   if sale['sale_order_id']!=oid:continue
   assert dt(sale['sales_date'])==out
   for line in after['mes_wm_product_sales_line']:
    if line['sales_id']!=sale['id'] or not line['oqc_id']:continue
    qc=ids('mes_qc_oqc')[line['oqc_id']];assert production<=dt(qc['inspect_date'])<out and dt(qc['out_date'])==out
 for qc in after['mes_qc_ipqc']:
  if ('mes_qc_ipqc',qc['id']) not in changes:continue
  task=ids('mes_pro_task')[qc['task_id']];assert dt(task['end_time'])<=dt(qc['inspect_date'])<=dt(task['finish_date'])
 for receipt in after['mes_wm_item_receipt']:
  if ('mes_wm_item_receipt',receipt['id']) not in changes:continue
  iqc=ids('mes_qc_iqc')[receipt['iqc_id']];notice=ids('mes_wm_arrival_notice')[receipt['notice_id']]
  assert dt(notice['arrival_date'])<=dt(iqc['inspect_date'])<=dt(receipt['receipt_date'])
 for produce in after['mes_wm_product_produce']:
  if ('mes_wm_product_produce',produce['id']) not in changes:continue
  if produce['feedback_id']:
   f=ids('mes_pro_feedback')[produce['feedback_id']];assert dt(f['feedback_time'])<=dt(produce['produce_date'])
 # Shared inventory and compensation receipts must not acquire new timing deficits.
 affected={r['material_stock_id'] for r in after['mes_wm_transaction'] if ('mes_wm_transaction',r['id']) in changes}
 def minimum(rows,sid):
  balance=Decimal(0);smallest=Decimal(0)
  for r in sorted([x for x in rows if x['material_stock_id']==sid],key=lambda x:(dt(x['transaction_time']),x['id'])):
   balance+=Decimal(str(r['quantity']));smallest=min(smallest,balance)
  return smallest
 for sid in affected:assert minimum(after['mes_wm_transaction'],sid)>=minimum(before['mes_wm_transaction'],sid),('New chronological stock deficit',sid)
 return {'passed':True,'orders':len(selected),'changed_rows':len(changes),'changed_tables':len({t for t,id in changes}),'cycles':[{'no':c['order_no'],'quantity':c['quantity'],'workdays':c['cycle_workdays'],'start':c['order_time'],'end':c['outbound_time']} for c in plan['chains']],'outside_scope_unchanged':True,'quantities_money_states_results_unchanged':True,'no_new_inventory_timing_deficits':True}
if __name__=='__main__':
 s=json.loads((P/'snapshot.json').read_text(encoding='utf8'));p=json.loads((P/'plan.json').read_text(encoding='utf8'));before={t:x['rows'] for t,x in s['tables'].items()};changes={(x['table'],x['id']):x['after'] for x in p['changes']}
 after={t:[{**r,**changes.get((t,r['id']),{})} for r in rs] for t,rs in before.items()}
 print(json.dumps(validate(before,after,p,datetime.fromisoformat(s['state']['now'])),ensure_ascii=False))