from datetime import date from decimal import Decimal as D import pytest from test_pl_quota_usage import case, run, request, _second_pair def test_supplement_preserves_original_order_and_identity(case): c=case; _second_pair(c) sources=request()['sources']; sources[0]['quantity']='30' run(c,sources=sources,purchase_cert_ids=['p','p2'],transport_cert_ids=['t','t2']) ids={i.id for i in c.db.query(c.m.PlPlanCertUsageItem).all()} for model in (c.m.PlPurchaseCert,c.m.PlTransportCert): c.db.get(model,'p' if model==c.m.PlPurchaseCert else 't').total_qty=100 c.db.flush() for index,(actual,expected) in enumerate([('35',[25,10]),('22',[20,2]),('36',[26,10])]): kw=dict(operation_id='s'+str(index),action='settle' if index==0 else 'actual_adjust',sources=sources,allow_supplement=True,actual_by_source={'load:sp':actual},reason='更正',source_note='磅单') result=run(c,**kw) assert run(c,**kw)==result assert [c.db.get(c.m.PlPurchaseCert,p).used_qty for p in ['p','p2']]==expected assert {i.id for i in c.db.query(c.m.PlPlanCertUsageItem).all()}==ids assert sorted(i.allocated_source_qty for i in c.db.query(c.m.PlPlanCertUsageItem).all())==[10,10,20,20] def test_expired_original_new_supplement_and_rollback(case): c=case; _second_pair(c) c.db.get(c.m.PlPurchaseCert,'p').total_qty=100; c.db.get(c.m.PlTransportCert,'t').total_qty=100; c.db.flush() run(c) c.db.get(c.m.PlPurchaseCert,'p').valid_to=date(2000,1,1); c.db.get(c.m.PlTransportCert,'t').valid_to=date(2000,1,1); c.db.flush() kw=dict(reason='超量补证',source_note='磅单',operation_id='supp',action='settle',allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t2'],actual_by_source={'load:sp':'35'}) c.db.get(c.m.PlTransportCert,'t2').total_qty=5; c.db.flush() with pytest.raises(ValueError): run(c,**kw) assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29 assert c.db.query(c.m.PlPlanCertUsage).count()==2 c.db.get(c.m.PlTransportCert,'t2').total_qty=100; c.db.flush() run(c,**kw) assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==29 assert c.db.get(c.m.PlPurchaseCert,'p2').used_qty==6 run(c,operation_id='down',action='actual_adjust',actual_by_source={'load:sp':'18'},reason='更正',source_note='磅单') assert c.db.get(c.m.PlPurchaseCert,'p2').used_qty==0 @pytest.mark.parametrize('failure',['expiry','vehicle','scope','opening','once','tenant','reference','reason','evidence']) def test_supplement_current_qualification_is_atomic(case,failure): c=case; _second_pair(c) c.db.get(c.m.PlPurchaseCert,'p').total_qty=100; c.db.get(c.m.PlTransportCert,'t').total_qty=100; c.db.flush() run(c) kw=dict(operation_id='supp',action='settle',allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t2'],actual_by_source={'load:sp':'35'},reason='补量',source_note='磅单') if failure=='expiry': c.db.get(c.m.PlPurchaseCert,'p2').valid_to=date(2000,1,1) if failure=='vehicle': kw['vehicle_no']='wrong' if failure=='scope': c.db.get(c.m.PlContractCertProduct,'sp2').scope_status='inactive' if failure=='opening': c.db.get(c.m.PlCertQtyLog,'openingp2').del_flag=1 if failure=='once': c.db.get(c.m.PlPurchaseCert,'p2').valid_use_text='一次有效'; c.db.get(c.m.PlPurchaseCert,'p2').opening_single_use_consumed=1 if failure=='tenant': c.db.get(c.m.PlPurchaseCert,'p2').tenant_no='foreign' if failure=='reference': c.db.get(c.m.PlTransportCertPurchaseRef,'ref2').purchase_cert_id='p' if failure=='reason': kw['reason']='' if failure=='evidence': kw['source_note']='' c.db.flush() from fastapi import HTTPException with pytest.raises((ValueError,HTTPException)): run(c,**kw) assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==29 assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==0 assert c.db.query(c.m.PlCertOperation).filter_by(operation_id='supp').count()==0 assert c.db.query(c.m.PlPlanCertUsage).count()==2 def test_supplement_foreign_unit_requires_fresh_manual_and_keeps_audit(case): c=case; c.db.get(c.m.PlTransportCert,'t').quantity_unit='kg'; c.db.flush() def manual(sq,pq): return [dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity=sq,permit_quantity=pq,note='人工核实')] run(c,manual_allocations=manual('29','58')) kw=dict(operation_id='supp',action='settle',allow_supplement=True,actual_by_source={'load:sp':'35'},reason='补量',source_note='磅单',manual_actuals=manual('29','55')) with pytest.raises(ValueError): run(c,**kw) result=run(c,**kw,manual_allocations=manual('6','10')) assert result['supplementCerts']==[{'certType':'purchase','certId':'p'},{'certType':'transport','certId':'t'}] assert c.db.get(c.m.PlTransportCert,'t').used_qty==65 log=c.db.query(c.m.PlCertQtyLog).filter_by(operation_id='supp',cert_type='transport').one() assert log.event_snapshot['supplement']['extraActualBySource']=={'load:sp':'6'} assert log.event_snapshot['supplement']['allocations'][0]['permitQuantity']=='10' with pytest.raises(ValueError): run(c,operation_id='again',action='actual_adjust',allow_supplement=True,actual_by_source={'load:sp':'36'},reason='更正',source_note='磅单') assert c.db.get(c.m.PlTransportCert,'t').used_qty==65 def test_supplement_does_not_borrow_other_source_reservation(case): c=case first=request()['sources'][0]; first['quantity']='20' second={**first,'source_stop_product_id':'sp2','quantity':'10'} c.db.get(c.m.PlPurchaseCert,'p').total_qty=30; c.db.get(c.m.PlTransportCert,'t').total_qty=30; c.db.flush() run(c,sources=[first,second]) # 来源二未使用的10不能扩成来源一的原始容量,必须先经过当前证件资格校验。 c.db.get(c.m.PlPurchaseCert,'p').valid_to=date(2000,1,1); c.db.flush() with pytest.raises(ValueError,match='有效日期'): run(c,operation_id='supp',action='settle',sources=[first,second],allow_supplement=True,actual_by_source={'load:sp':'25','load:sp2':'0'},reason='补量',source_note='磅单') assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty==30 def test_once_supplement_same_plan_keeps_single_qualification(case): c=case; c.db.get(c.m.PlPurchaseCert,'p').valid_use_text='一次有效'; c.db.flush() run(c) run(c,operation_id='supp',action='settle',allow_supplement=True,actual_by_source={'load:sp':'35'},reason='补量',source_note='磅单') assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==35 with pytest.raises(ValueError,match='一次有效'): run(c,operation_id='other',plan_id='other') def test_permit_only_manual_excess_requires_current_validation(case): c=case; c.db.get(c.m.PlTransportCert,'t').quantity_unit='kg'; c.db.flush() def manual(q): return [dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity='29',permit_quantity=q,note='重新称重')] run(c,manual_allocations=manual('58')) kw=dict(operation_id='permit-extra',action='settle',allow_supplement=True,actual_by_source={'load:sp':'29'},manual_actuals=manual('65'),purchase_cert_ids=[],transport_cert_ids=['t'],reason='重新核实',source_note='凭证') # 原购证本次无新增额度,历史过期仍可结转原量。 c.db.get(c.m.PlPurchaseCert,'p').valid_to=date(2000,1,1); c.db.flush() result=run(c,**kw) assert c.db.get(c.m.PlTransportCert,'t').used_qty==65 assert c.db.get(c.m.PlPurchaseCert,'p').used_qty==29 assert result['supplementCerts']==[{'certType':'transport','certId':'t'}] item=c.db.query(c.m.PlPlanCertUsageItem).filter_by(source_stop_product_id='sp',source_unit='吨').all() assert sorted(i.planned_cert_qty for i in item)==[29,58] log=c.db.query(c.m.PlCertQtyLog).filter_by(operation_id='permit-extra',cert_type='transport').one() assert log.event_snapshot['supplement']['permitOnlyAllocations'][0]['permitQuantity']=='7' c.db.get(c.m.PlTransportCert,'t').valid_to=date(2000,1,1); c.db.flush() with pytest.raises(ValueError,match='有效日期'): run(c,**{**kw,'operation_id':'fail','action':'actual_adjust','manual_actuals':manual('66')}) assert c.db.get(c.m.PlTransportCert,'t').used_qty==65 def test_explicit_foreign_unit_split_can_supplement_new_cert_without_source_growth(case): c=case; _second_pair(c) c.db.get(c.m.PlPurchaseCert,'p').total_qty=100 for pid in ('t','t2'): c.db.get(c.m.PlTransportCert,pid).quantity_unit='kg'; c.db.get(c.m.PlTransportCert,pid).total_qty=100 c.db.flush() def manual(pid,sq,pq): return dict(permit_kind='transport',source_id='load:sp',permit_id=pid,source_quantity=sq,permit_quantity=pq,note='明确人工拆分') run(c,manual_allocations=[manual('t','29','58')]) run(c,operation_id='split',action='settle',allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t2'],actual_by_source={'load:sp':'29'},manual_actuals=[manual('t','24','48')],manual_allocations=[manual('t2','5','11')],reason='更正分配',source_note='凭证') assert [c.db.get(c.m.PlPurchaseCert,pid).used_qty for pid in ('p','p2')]==[24,5] assert [c.db.get(c.m.PlTransportCert,pid).used_qty for pid in ('t','t2')]==[48,11] assert sum(i.actual_allocated_source_qty for i in c.db.query(c.m.PlPlanCertUsageItem).all())==58 @pytest.mark.parametrize('failure',['not_selected','capacity','scope','no_opt_in']) def test_permit_only_excess_failures_leave_original_reservation(case,failure): c=case; c.db.get(c.m.PlTransportCert,'t').quantity_unit='kg'; c.db.flush() def manual(q): return [dict(permit_kind='transport',source_id='load:sp',permit_id='t',source_quantity='29',permit_quantity=q,note='人工核实')] run(c,manual_allocations=manual('58')) kw=dict(operation_id='extra',action='settle',allow_supplement=True,actual_by_source={'load:sp':'29'},manual_actuals=manual('65'),reason='超量',source_note='凭证') if failure=='not_selected': kw['transport_cert_ids']=[] if failure=='capacity': c.db.get(c.m.PlTransportCert,'t').total_qty=60 if failure=='scope': c.db.get(c.m.PlContractCertProduct,'st').scope_status='inactive' if failure=='no_opt_in': kw['allow_supplement']=False c.db.flush() with pytest.raises(ValueError): run(c,**kw) assert c.db.get(c.m.PlTransportCert,'t').reserved_qty==58 assert c.db.get(c.m.PlTransportCert,'t').used_qty==0 def test_same_unit_manual_supplement_cannot_bypass_original_order(case): c=case; _second_pair(c) sources=request()['sources']; sources[0]['quantity']='30' run(c,sources=sources,purchase_cert_ids=['p','p2'],transport_cert_ids=['t','t2']) manual=[dict(permit_kind=kind,source_id='load:sp',permit_id=pid,source_quantity='18',permit_quantity='18',note='试图重新安排') for kind,pid in [('purchase','p2'),('transport','t2')]] with pytest.raises(ValueError,match='异单位'): run(c,operation_id='invalid-split',action='settle',sources=sources,allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t2'],actual_by_source={'load:sp':'18'},manual_allocations=manual,reason='调整',source_note='凭证') assert [c.db.get(c.m.PlPurchaseCert,pid).reserved_qty for pid in ('p','p2')]==[20,10] run(c,operation_id='normal',action='settle',sources=sources,actual_by_source={'load:sp':'18'}) assert [c.db.get(c.m.PlPurchaseCert,pid).used_qty for pid in ('p','p2')]==[18,0] def test_historical_zero_capacity_foreign_cert_cannot_enable_same_unit_reallocation(case): c=case; _second_pair(c) sources=request()['sources']; sources[0]['quantity']='30' run(c,sources=sources,purchase_cert_ids=['p','p2'],transport_cert_ids=['t','t2']) original=c.db.get(c.m.PlTransportCert,'t2') values={col.name:getattr(original,col.name) for col in c.m.PlTransportCert.__table__.columns} values.update(id='t3',cert_no='t3',quantity_unit='kg',reserved_qty=0,used_qty=0) c.db.add(c.m.PlTransportCert(**values)) c.db.add(c.m.PlContractCertLink(id='lt3',contract_id='c',contract_no_snapshot='c',cert_type='transport',cert_id='t3',tenant_no='t')) c.db.add(c.m.PlContractCertProduct(id='st3',contract_cert_link_id='lt3',contract_product_id='cp',tenant_no='t')) c.db.add(c.m.PlTransportCertPurchaseRef(id='ref3',transport_cert_id='t3',reference_type='purchase_permit',reference_no='p2',purchase_cert_id='p2',tenant_no='t')) original_log=c.db.get(c.m.PlCertQtyLog,'openingt2') log={col.name:getattr(original_log,col.name) for col in c.m.PlCertQtyLog.__table__.columns} log.update(id='openingt3',operation_id='openingt3',cert_id='t3',quantity_unit='kg') c.db.add(c.m.PlCertQtyLog(**log)); c.db.flush() def manual(kind,pid,sq,pq): return dict(permit_kind=kind,source_id='load:sp',permit_id=pid,source_quantity=str(sq),permit_quantity=str(pq),note='人工确认') run(c,operation_id='first-extra',action='settle',sources=sources,allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t3'],actual_by_source={'load:sp':'35'},manual_allocations=[manual('transport','t3',5,10)],reason='补量',source_note='凭证') # 增加合法购证引用,确保拒绝来自原顺序校验,而非最终双证不匹配。 c.db.add(c.m.PlTransportCertPurchaseRef(id='ref2-p',transport_cert_id='t2',reference_type='purchase_permit',reference_no='p',purchase_cert_id='p',tenant_no='t')); c.db.flush() with pytest.raises(ValueError,match='顺序'): run(c,operation_id='invalid-manual-adjust',action='actual_adjust',sources=sources,actual_by_source={'load:sp':'18'},manual_actuals=[manual('transport','t',8,8),manual('transport','t2',10,10),manual('transport','t3',0,0)],reason='调整',source_note='凭证') assert [c.db.get(c.m.PlTransportCert,pid).used_qty for pid in ('t','t2','t3')]==[20,10,10] with pytest.raises(ValueError,match='异单位'): run(c,operation_id='invalid-adjust',action='actual_adjust',sources=sources,allow_supplement=True,purchase_cert_ids=['p2'],transport_cert_ids=['t2','t3'],actual_by_source={'load:sp':'18'},manual_actuals=[manual('transport',pid,0,0) for pid in ('t','t2','t3')],manual_allocations=[manual('purchase','p2',18,18),manual('transport','t2',18,18),manual('transport','t3',0,0)],reason='调整',source_note='凭证') assert [c.db.get(c.m.PlPurchaseCert,pid).used_qty for pid in ('p','p2')]==[20,15] run(c,operation_id='valid-adjust',action='actual_adjust',sources=sources,actual_by_source={'load:sp':'18'},manual_actuals=[manual('transport','t',18,18),manual('transport','t2',0,0),manual('transport','t3',0,0)],reason='调整',source_note='凭证') assert [c.db.get(c.m.PlPurchaseCert,pid).used_qty for pid in ('p','p2')]==[18,0]