194 lines
14 KiB
Python
194 lines
14 KiB
Python
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]
|