Files
shanxia2.0/backend/tests/test_pl_quota_sources.py
T

210 lines
11 KiB
Python

from datetime import date
from decimal import Decimal
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
@pytest.fixture(params=[False, True])
def case(request):
from app.models import pl_models as m
from app.services import pl_quota_sources as svc
engine = create_engine('sqlite://')
for model in (m.PlPlan, m.PlPlanProduct, m.PlPlanLoad, m.PlPlanLoadProduct, m.PlPlanDelivery, m.PlPlanDeliveryProduct):
model.__table__.create(engine)
db = Session(engine, autoflush=request.param)
user = SimpleNamespace(id='u', tenant_no='t', to_user_info=lambda: {'role':'super_admin','user_id':'u'})
db.add(m.PlPlan(id='plan', plan_no='PLAN', create_date=date.today(), plan_type='direct', belongs_company='shanxia', tenant_no='t'))
db.add(m.PlPlanProduct(id='pp', plan_id='plan', product_id='sku', product_no='SKU', product_name='硫酸', is_toxic=1, tenant_no='t'))
db.add(m.PlPlanLoad(id='load', plan_id='plan', tenant_no='t'))
db.add(m.PlPlanDelivery(id='delivery', plan_id='plan', tenant_no='t'))
db.add(m.PlPlanLoadProduct(id='lp', load_id='load', plan_id='plan', plan_product_id='pp', product_id='sku', product_no='SKU', product_name='硫酸', plan_qty=30, contract_id='purchase', contract_product_id='cp', tenant_no='t'))
db.add(m.PlPlanDeliveryProduct(id='dp', delivery_id='delivery', plan_id='plan', plan_product_id='pp', product_id='sku', product_no='SKU', product_name='硫酸', plan_qty=30, contract_id='sale', contract_product_id='cs', tenant_no='t', outbound_qty=0))
db.commit()
yield SimpleNamespace(db=db, user=user, m=m, svc=svc)
db.close(); engine.dispose()
def mapping():
return [{'sourceStopType':'delivery', 'sourceStopProductId':'dp', 'relatedStopProductIds':['lp']}]
def test_two_sides_require_explicit_pairing_and_count_once(case):
c = case
with pytest.raises(HTTPException): c.svc.extract_sources(c.db,c.user,'plan')
sources, actual = c.svc.extract_sources(c.db,c.user,'plan',mapping(),actual=True)
assert len(sources)==1 and sources[0].quantity==Decimal('30')
assert sources[0].contract_id=='sale' and sources[0].related_stop_products==['lp']
assert actual=={'delivery:dp':Decimal('0')}
def test_missing_actual_not_replaced_by_plan_quantity(case):
c = case
c.db.get(c.m.PlPlanDeliveryProduct,'dp').outbound_qty=None
c.db.flush()
with pytest.raises(HTTPException) as error:
c.svc.extract_sources(c.db,c.user,'plan',mapping(),actual=True)
assert error.value.status_code==409
def test_invalid_or_duplicate_pairing_and_omitted_opposite_rejected(case):
c = case
for value in (mapping()*2,[{'sourceStopType':'delivery','sourceStopProductId':'dp','relatedStopProductIds':[]}],
[{'sourceStopType':'delivery','sourceStopProductId':'dp','relatedStopProductIds':['foreign']}],
mapping()+[{'sourceStopType':'load','sourceStopProductId':'lp','relatedStopProductIds':['dp']}]):
with pytest.raises(HTTPException): c.svc.extract_sources(c.db,c.user,'plan',value)
def test_single_side_can_be_derived_and_fingerprint_tracks_plan_facts(case):
c = case
c.db.get(c.m.PlPlanLoadProduct,'lp').del_flag=1; c.db.flush()
first,_=c.svc.extract_sources(c.db,c.user,'plan')
c.db.get(c.m.PlPlan,'plan').license_plate='皖A12345'; c.db.flush()
second,_=c.svc.extract_sources(c.db,c.user,'plan')
assert first[0].fingerprint!=second[0].fingerprint
c.db.get(c.m.PlPlanDeliveryProduct,'dp').outbound_qty=18; c.db.flush()
third,_=c.svc.extract_sources(c.db,c.user,'plan',actual=True)
assert second[0].fingerprint==third[0].fingerprint
def test_explosive_only_needs_no_quota_and_wrong_tenant_rejected(case):
c = case
row=c.db.get(c.m.PlPlanProduct,'pp'); row.is_toxic=0; row.is_explosive=1; c.db.flush()
assert c.svc.extract_sources(c.db,c.user,'plan')==([], {})
other=SimpleNamespace(**{**c.user.__dict__,'tenant_no':'other'})
with pytest.raises(HTTPException): c.svc.extract_sources(c.db,other,'plan')
def test_orphan_effective_stop_product_blocks_instead_of_silently_omitted(case):
c=case
c.db.add(c.m.PlPlanDeliveryProduct(id='orphan',delivery_id='deleted-stop',plan_id='plan',
plan_product_id='pp',product_id='sku',product_no='SKU',product_name='硫酸',plan_qty=70,
contract_id='sale',contract_product_id='cs',tenant_no='t'))
c.db.flush()
with pytest.raises(HTTPException) as error:
c.svc.extract_sources(c.db,c.user,'plan',mapping())
assert error.value.status_code==409
def test_extraction_does_not_flush_pending_changes(case):
from sqlalchemy import event
c=case
c.db.get(c.m.PlPlan,'plan').remark='未保存'
statements=[]
def capture(conn,cursor,statement,parameters,context,executemany):
statements.append(statement)
event.listen(c.db.bind,'before_cursor_execute',capture)
try:
c.svc.extract_sources(c.db,c.user,'plan',mapping())
finally:
event.remove(c.db.bind,'before_cursor_execute',capture)
assert not any(sql.lstrip().upper().startswith(('UPDATE','INSERT','DELETE')) for sql in statements)
def test_preview_vehicle_has_same_fingerprint_as_formal_facts_without_mutation(case):
c = case
plan = c.db.get(c.m.PlPlan, 'plan')
original = plan.license_plate
first, _ = c.svc.extract_sources(c.db, c.user, 'plan', mapping(),
preview_vehicle={'licensePlate': '皖A12345', 'trailerNo': '皖A挂'})
assert plan.license_plate == original and not c.db.dirty
plan.license_plate = '皖A12345'; plan.trailer_no = '皖A挂'; c.db.flush()
second, _ = c.svc.extract_sources(c.db, c.user, 'plan', mapping())
assert first[0].fingerprint == second[0].fingerprint
def test_planning_context_and_options_return_readonly_authoritative_facts(case):
from app.services import pl_quota_planning as planning
from app.models.pl_quota_models import PlPlanCertUsage, PlPlanCertUsageItem
c = case
for model in (PlPlanCertUsage, PlPlanCertUsageItem):
model.__table__.create(c.db.bind)
from app.models.crm_models import CrmContract
CrmContract.__table__.create(c.db.bind)
info = planning.get_plan_quota(c.db, c.user, 'plan')
assert len(info['sourceOptions']) == 2
context = planning.configuration_context(c.db, c.user, 'plan', {'sourceMapping': mapping()},
{'licensePlate': '皖A12345'})
assert context['sources'][0]['snapshot']['vehicle'] == '皖A12345'
assert len(context['sourceFingerprint']) == 64
assert not c.db.dirty and not c.db.new
def test_overview_filters_real_usage_states_ignores_old_ready_and_other_tenant(case):
from app.services.pl_quota_overview import list_plans
from app.models.pl_quota_models import PlPlanCertUsage
c = case
PlPlanCertUsage.__table__.create(c.db.bind)
c.db.get(c.m.PlPlan, 'plan').hazard_cert_ready = 1
c.db.flush()
assert list_plans(c.db, c.user)['list'][0]['quotaStatus'] == 'needs_configuration'
c.db.add(PlPlanCertUsage(id='usage',tenant_no='t',plan_id='plan',plan_no_snapshot='PLAN',cert_type='purchase',cert_id='g',cert_no_snapshot='G',cert_revision=1,quantity_unit='吨',usage_status='reserved'))
c.db.flush()
assert list_plans(c.db, c.user, status='reserved')['total'] == 1
assert list_plans(c.db, c.user, status='needs_configuration')['total'] == 0
c.db.get(PlPlanCertUsage, 'usage').usage_status = 'settled'; c.db.flush()
assert list_plans(c.db, c.user)['list'][0]['quotaStatus'] == 'settled'
c.db.get(PlPlanCertUsage, 'usage').confirmation_snapshot = {'settlementError': '实际纠错失败'}; c.db.flush()
assert list_plans(c.db, c.user, status='exception')['total'] == 1
c.db.get(PlPlanCertUsage, 'usage').confirmation_snapshot = {}; c.db.flush()
assert list_plans(c.db, c.user, status='settled')['total'] == 1
c.db.get(PlPlanCertUsage, 'usage').usage_status = 'exception'; c.db.flush()
assert list_plans(c.db, c.user)['list'][0]['quotaStatus'] == 'exception'
other=SimpleNamespace(**{**c.user.__dict__,'tenant_no':'other'})
assert list_plans(c.db, other)['total'] == 0
c.db.get(c.m.PlPlanProduct, 'pp').is_toxic = 0; c.db.flush()
assert list_plans(c.db, c.user)['list'][0]['quotaStatus'] == 'not_required'
def test_per_stop_counts_all_sides_ignoring_old_client_selection(case):
c = case
c.db.get(c.m.PlPlanLoadProduct, 'lp').plan_qty = 100
c.db.get(c.m.PlPlanDeliveryProduct, 'dp').plan_qty = 40
c.db.add(c.m.PlPlanDelivery(id='delivery2', plan_id='plan', tenant_no='t', seq=2))
c.db.add(c.m.PlPlanDeliveryProduct(id='dp2', delivery_id='delivery2', plan_id='plan',
plan_product_id='pp', product_id='sku', product_no='SKU', product_name='硫酸',
plan_qty=60, contract_id='sale2', contract_product_id='cs2', tenant_no='t'))
c.db.flush()
sources, _ = c.svc.extract_sources(c.db, c.user, 'plan', mapping(), measurement_version='per_stop_v2')
assert sum(s.quantity for s in sources) == 200
assert {(s.contract_id, s.contract_product_id) for s in sources} == {('purchase', 'cp'), ('sale', 'cs'), ('sale2', 'cs2')}
assert all(s.snapshot['measurementVersion'] == 'per_stop_v2' and not s.related_stop_products for s in sources)
@pytest.mark.parametrize('history', ['draft', 'arranged', 'archived', 'ledger'])
def test_server_version_cannot_be_overridden_by_client(case, history):
from app.models.pl_quota_models import PlPlanCertUsage
from app.services.pl_quota_planning import configuration_context
c = case
PlPlanCertUsage.__table__.create(c.db.bind)
plan = c.db.get(c.m.PlPlan, 'plan')
if history == 'arranged': plan.transport_status = 'arranged'
if history == 'archived': plan.archive_status = 'archived'
if history == 'ledger':
c.db.add(PlPlanCertUsage(id='u', tenant_no='t', plan_id='plan', plan_no_snapshot='PLAN', cert_type='purchase', cert_id='p', cert_no_snapshot='P', cert_revision=1, quantity_unit='吨'))
c.db.flush()
config = {'sourceMapping': mapping(), 'measurementVersion': 'legacy_single_side' if history == 'draft' else 'per_stop_v2'}
result = configuration_context(c.db, c.user, 'plan', config)
assert result['measurementVersion'] == ('per_stop_v2' if history == 'draft' else 'legacy_single_side')
assert sum(Decimal(s['quantity']) for s in result['sources']) == (60 if history == 'draft' else 30)
def test_options_include_stop_contract_and_party_without_foreign_contract_leak(case):
from app.models.crm_models import CrmContract
from app.models.pl_quota_models import PlPlanCertUsage, PlPlanCertUsageItem
from app.services.pl_quota_planning import get_plan_quota
c = case
for model in (CrmContract, PlPlanCertUsage, PlPlanCertUsageItem): model.__table__.create(c.db.bind)
c.db.add(CrmContract(id='purchase', contract_no='PUR-1', contract_name='购买合同', customer_id='customer', contract_type='purchase', tenant_no='t'))
c.db.add(CrmContract(id='sale', contract_no='SECRET', contract_name='其他租户', customer_id='customer', contract_type='sale', tenant_no='other'))
c.db.get(c.m.PlPlanLoad, 'load').customer_name = '装货方'
c.db.flush()
options = {o['sourceStopType']: o for o in get_plan_quota(c.db, c.user, 'plan')['sourceOptions']}
assert options['load']['contractNo'] == 'PUR-1'
assert options['load']['partyName'] == '装货方'
assert options['load']['stopId'] == 'load' and options['load']['stopOrder'] == 1
assert options['delivery']['contractNo'] is None