440 lines
24 KiB
Python
440 lines
24 KiB
Python
import pytest
|
|
from fastapi import HTTPException
|
|
from types import SimpleNamespace
|
|
|
|
|
|
def test_generic_status_cannot_bypass_quota():
|
|
from app.services.pl_quota_business import validate_generic_status
|
|
with pytest.raises(HTTPException):
|
|
validate_generic_status(SimpleNamespace(transport_status='arranged', model_fields_set={'transport_status'}))
|
|
|
|
|
|
def test_business_hash_includes_non_quota_changes():
|
|
from app.services.pl_quota_business import business_hash
|
|
assert business_hash('arrange', 'p', {'freight': 1}) != business_hash('arrange', 'p', {'freight': 2})
|
|
|
|
|
|
def test_deadlock_retries_entire_transaction():
|
|
from app.services.pl_quota_business import retry_transaction
|
|
from sqlalchemy.exc import OperationalError
|
|
calls=[]
|
|
db=SimpleNamespace(rollback=lambda:calls.append('rollback'))
|
|
def work():
|
|
calls.append('work')
|
|
if calls.count('work') == 1:
|
|
raise OperationalError('', {}, Exception(1213, 'deadlock'))
|
|
return 42
|
|
assert retry_transaction(db, work) == 42
|
|
assert calls == ['work','rollback','work']
|
|
|
|
|
|
def test_arrange_schema_retains_quota_configuration():
|
|
from app.schemas.pl.transport import TransportArrange
|
|
value=TransportArrange(vehicleId='v', operationId='op', quotaConfiguration={'purchaseCertIds':['p']})
|
|
assert value.operation_id == 'op'
|
|
assert value.quota_configuration.purchase_cert_ids == ['p']
|
|
|
|
|
|
def test_ready_route_is_retired():
|
|
from app.api.pl.plan import patch_plan_hazard_cert_ready
|
|
with pytest.raises(HTTPException) as error:
|
|
patch_plan_hazard_cert_ready('p', None, None, None)
|
|
assert error.value.status_code == 410
|
|
|
|
from test_pl_quota_usage import case
|
|
|
|
@pytest.fixture
|
|
def business(case, monkeypatch):
|
|
from app.models import pl_models as m
|
|
from app.services import pl_quota_planning
|
|
from app.services.pl_quota_sources import extract_sources
|
|
from app.services.pl_quota_planning import plan_fingerprint
|
|
from app.schemas.pl.transport import TransportArrange
|
|
for model in [m.PlPlanProduct,m.PlPlanLoad,m.PlPlanLoadProduct,m.PlPlanDelivery,m.PlPlanDeliveryProduct]:
|
|
model.__table__.create(case.db.bind)
|
|
plan=case.db.get(m.PlPlan,'plan'); plan.license_plate='CAR'
|
|
case.db.add(m.PlPlanProduct(id='pp',plan_id='plan',product_id='product',product_no='SKU',product_name='酸',is_toxic=1,tenant_no='t'))
|
|
case.db.add(m.PlPlanLoad(id='load',plan_id='plan',tenant_no='t'))
|
|
case.db.add(m.PlPlanLoadProduct(id='sp',load_id='load',plan_id='plan',plan_product_id='pp',product_id='product',product_no='SKU',product_name='酸',plan_qty=29,unit='吨',contract_id='c',contract_product_id='cp',tenant_no='t',inbound_qty=0))
|
|
case.db.commit()
|
|
monkeypatch.setattr(pl_quota_planning,'plan_execution_lock_reason', lambda *a: None)
|
|
sources,_=extract_sources(case.db,case.user,'plan', measurement_version='per_stop_v2')
|
|
fingerprint=plan_fingerprint(sources)
|
|
payload=TransportArrange(vehicleId='v',operationId='business',quotaConfiguration={'purchaseCertIds':['p'],'transportCertIds':['t'],'conditions':[
|
|
{'certType':kind,'certId':cid,'certRevision':1,'sourceFingerprint':fingerprint,'partiesConfirmed':True,'routeConfirmed':True,'note':'已核对'}
|
|
for kind,cid in [('purchase','p'),('transport','t')]]})
|
|
return SimpleNamespace(**case.__dict__,plan=plan,payload=payload,models=m)
|
|
|
|
|
|
def test_real_source_reserve_zero_settlement(business):
|
|
from app.services.pl_quota_business import apply_plan_quota,settle_plan_quota
|
|
c=business
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 29
|
|
assert settle_plan_quota(c.db,c.user,c.plan,c.payload)
|
|
cert=c.db.get(c.m.PlPurchaseCert,'p')
|
|
assert cert.reserved_qty == 0 and cert.used_qty == 0
|
|
assert all(u.confirmation_snapshot['executed'] for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
|
|
def test_overage_preserves_actual_and_reserved(business):
|
|
from app.services.pl_quota_business import apply_plan_quota,settle_plan_quota
|
|
c=business
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
row=c.db.get(c.models.PlPlanLoadProduct,'sp');row.inbound_qty=101
|
|
assert not settle_plan_quota(c.db,c.user,c.plan,c.payload)
|
|
c.db.commit()
|
|
assert c.db.get(c.models.PlPlanLoadProduct,'sp').inbound_qty == 101
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 29
|
|
assert c.plan.archive_status == 'exception'
|
|
assert all(u.usage_status == 'exception' for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
|
|
def test_business_replay_skips_side_effect_and_changed_freight_conflicts(business):
|
|
from app.services.pl_quota_business import atomic_plan_operation
|
|
c=business;calls=[]
|
|
@atomic_plan_operation('test')
|
|
def operation(plan_id,payload,db,user):
|
|
calls.append(1)
|
|
db.get(c.models.PlPlan,plan_id).remark='business-write'
|
|
db.flush()
|
|
return {'ok':True}
|
|
assert operation('plan',c.payload,c.db,c.user) == {'ok':True}
|
|
assert operation('plan',c.payload,c.db,c.user) == {'ok':True}
|
|
assert calls == [1]
|
|
c.payload.freight_amount=8
|
|
with pytest.raises(HTTPException): operation('plan',c.payload,c.db,c.user)
|
|
assert calls == [1]
|
|
|
|
|
|
def test_failed_adjustment_retains_settled_state_for_retry(business):
|
|
from app.services.pl_quota_business import apply_plan_quota,settle_plan_quota
|
|
c=business
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
assert settle_plan_quota(c.db,c.user,c.plan,c.payload)
|
|
c.plan.archive_status='archived'
|
|
c.db.get(c.models.PlPlanLoadProduct,'sp').inbound_qty=101
|
|
assert not settle_plan_quota(c.db,c.user,c.plan,c.payload,adjustment=True)
|
|
assert all(u.usage_status == 'settled' for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
assert c.plan.archive_status == 'archived'
|
|
|
|
|
|
def test_generic_form_cannot_reset_formal_status():
|
|
from app.services.pl_quota_business import validate_generic_status
|
|
with pytest.raises(HTTPException):
|
|
validate_generic_status(SimpleNamespace(model_fields_set={'transport_status'},transport_status='pending'),
|
|
SimpleNamespace(transport_status='arranged'))
|
|
|
|
|
|
def test_stop_actual_zero_overwrites_old_aggregate(business):
|
|
from app.services.pl_plan_archive_stop_service import sync_plan_products_from_stop_archive
|
|
c=business
|
|
product=c.db.get(c.models.PlPlanProduct,'pp');product.actual_qty=9
|
|
c.db.add(c.models.PlPlanDelivery(id='delivery',plan_id='plan',tenant_no='t'))
|
|
c.db.add(c.models.PlPlanDeliveryProduct(id='dp',delivery_id='delivery',plan_id='plan',plan_product_id='pp',product_id='product',product_no='SKU',product_name='酸',plan_qty=29,outbound_qty=0,tenant_no='t'))
|
|
c.db.flush()
|
|
sync_plan_products_from_stop_archive(c.db,tenant_no='t',plan_id='plan',user_id='u')
|
|
assert product.actual_qty == 0
|
|
|
|
|
|
def test_legacy_ready_services_cannot_authorize_transport():
|
|
from app.services.pl_plan_service import set_hazard_cert_ready, validate_hazard_cert_ready_for_transport, maybe_auto_set_hazard_cert_ready
|
|
plan=SimpleNamespace(hazard_cert_ready=1)
|
|
with pytest.raises(HTTPException) as error:
|
|
validate_hazard_cert_ready_for_transport(None,'t',plan,[])
|
|
assert error.value.status_code == 410
|
|
with pytest.raises(HTTPException):
|
|
set_hazard_cert_ready(None,tenant_no='t',plan_id='p',ready=True,user_id='u')
|
|
plan.hazard_cert_ready=0
|
|
maybe_auto_set_hazard_cert_ready(None,tenant_no='t',plan=plan,products=[],user_id='u')
|
|
assert plan.hazard_cert_ready == 0
|
|
|
|
|
|
def test_business_failure_rolls_back_reservation_and_business_row(business):
|
|
from app.services.pl_quota_business import atomic_plan_operation, apply_plan_quota
|
|
c=business
|
|
@atomic_plan_operation('failed-business')
|
|
def operation(plan_id,payload,db,user):
|
|
plan=db.get(c.models.PlPlan,plan_id)
|
|
apply_plan_quota(db,user,plan,'reserve',payload)
|
|
plan.remark='must roll back'
|
|
raise HTTPException(409,'later downstream failure')
|
|
with pytest.raises(HTTPException): operation('plan',c.payload,c.db,c.user)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
assert c.db.get(c.models.PlPlan,'plan').remark is None
|
|
assert c.db.query(c.m.PlCertOperation).count() == 0
|
|
|
|
|
|
def test_force_void_preserves_executed_guard_after_evidence_revoked(business, monkeypatch):
|
|
from app.api.pl import plan as api
|
|
from app.schemas.pl.cancel_reason import CancelReasonBody
|
|
from app.services.pl_quota_business import apply_plan_quota
|
|
c=business
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
monkeypatch.setattr(api,'_load_force_void_plan',lambda *a:('t',c.plan,'executed'))
|
|
monkeypatch.setattr(api,'apply_force_void_downstream',lambda *a,**k:None)
|
|
api.force_void_plan.__wrapped__('plan',CancelReasonBody(cancelReason='停止执行'),c.db,c.user)
|
|
with pytest.raises(HTTPException):
|
|
apply_plan_quota(c.db,c.user,c.plan,'release')
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 29
|
|
assert all(u.confirmation_snapshot['executed'] for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
|
|
def test_force_status_cannot_reopen_cancelled_plan(monkeypatch):
|
|
from app.api.pl import plan as api
|
|
from app.schemas.pl.plan import PlanForceFollowStatusBody
|
|
from unittest.mock import MagicMock
|
|
plan=SimpleNamespace(follow_status='cancelled',archive_status='cancelled',transport_status='pending')
|
|
user=SimpleNamespace(role=api.SUPER_ADMIN_ROLE,id='u',to_tenant_user_dict=lambda:{})
|
|
monkeypatch.setattr(api,'get_tenant_no',lambda *a:'t')
|
|
monkeypatch.setattr(api,'_get_plan',lambda *a,**k:plan)
|
|
with pytest.raises(HTTPException):
|
|
api.force_plan_follow_status('p',PlanForceFollowStatusBody(followStatus='pending_arrange'),MagicMock(),user)
|
|
|
|
|
|
def test_successful_retry_clears_active_settlement_error(business):
|
|
from app.services.pl_quota_business import apply_plan_quota,settle_plan_quota
|
|
c=business
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
row=c.db.get(c.models.PlPlanLoadProduct,'sp');row.inbound_qty=101
|
|
assert not settle_plan_quota(c.db,c.user,c.plan,c.payload)
|
|
row.inbound_qty=0
|
|
c.payload.operation_id='retry-success'
|
|
assert settle_plan_quota(c.db,c.user,c.plan,c.payload)
|
|
assert all('settlementError' not in u.confirmation_snapshot for u in c.db.query(c.m.PlPlanCertUsage).all())
|
|
|
|
|
|
def test_final_approval_reserves_with_autoflush_disabled(business, monkeypatch):
|
|
from app.api.pl import transport as api
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
c=business
|
|
for model in [c.models.PlTransport,c.models.PlTransportApproval,c.models.PlVehicle,c.models.PlCert,c.models.PlVehiclePersonnel,c.models.PlCarrier,c.models.PlPersonnel]: model.__table__.create(c.db.bind)
|
|
c.db.add(c.models.PlVehicle(id='v',vehicle_type='self',license_plate='CAR',tenant_no='t'))
|
|
transport=c.models.PlTransport(id='tr',transport_no='TR',tenant_no='t',approval_status='pending',vehicle_id='v')
|
|
c.db.add(transport)
|
|
c.db.add(c.models.PlTransportApproval(id='approval',transport_id='tr',approver_id='u',approver_name='U',approval_order=1,tenant_no='t'))
|
|
c.plan.transport_id='tr';c.plan.cert_configuration=c.payload.quota_configuration.model_dump(mode='json',by_alias=True)
|
|
c.db.commit()
|
|
c.user.to_tenant_user_dict=lambda:{}
|
|
monkeypatch.setattr(api,'get_tenant_no',lambda *a:'t')
|
|
api.process_approval('approval',ApprovalRequest(action='approve',operationId='final-approve'),c.db,c.user)
|
|
assert transport.approval_status == 'approved'
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 29
|
|
assert c.plan.transport_status == 'arranged'
|
|
|
|
|
|
def test_business_evidence_cannot_reference_foreign_tenant(business):
|
|
from app.models.crm_models import SysOss
|
|
from app.services.pl_quota_business import apply_plan_quota
|
|
c=business
|
|
SysOss.__table__.create(c.db.bind)
|
|
c.db.add(SysOss(id='foreign',tenant_no='other',url='foreign.pdf',create_by='u',service='minio'))
|
|
c.db.commit()
|
|
c.payload.quota_evidence_urls='foreign.pdf'
|
|
with pytest.raises(HTTPException) as error:
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
assert error.value.status_code == 403
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
|
|
|
|
@pytest.fixture
|
|
def approval_case(business, monkeypatch):
|
|
from app.api.pl import transport as api
|
|
c=business
|
|
for model in [c.models.PlTransport,c.models.PlTransportApproval,c.models.PlVehicle,c.models.PlCert,c.models.PlVehiclePersonnel,c.models.PlCarrier,c.models.PlPersonnel]: model.__table__.create(c.db.bind)
|
|
c.db.add(c.models.PlVehicle(id='v',vehicle_type='self',license_plate='CAR',tenant_no='t'))
|
|
c.transport=c.models.PlTransport(id='tr',transport_no='TR',tenant_no='t',approval_status='pending',vehicle_id='v',license_plate='CLIENT')
|
|
c.db.add(c.transport)
|
|
c.approval=c.models.PlTransportApproval(id='approval',transport_id='tr',approver_id='u',approver_name='U',approval_order=1,tenant_no='t')
|
|
c.db.add(c.approval)
|
|
c.plan.transport_id='tr';c.plan.cert_configuration=c.payload.quota_configuration.model_dump(mode='json',by_alias=True)
|
|
c.db.commit()
|
|
c.user.to_tenant_user_dict=lambda:{}
|
|
monkeypatch.setattr(api,'get_tenant_no',lambda *a:'t')
|
|
c.api=api
|
|
return c
|
|
|
|
|
|
@pytest.mark.parametrize('state',['cancelled','archived','exception','completed'])
|
|
def test_approval_cannot_reactivate_terminal_plan(approval_case,state):
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
c=approval_case
|
|
if state in ('archived','exception'): c.plan.archive_status=state
|
|
else: c.plan.follow_status=state
|
|
c.db.commit()
|
|
with pytest.raises(HTTPException): c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
assert c.approval.status == 'pending'
|
|
|
|
|
|
def test_approval_requires_authoritative_vehicle_and_current_approver(approval_case):
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
c=approval_case
|
|
c.transport.vehicle_id=None;c.transport.license_plate='CAR';c.db.commit()
|
|
with pytest.raises(HTTPException): c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
c.transport.vehicle_id='v';c.approval.approver_id='other';c.db.commit()
|
|
with pytest.raises(HTTPException) as error: c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
assert error.value.status_code == 403
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
|
|
|
|
def test_approval_uses_actual_vehicle_not_client_plate(approval_case):
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
c=approval_case
|
|
c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
assert c.transport.license_plate == c.plan.license_plate == 'CAR'
|
|
assert c.transport.resource_snapshot and c.plan.resource_snapshot
|
|
|
|
|
|
@pytest.mark.parametrize('foreign,deleted',[(True,False),(False,True)])
|
|
def test_vehicle_info_cannot_read_foreign_or_deleted_vehicle(approval_case,foreign,deleted):
|
|
c=approval_case
|
|
vehicle=c.db.get(c.models.PlVehicle,'v')
|
|
vehicle.tenant_no='foreign' if foreign else 't';vehicle.del_flag=int(deleted);c.db.commit()
|
|
with pytest.raises(HTTPException): c.api._get_vehicle_info(c.db,'v','t')
|
|
|
|
|
|
|
|
def test_authoritative_vehicle_change_invalidates_confirmation(approval_case):
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
c=approval_case
|
|
vehicle=c.db.get(c.models.PlVehicle,'v');vehicle.license_plate='OTHER';vehicle.trailer_no='TRAILER'
|
|
c.transport.license_plate='CAR';c.db.commit()
|
|
with pytest.raises(HTTPException): c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
assert c.approval.status == 'pending'
|
|
|
|
|
|
def test_cancelled_released_plan_does_not_reserve_again(approval_case):
|
|
from app.schemas.pl.transport import ApprovalRequest
|
|
from app.services.pl_quota_business import apply_plan_quota
|
|
c=approval_case
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
apply_plan_quota(c.db,c.user,c.plan,'release')
|
|
c.plan.follow_status='cancelled';c.plan.archive_status='cancelled';c.transport.transport_status='cancelled'
|
|
c.db.commit()
|
|
with pytest.raises(HTTPException): c.api.process_approval('approval',ApprovalRequest(action='approve'),c.db,c.user)
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
assert c.plan.follow_status == 'cancelled'
|
|
|
|
|
|
@pytest.mark.parametrize('entry',['arrange','draft'])
|
|
@pytest.mark.parametrize('state',['cancelled','archived','exception','completed'])
|
|
def test_arrange_and_draft_reject_terminal_plan(approval_case,entry,state):
|
|
from app.schemas.pl.transport import TransportCreate
|
|
c=approval_case
|
|
c.models.PlPlanTransport.__table__.create(c.db.bind)
|
|
if state in ('archived','exception'): c.plan.archive_status=state
|
|
else: c.plan.follow_status=state
|
|
c.db.commit()
|
|
with pytest.raises(HTTPException) as error:
|
|
if entry=='arrange': c.api.arrange_transport('plan',c.payload,c.db,c.user)
|
|
else: c.api.create_transport(TransportCreate(planIds=['plan']),c.db,c.user)
|
|
assert error.value.status_code == 409
|
|
c.db.rollback()
|
|
assert c.db.get(c.m.PlPurchaseCert,'p').reserved_qty == 0
|
|
assert c.db.query(c.models.PlTransport).count() == 1
|
|
|
|
|
|
def test_single_plan_arrange_cannot_change_shared_transport_or_either_quota(approval_case, monkeypatch):
|
|
from app.services.pl_quota_business import apply_plan_quota
|
|
from app.services.pl_quota_sources import extract_sources
|
|
from app.services.pl_quota_planning import plan_fingerprint
|
|
from app.services import pl_plan_execution_lock
|
|
c=approval_case
|
|
c.models.PlPlanTransport.__table__.create(c.db.bind)
|
|
other=c.db.get(c.models.PlPlan,'other');other.license_plate='CAR';other.transport_id='tr'
|
|
c.db.add(c.models.PlPlanProduct(id='ppb',plan_id='other',product_id='product',product_no='SKU',product_name='酸',is_toxic=1,tenant_no='t'))
|
|
c.db.add(c.models.PlPlanLoad(id='loadb',plan_id='other',tenant_no='t'))
|
|
c.db.add(c.models.PlPlanLoadProduct(id='spb',load_id='loadb',plan_id='other',plan_product_id='ppb',product_id='product',product_no='SKU',product_name='酸',plan_qty=29,unit='吨',contract_id='c',contract_product_id='cp',tenant_no='t'))
|
|
c.db.flush()
|
|
payload_b=c.payload.model_copy(deep=True);payload_b.operation_id='reserve-b'
|
|
fingerprint=plan_fingerprint(extract_sources(c.db,c.user,'other', measurement_version='per_stop_v2')[0])
|
|
for condition in payload_b.quota_configuration.conditions: condition.source_fingerprint=fingerprint
|
|
apply_plan_quota(c.db,c.user,c.plan,'reserve',c.payload)
|
|
apply_plan_quota(c.db,c.user,other,'reserve',payload_b)
|
|
for plan in (c.plan,other):
|
|
plan.transport_status='arranged';plan.follow_status='arranged';plan.vehicle_id='v';plan.resource_snapshot='original-'+plan.id
|
|
c.db.add(c.models.PlPlanTransport(id='link-'+plan.id,plan_id=plan.id,transport_id='tr',tenant_no='t'))
|
|
c.transport.license_plate='CAR';c.transport.resource_snapshot='original-transport'
|
|
c.db.add(c.models.PlVehicle(id='new-vehicle',license_plate='NEW',vehicle_type='self',tenant_no='t'))
|
|
c.db.commit()
|
|
def snapshot():
|
|
models=[c.models.PlPlan,c.models.PlTransport,c.models.PlPlanTransport,c.m.PlPurchaseCert,c.m.PlTransportCert,c.m.PlPlanCertUsage,c.m.PlPlanCertUsageItem,c.m.PlCertOperation,c.m.PlCertQtyLog]
|
|
return {m.__tablename__:[{col.name:getattr(row,col.name) for col in m.__table__.columns} for row in c.db.query(m).order_by(m.id)] for m in models}
|
|
before=snapshot()
|
|
monkeypatch.setattr(pl_plan_execution_lock,'plan_execution_lock_reason',lambda *a:None)
|
|
payload=c.payload.model_copy(deep=True);payload.operation_id='change-shared';payload.vehicle_id='new-vehicle'
|
|
with pytest.raises(HTTPException) as error:
|
|
c.api.arrange_transport('plan',payload,c.db,c.user)
|
|
assert error.value.status_code == 409
|
|
assert '共享运输' in error.value.detail
|
|
assert snapshot() == before
|
|
|
|
|
|
def test_per_stop_reserves_both_certificates_and_settles_each_actual(business):
|
|
from app.services.pl_quota_planning import configuration_context
|
|
from app.services.pl_quota_business import apply_plan_quota, settle_plan_quota
|
|
c = business
|
|
c.db.get(c.models.PlPlanLoadProduct, 'sp').plan_qty = 100
|
|
c.db.get(c.models.PlPlanLoadProduct, 'sp').inbound_qty = 90
|
|
for index, qty in [(1, 40), (2, 60)]:
|
|
c.db.add(c.models.PlPlanDelivery(id=f'd{index}', plan_id='plan', tenant_no='t', seq=index))
|
|
c.db.add(c.models.PlPlanDeliveryProduct(id=f'dp{index}', delivery_id=f'd{index}', plan_id='plan', plan_product_id='pp', product_id='product', product_no='SKU', product_name='酸', plan_qty=qty, outbound_qty=qty-5, unit='吨', contract_id='c', contract_product_id='cp', tenant_no='t'))
|
|
for model, cid in [(c.m.PlPurchaseCert, 'p'), (c.m.PlTransportCert, 't')]:
|
|
c.db.get(model, cid).total_qty = 300
|
|
c.db.flush()
|
|
config = c.payload.quota_configuration
|
|
fingerprint = configuration_context(c.db, c.user, 'plan', config)['sourceFingerprint']
|
|
for condition in config.conditions: condition.source_fingerprint = fingerprint
|
|
apply_plan_quota(c.db, c.user, c.plan, 'reserve', c.payload)
|
|
assert c.plan.cert_configuration['measurementVersion'] == 'per_stop_v2'
|
|
assert all(u.confirmation_snapshot['measurementVersion'] == 'per_stop_v2' for u in c.db.query(c.m.PlPlanCertUsage))
|
|
assert all(i.source_snapshot['measurementVersion'] == 'per_stop_v2' for i in c.db.query(c.m.PlPlanCertUsageItem))
|
|
for model, cid in [(c.m.PlPurchaseCert, 'p'), (c.m.PlTransportCert, 't')]:
|
|
assert c.db.get(model, cid).reserved_qty == 200
|
|
assert settle_plan_quota(c.db, c.user, c.plan, c.payload)
|
|
for model, cid in [(c.m.PlPurchaseCert, 'p'), (c.m.PlTransportCert, 't')]:
|
|
assert c.db.get(model, cid).used_qty == 180
|
|
|
|
|
|
def test_arrange_pins_v2_before_state_changes(approval_case, monkeypatch):
|
|
from app.services import pl_plan_execution_lock, pl_contract_plan_qty
|
|
c = approval_case
|
|
c.models.PlPlanTransport.__table__.create(c.db.bind)
|
|
monkeypatch.setattr(pl_plan_execution_lock, 'plan_execution_lock_reason', lambda *a: None)
|
|
monkeypatch.setattr(pl_contract_plan_qty, 'auto_start_execute_approved_contracts', lambda *a, **kw: None)
|
|
c.models.PlPlanContract.__table__.create(c.db.bind)
|
|
# 检查正式安排路径抵达额度调用时,pending 计划的版本已经固定。
|
|
def check_quota(db, user, plan, action, payload):
|
|
assert plan.transport_status == 'arranged'
|
|
assert plan.cert_configuration.get('measurementVersion') == 'per_stop_v2'
|
|
raise RuntimeError('checked quota boundary')
|
|
monkeypatch.setattr(c.api, 'apply_plan_quota', check_quota)
|
|
with pytest.raises(RuntimeError, match='checked quota boundary'):
|
|
c.api.arrange_transport('plan', c.payload, c.db, c.user)
|
|
|
|
|
|
def test_supplement_new_certificate_keeps_server_measurement_version(business):
|
|
from datetime import date
|
|
from test_pl_quota_usage import _second_pair
|
|
from app.services.pl_quota_business import apply_plan_quota, settle_plan_quota
|
|
from app.services.pl_quota_planning import configuration_context
|
|
from app.schemas.pl.quota_planning import CertCondition
|
|
c = business
|
|
apply_plan_quota(c.db, c.user, c.plan, 'reserve', c.payload)
|
|
_second_pair(c)
|
|
for model, cid in [(c.m.PlPurchaseCert, 'p'), (c.m.PlTransportCert, 't')]:
|
|
c.db.get(model, cid).total_qty = 100
|
|
c.db.get(model, cid).valid_to = date(2000, 1, 1)
|
|
c.db.get(c.models.PlPlanLoadProduct, 'sp').inbound_qty = 35
|
|
c.db.flush()
|
|
config = c.payload.quota_configuration.model_copy(deep=True)
|
|
config.purchase_cert_ids = ['p2']; config.transport_cert_ids = ['t2']
|
|
fingerprint = configuration_context(c.db, c.user, 'plan', config)['sourceFingerprint']
|
|
config.conditions = [CertCondition(certType=kind, certId=cid, certRevision=1, sourceFingerprint=fingerprint, partiesConfirmed=True, routeConfirmed=True, note='核对补证') for kind, cid in [('purchase', 'p2'), ('transport', 't2')]]
|
|
payload = SimpleNamespace(operation_id='supplement', quota_allow_supplement=True, quota_configuration=config, quota_reason='实际超量', quota_source_note='磅单')
|
|
assert settle_plan_quota(c.db, c.user, c.plan, payload)
|
|
assert all(u.confirmation_snapshot.get('measurementVersion') == 'per_stop_v2' for u in c.db.query(c.m.PlPlanCertUsage))
|
|
assert configuration_context(c.db, c.user, 'plan', config)['measurementVersion'] == 'per_stop_v2'
|