diff --git a/backend/app/services/pl_quota_planning.py b/backend/app/services/pl_quota_planning.py
index d6ca420f1..08bffcbf6 100644
--- a/backend/app/services/pl_quota_planning.py
+++ b/backend/app/services/pl_quota_planning.py
@@ -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:
diff --git a/backend/tests/test_pl_quota_sources.py b/backend/tests/test_pl_quota_sources.py
index bff0b3642..70dae3ad5 100644
--- a/backend/tests/test_pl_quota_sources.py
+++ b/backend/tests/test_pl_quota_sources.py
@@ -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'] == []
diff --git a/web-admin/scripts/quota-config-submission.test.ts b/web-admin/scripts/quota-config-submission.test.ts
index 4231e426d..ecddff784 100644
--- a/web-admin/scripts/quota-config-submission.test.ts
+++ b/web-admin/scripts/quota-config-submission.test.ts
@@ -29,3 +29,42 @@ for (const arrange of [false, true]) test(`stale ${arrange ? 'preview' : 'save'}
assert.equal(h.state.visible.value, true)
assert.equal(h.state.busy.value, true)
})
+
+for (const methodName of ['preview', 'loadContext'])
+ test(`${methodName} ignores a result when configuration changes in flight`, async () => {
+ const script = parse(
+ readFileSync(
+ new URL('../src/components/business/pl-plan-quota-dialog/index.vue', import.meta.url),
+ 'utf8'
+ )
+ ).descriptor.scriptSetup!.content
+ const ast = ts.createSourceFile('quota.ts', script, ts.ScriptTarget.Latest, true)
+ const method = ast.statements
+ .find((n) => ts.isFunctionDeclaration(n) && n.name?.text === methodName)!
+ .getText(ast)
+ let finish!: (value: any) => void
+ const pending = new Promise
当前计划没有易制毒产品,无需购买证、运输证额度。
-{{ reason }}
+ >{{ readableReason(reason) }}
{{ row.reason }}
+{{ readableReason(row.reason) }}
+{{ + row.sourceIds.map((id) => sourceLabel(id, plan?.sourceOptions || [])).join(';') + }}