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

249 lines
13 KiB
Python

"""统计只读、主对象分页与授权明细聚合回归。"""
from datetime import date
from types import SimpleNamespace
import importlib
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from app.models.crm_models import CrmContract, CrmContractProduct
from app.models.pl_models import PlPlan
from app.models import pl_quota_models as m
@pytest.fixture
def case():
engine = create_engine('sqlite://')
models = (CrmContract, CrmContractProduct, PlPlan, m.PlPurchaseCert, m.PlTransportCert,
m.PlContractCertLink, m.PlContractCertProduct, m.PlPlanCertUsage,
m.PlPlanCertUsageItem, m.PlCertQtyLog)
for model in models:
model.__table__.create(engine)
db = Session(engine)
user = SimpleNamespace(tenant_no='t', to_user_info=lambda: {'role': 'super_admin', 'user_id': 'u'})
for cid in ('a', 'b'):
db.add(CrmContract(id=cid, contract_no=cid, contract_name=cid, customer_id='customer',
contract_type='sale', status='approved', tenant_no='t'))
db.add(CrmContractProduct(id=cid, contract_id=cid, product_id='chemical', product_name=cid,
unit='吨', quantity=100, tenant_no='t'))
db.add(m.PlContractCertLink(id=cid, contract_id=cid, contract_no_snapshot=cid,
cert_type='purchase', cert_id='same', tenant_no='t'))
db.add(m.PlContractCertProduct(id=cid, contract_cert_link_id=cid,
contract_product_id=cid, tenant_no='t'))
db.add(m.PlPurchaseCert(id='same', cert_no='P', product_name_on_cert='硫酸', total_qty=300,
quantity_unit='吨', reserved_qty=180, used_qty=20, valid_use_text='多次有效',
buyer_name='买方', seller_name='卖方', tenant_no='t'))
db.commit()
yield SimpleNamespace(db=db, user=user)
db.close()
engine.dispose()
def service():
assert importlib.util.find_spec('app.services.pl_quota_statistics'), '缺少统计服务'
return importlib.import_module('app.services.pl_quota_statistics')
def add_usage(c, cid, ident, reserved, used=0):
c.db.add(PlPlan(id=ident, plan_no=ident, create_date=date.today(), belongs_company='shanxia', plan_type='direct', tenant_no='t'))
c.db.add(m.PlPlanCertUsage(id=ident, plan_id=ident, plan_no_snapshot=ident, cert_type='purchase',
cert_id='same', cert_no_snapshot='P', cert_revision=1, quantity_unit='吨', tenant_no='t'))
c.db.add(m.PlPlanCertUsageItem(id=ident, usage_id=ident, contract_cert_link_id=cid,
contract_cert_product_id=cid, contract_scope_revision=1, contract_id=cid,
contract_product_id=cid, plan_product_id=ident, source_stop_type='delivery',
source_stop_id=ident, source_stop_product_id=ident, allocation_seq=1, source_unit='吨',
planned_source_qty=999, reserved_qty=reserved, used_qty=used, tenant_no='t'))
c.db.commit()
def test_shared_balance_and_contract_allocation_are_distinct_and_read_only(case):
s = service()
add_usage(case, 'a', 'one', 60, 20)
add_usage(case, 'a', 'two', 40)
add_usage(case, 'b', 'three', 80)
row = s.contract_statistics(case.db, case.user, 'a')['certificates'][0]
assert (row['totalQty'], row['reservedQty'], row['usedQty'], row['availableQty']) == (300, 180, 20, 100)
assert row['contractReservedQty'] == 100 and row['contractUsedQty'] == 20
assert row['products'][0]['reservedQty'] == 100
detail = s.certificate_statistics(case.db, case.user, 'purchase', 'same')
assert len(detail['usageItems']) == 3
assert detail['openingUsedQty'] == 0
assert case.db.query(m.PlContractCertLink).count() == 2
assert not case.db.new and not case.db.dirty
def test_lists_paginate_main_objects_and_filter_keywords(case):
s = service()
result = s.list_contracts(case.db, case.user, page=1, page_size=1)
assert result['total'] == 2 and len(result['list']) == 1 and result['pageSize'] == 1
assert s.list_contracts(case.db, case.user, keyword='a')['total'] == 1
assert s.list_certificates(case.db, case.user, keyword='硫酸')['total'] == 1
other = SimpleNamespace(**{**case.user.__dict__, 'tenant_no': 'other'})
assert s.list_contracts(case.db, other)['total'] == 0
assert s.list_certificates(case.db, other)['total'] == 0
def test_hidden_contracts_plans_and_deleted_products_never_enter_details(case, monkeypatch):
s = service()
add_usage(case, 'a', 'visible', 30)
add_usage(case, 'a', 'hidden-plan', 70)
add_usage(case, 'b', 'hidden-contract', 80)
monkeypatch.setattr(s, 'apply_crm_owner_scope', lambda q, model, *a: q.filter(model.id == 'a'))
monkeypatch.setattr(s, 'apply_pl_scope', lambda q, model, *a: q.filter(model.id != 'hidden-plan'))
detail = s.certificate_statistics(case.db, case.user, 'purchase', 'same')
assert [x['id'] for x in detail['contracts']] == ['a']
assert detail['contracts'][0]['contractReservedQty'] == 30
assert [x['planId'] for x in detail['usageItems']] == ['visible']
assert detail['openingUsedQty'] == 0 and detail['unattributedUsedQty'] == 0
case.db.query(CrmContractProduct).filter_by(id='a').one().del_flag = 1
case.db.commit()
assert s.certificate_statistics(case.db, case.user, 'purchase', 'same')['usageItems'] == []
def test_same_id_certificate_types_units_and_real_opening_are_separate(case):
s = service()
# 按当前模型构造运输证,验证同 ID 两个额度池不会在分页或汇总中合并。
purchase = case.db.query(m.PlPurchaseCert).one()
values = {col.name: getattr(purchase, col.name) for col in m.PlTransportCert.__table__.columns
if hasattr(purchase, col.name)}
values.update(cert_no='T', quantity_unit='kg', total_qty=500, reserved_qty=0, used_qty=0)
for col in m.PlTransportCert.__table__.columns:
if not col.nullable and col.default is None and col.name not in values:
values[col.name] = '运输主体'
case.db.add(m.PlTransportCert(**values))
for ident, event, amount, plan, tenant, deleted in (
('opening', 'opening', 7, None, 't', 0), ('adjust', 'adjust', 2, None, 't', 0),
('hidden', 'settle', 99, 'hidden-plan', 't', 0),
('other', 'opening', 99, None, 'other', 0), ('deleted', 'opening', 99, None, 't', 1)):
case.db.add(m.PlCertQtyLog(id=ident, operation_id=ident, request_hash='hash', cert_type='purchase',
cert_id='same', plan_id=plan, event_type=event, quantity_unit='吨', used_delta=amount,
before_total_qty=300, after_total_qty=300, before_reserved_qty=0, after_reserved_qty=0,
before_used_qty=0, after_used_qty=amount, reason='凭据', tenant_no=tenant, del_flag=deleted))
case.db.commit()
result = s.list_certificates(case.db, case.user, page_size=1)
assert result['total'] == 2 and len(result['list']) == 1
assert s.list_certificates(case.db, case.user, cert_type='transport')['list'][0]['quantityUnit'] == 'kg'
purchase = s.certificate_statistics(case.db, case.user, 'purchase', 'same')
transport = s.certificate_statistics(case.db, case.user, 'transport', 'same')
assert purchase['openingUsedQty'] == 7 and purchase['unattributedUsedQty'] == 2
assert transport['openingUsedQty'] == 0 and transport['contracts'] == []
@pytest.mark.parametrize('model,field,value', [
(m.PlPlanCertUsage, 'tenant_no', 'other'), (m.PlPlanCertUsage, 'del_flag', 1),
(m.PlPlanCertUsageItem, 'tenant_no', 'other'), (m.PlPlanCertUsageItem, 'del_flag', 1),
(PlPlan, 'tenant_no', 'other'), (PlPlan, 'del_flag', 1),
(CrmContract, 'del_flag', 1), (CrmContractProduct, 'tenant_no', 'other')])
def test_invalid_ancestors_exclude_usage(case, model, field, value):
s = service()
add_usage(case, 'a', 'one', 100)
row = case.db.query(model).filter_by(id='a' if model in (CrmContract, CrmContractProduct) else 'one').one()
setattr(row, field, value)
case.db.commit()
detail = s.certificate_statistics(case.db, case.user, 'purchase', 'same')
assert detail['usageItems'] == []
assert all(c['contractReservedQty'] == 0 for c in detail['contracts'])
def test_hidden_certificates_excluded_and_page_queries_do_not_grow_with_rows(case, monkeypatch):
from sqlalchemy import event
s = service()
counts = []
for size in (1, 20):
statements = []
def capture(*args):
statements.append(args[2])
event.listen(case.db.bind, 'before_cursor_execute', capture)
try:
s.list_contracts(case.db, case.user, page_size=size)
finally:
event.remove(case.db.bind, 'before_cursor_execute', capture)
counts.append(len(statements))
assert all(sql.lstrip().upper().startswith('SELECT') for sql in statements)
assert counts[0] == counts[1]
monkeypatch.setattr(s, 'apply_pl_scope', lambda q, model, *a: q.filter(model.id != 'same'))
assert s.list_contracts(case.db, case.user)['total'] == 0
assert s.contract_statistics(case.db, case.user, 'a')['certificates'] == []
def test_historical_usage_survives_removed_binding_without_recreation(case):
s = service()
add_usage(case, 'a', 'one', 0, 20)
case.db.query(m.PlContractCertLink).filter_by(id='a').one().del_flag = 1
case.db.query(m.PlContractCertProduct).filter_by(id='a').one().del_flag = 1
case.db.commit()
row = s.contract_statistics(case.db, case.user, 'a')['certificates'][0]
assert row['contractUsedQty'] == 20 and row['products'][0]['bindingId'] is None
assert case.db.query(m.PlContractCertLink).filter_by(id='a').one().del_flag == 1
def test_role_union_uses_existing_crm_owner_and_pl_creator_helpers(case, monkeypatch):
from app.core import data_scope
s = service()
add_usage(case, 'a', 'self-plan', 30)
add_usage(case, 'a', 'custom-plan', 40)
add_usage(case, 'a', 'hidden-plan', 50)
case.db.query(m.PlPurchaseCert).one().create_by = 'u'
case.db.query(CrmContract).filter_by(id='a').one().owner_user_id = 'u'
case.db.query(CrmContract).filter_by(id='b').one().owner_user_id = 'hidden'
for ident, creator in [('self-plan', 'u'), ('custom-plan', 'colleague'), ('hidden-plan', 'hidden')]:
case.db.query(PlPlan).filter_by(id=ident).one().create_by = creator
case.db.commit()
monkeypatch.setattr(data_scope, '_resolve_user_role_scopes', lambda *a: [
{'data_scope': '4', 'details': []},
{'data_scope': '5', 'details': [SimpleNamespace(scope_type='user', scope_value='colleague')]}])
user = SimpleNamespace(tenant_no='t', to_user_info=lambda: {'role': 'staff', 'user_id': 'u', 'dept_id': 'd'})
result = s.certificate_statistics(case.db, user, 'purchase', 'same')
assert [row['id'] for row in result['contracts']] == ['a']
assert result['contracts'][0]['contractReservedQty'] == 70
assert {row['planId'] for row in result['usageItems']} == {'self-plan', 'custom-plan'}
def test_all_statistics_reads_emit_no_mutations_or_commit(case, monkeypatch):
from sqlalchemy import event
s = service()
statements = []
def no_commit():
pytest.fail('统计不得提交事务')
monkeypatch.setattr(case.db, 'commit', no_commit)
def capture(*args):
statements.append(args[2])
event.listen(case.db.bind, 'before_cursor_execute', capture)
try:
s.list_contracts(case.db, case.user)
s.list_certificates(case.db, case.user)
s.contract_statistics(case.db, case.user, 'a')
s.certificate_statistics(case.db, case.user, 'purchase', 'same')
finally:
event.remove(case.db.bind, 'before_cursor_execute', capture)
assert all(sql.lstrip().upper().startswith('SELECT') for sql in statements)
@pytest.mark.parametrize('source_type,source_stop,expected', [
('delivery', {'id': 'source', 'customer_name': '本段客户', 'address': '本段送货地址'}, ('本段客户', '本段送货地址')),
('load', {'id': 'source', 'supplier_name': '本段供应商', 'address': '本段装货地址'}, ('本段供应商', '本段装货地址')),
('delivery', {'id': 'source'}, ('—', '—')),
('delivery', {'id': 'missing', 'customer_name': '其他客户', 'address': '其他地址'}, ('—', '—')),
])
def test_usage_party_is_taken_only_from_exact_source_stop_snapshot(case, source_type, source_stop, expected):
add_usage(case, 'a', 'source', 20)
item = case.db.query(m.PlPlanCertUsageItem).filter_by(id='source').one()
item.source_stop_type = source_type
item.source_snapshot = {'stops': [
{'id': 'related', 'customer_name': '关联段客户', 'supplier_name': '关联段供应商', 'address': '关联段地址'},
source_stop,
]}
case.db.commit()
row = service().certificate_statistics(case.db, case.user, 'purchase', 'same')['usageItems'][0]
assert (row['partyName'], row['address']) == expected
assert item.source_snapshot['stops'][0]['address'] == '关联段地址'
def test_number_filters_are_independent_from_keyword_and_applied_before_pagination(case):
s = service()
case.db.query(CrmContract).filter_by(id='a').one().contract_name = '目标合同'
case.db.commit()
assert s.list_contracts(case.db, case.user, keyword='目标', contract_no='a', page_size=1)['total'] == 1
assert s.list_contracts(case.db, case.user, keyword='目标', contract_no='b')['total'] == 0
assert s.list_contracts(case.db, case.user, contract_no='目标')['total'] == 0
assert s.list_certificates(case.db, case.user, keyword='硫酸', cert_no='P', page_size=1)['total'] == 1
assert s.list_certificates(case.db, case.user, keyword='硫酸', cert_no='missing')['total'] == 0
assert s.list_certificates(case.db, case.user, cert_no='硫酸')['total'] == 0