feat(pl): 展示各装送段用证配置与独立占用明细并保留历史计量口径。

This commit is contained in:
wuzilong committed 2026-11-01 22:54:51 +08:00
1 parent 706260ff7a
commit b764eb02a7
10 files changed
+497 -40

No files matched your search

+35 -14
View File
@@ -156,34 +156,55 @@ def save_configuration(db, user, plan_id, config):
def get_plan_quota(db, user, plan_id):
with db.no_autoflush:
plan, products, rows, stops = source_context(db, user, plan_id)
from app.services.pl_quota_contract_service import _contract
contract_numbers = {}
from app.services.pl_quota_contract_service import _contract, get_contract_bindings
contract_numbers, contract_bindings = {}, {}
for cid in {r.contract_id for values in rows.values() for r in values.values() if r.contract_id}:
try:
contract_numbers[cid] = _contract(db, user, cid).contract_no
bindings = get_contract_bindings(db, user, cid)
contract_bindings[cid] = []
for binding in bindings:
if binding['linkStatus'] != 'active':
continue
cert = _get_row(db, user, binding['certType'], binding['certId'])
contract_bindings[cid].append({
'certId': cert.id, 'certType': binding['certType'],
'certNo': cert.cert_no, 'quantityUnit': cert.quantity_unit,
'productIds': [p['contractProductId'] for p in binding['products']
if p['scopeStatus'] == 'active']})
except HTTPException as exc:
if exc.status_code not in (403, 404):
raise
contract_numbers[cid] = None
options = []
contract_bindings.pop(cid, None)
options, stop_products = [], []
for side, values in rows.items():
for row in values.values():
if products[row.plan_product_id].is_toxic == 1:
stop = stops[side][getattr(row, 'load_id' if side == 'load' else 'delivery_id')]
options.append({'sourceStopId': stop.id, 'stopId': stop.id,
'stopOrder': getattr(stop, 'seq', None) or stop.order_num or 1,
'productId': row.product_id, 'productNo': row.product_no,
'sourceStopType': side, 'sourceStopProductId': row.id,
'planProductId': row.plan_product_id, 'productName': row.product_name,
'model': row.model, 'quantity': row.plan_qty, 'unit': row.unit,
'contractId': row.contract_id, 'contractNo': contract_numbers.get(row.contract_id),
'contractProductId': row.contract_product_id,
'partyName': stop.customer_name})
product = products[row.plan_product_id]
stop = stops[side][getattr(row, 'load_id' if side == 'load' else 'delivery_id')]
item = {'sourceStopId': stop.id, 'stopId': stop.id,
'stopOrder': getattr(stop, 'seq', None) or stop.order_num or 1,
'productId': row.product_id, 'productNo': row.product_no,
'sourceStopType': side, 'sourceStopProductId': row.id,
'planProductId': row.plan_product_id, 'productName': row.product_name,
'model': row.model, 'quantity': row.plan_qty, 'unit': row.unit,
'contractId': row.contract_id, 'contractNo': contract_numbers.get(row.contract_id),
'contractProductId': row.contract_product_id,
'partyName': stop.customer_name}
stop_products.append({**item, 'isToxic': product.is_toxic == 1,
'isExplosive': product.is_explosive == 1,
'bindingsReadable': row.contract_id in contract_bindings,
'certificates': [{k: v for k, v in cert.items() if k != 'productIds'}
for cert in contract_bindings.get(row.contract_id, [])
if row.contract_product_id in cert['productIds']]})
if product.is_toxic == 1:
options.append(item)
usages = db.query(PlPlanCertUsage).filter_by(tenant_no=user.tenant_no, plan_id=plan_id, del_flag=0).order_by(PlPlanCertUsage.create_time.desc()).all()
version = resolve_measurement_version(db, user, plan)
result = {'planId': plan.id, 'planNo': plan.plan_no, 'licensePlate': plan.license_plate,
'measurementVersion': version,
'configuration': {**(plan.cert_configuration or {}), 'measurementVersion': version}, 'sourceOptions': options,
'stopProducts': stop_products,
'canSaveConfiguration': plan.transport_status != 'arranged' and plan.archive_status != 'archived',
'usages': []}
for usage in usages:
+40 -1
View File
@@ -194,10 +194,11 @@ def test_server_version_cannot_be_overridden_by_client(case, history):
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 PlContractCertLink, PlContractCertProduct
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)
for model in (CrmContract, PlPlanCertUsage, PlPlanCertUsageItem, PlContractCertLink, PlContractCertProduct): 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 = '装货方'
@@ -207,3 +208,41 @@ def test_options_include_stop_contract_and_party_without_foreign_contract_leak(c
assert options['load']['partyName'] == '装货方'
assert options['load']['stopId'] == 'load' and options['load']['stopOrder'] == 1
assert options['delivery']['contractNo'] is None
def test_readonly_stop_products_include_explosive_without_changing_sources(case):
from app.models.crm_models import CrmContract
from app.models.pl_quota_models import PlContractCertLink, PlContractCertProduct
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, PlContractCertLink, PlContractCertProduct): model.__table__.create(c.db.bind)
product = c.db.get(c.m.PlPlanProduct, 'pp')
product.is_toxic = 0
product.is_explosive = 1
c.db.flush()
result = get_plan_quota(c.db, c.user, 'plan')
assert result['sourceOptions'] == []
assert len(result['stopProducts']) == 2
assert all(row['isExplosive'] and not row['isToxic'] for row in result['stopProducts'])
assert {row['contractId'] for row in result['stopProducts']} == {'purchase', 'sale'}
def test_stop_products_bindings_are_product_specific_and_hidden_contract_is_explicit(case):
from app.models.crm_models import CrmContract
from app.models.pl_quota_models import (PlPlanCertUsage, PlPlanCertUsageItem,
PlContractCertLink, PlContractCertProduct, PlPurchaseCert)
from app.services.pl_quota_planning import get_plan_quota
c = case
for model in (CrmContract, PlPlanCertUsage, PlPlanCertUsageItem, PlContractCertLink, PlContractCertProduct, PlPurchaseCert): model.__table__.create(c.db.bind)
c.db.add(CrmContract(id='purchase', contract_no='PUR', contract_name='采购', customer_id='customer', contract_type='purchase', tenant_no='t'))
c.db.add(PlPurchaseCert(id='cert', cert_no='BUY', product_name_on_cert='硫酸', valid_use_text='多次有效', buyer_name='买方', seller_name='卖方', total_qty=100, quantity_unit='吨', tenant_no='t'))
c.db.add(PlContractCertLink(id='link',contract_id='purchase',contract_no_snapshot='PUR',cert_type='purchase',cert_id='cert',link_status='active',tenant_no='t'))
c.db.add(PlContractCertProduct(id='scope',contract_cert_link_id='link',contract_product_id='cp',scope_status='active',tenant_no='t'))
c.db.flush()
rows = get_plan_quota(c.db,c.user,'plan')['stopProducts']
assert rows[0]['certificates'][0]['certNo'] == 'BUY'
assert rows[1]['bindingsReadable'] is False
assert rows[1]['certificates'] == []
c.db.get(PlContractCertProduct,'scope').scope_status='inactive'; c.db.flush()
assert get_plan_quota(c.db,c.user,'plan')['stopProducts'][0]['certificates'] == []