From da56a1e3193ac9191aa81299fb8a74eb47194b83 Mon Sep 17 00:00:00 2001 From: wuzilong <704541015@qq.com> Date: Sun, 1 Nov 2026 22:10:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(pl):=20=E7=94=A8=E5=90=88=E5=90=8C=E4=B8=8E?= =?UTF-8?q?=E6=9D=A5=E6=BA=90=E4=B8=BB=E4=BD=93=E4=BF=A1=E6=81=AF=E6=9B=BF?= =?UTF-8?q?=E4=BB=A3=E7=94=A8=E9=87=8F=E8=BF=BD=E8=B8=AA=E8=A1=A8=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=8A=80=E6=9C=AF=E6=A0=87=E8=AF=86=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/services/pl_quota_statistics.py | 4 ++ backend/tests/test_pl_quota_statistics.py | 19 +++++ web-admin/scripts/quota-statistics.test.ts | 71 +++++++++++++++++++ web-admin/src/api/pl/quota-cert.ts | 2 + .../pl/quota-statistics/StatisticsDetail.vue | 28 ++++++-- 5 files changed, 118 insertions(+), 6 deletions(-) diff --git a/backend/app/services/pl_quota_statistics.py b/backend/app/services/pl_quota_statistics.py index bc2664f21..09c9326c2 100644 --- a/backend/app/services/pl_quota_statistics.py +++ b/backend/app/services/pl_quota_statistics.py @@ -117,12 +117,16 @@ def _details(db, user, keys, contract_ids=None): group['products'].append(value) value['reservedQty'] += item.reserved_qty value['usedQty'] += item.used_qty + source_stop = next((stop for stop in (item.source_snapshot or {}).get('stops', []) + if stop.get('id') == item.source_stop_id), {}) + party_name = source_stop.get('supplier_name' if item.source_stop_type == 'load' else 'customer_name') usage_items.append({'id': item.id, 'usageId': usage.id, 'certType': usage.cert_type, 'certId': usage.cert_id, 'planId': plan.id, 'planNo': plan.plan_no, 'stopType': item.source_stop_type, 'stopId': item.source_stop_id, 'stopProductId': item.source_stop_product_id, 'planProductId': item.plan_product_id, 'contractId': item.contract_id, 'contractProductId': item.contract_product_id, 'productName': product.product_name, 'quantityUnit': usage.quantity_unit, + 'partyName': party_name or '—', 'address': source_stop.get('address') or '—', 'reservedQty': item.reserved_qty, 'usedQty': item.used_qty, 'itemStatus': item.item_status}) return groups, usage_items diff --git a/backend/tests/test_pl_quota_statistics.py b/backend/tests/test_pl_quota_statistics.py index ef0edf626..615267933 100644 --- a/backend/tests/test_pl_quota_statistics.py +++ b/backend/tests/test_pl_quota_statistics.py @@ -216,3 +216,22 @@ def test_all_statistics_reads_emit_no_mutations_or_commit(case, monkeypatch): 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'] == '关联段地址' diff --git a/web-admin/scripts/quota-statistics.test.ts b/web-admin/scripts/quota-statistics.test.ts index 6d62e958c..e6f2320c8 100644 --- a/web-admin/scripts/quota-statistics.test.ts +++ b/web-admin/scripts/quota-statistics.test.ts @@ -289,3 +289,74 @@ test('contract type tags translate all server values and legacy framework', () = assert.equal(renderLabel({ contractType }, ...Object.values(statisticsLabels)), label) } }) + +test('usage trace renders business columns and resolves contract/product by their exact identity', () => { + const sfc = parse( + readFileSync( + new URL('../src/views/pl/quota-statistics/StatisticsDetail.vue', import.meta.url), + 'utf8' + ) + ).descriptor + const source = ts.createSourceFile( + 'detail.ts', + sfc.scriptSetup!.content, + ts.ScriptTarget.Latest, + true + ) + const code = ts.transpile( + source.statements + .filter((s) => !ts.isImportDeclaration(s)) + .map((s) => s.getText(source)) + .join('\n'), + { target: ts.ScriptTarget.ESNext } + ) + const detail = { + contracts: [ + { + id: 'other-contract', + contractNo: '不匹配合同', + products: [{ contractProductId: 'product-id', model: '不匹配规格' }] + }, + { + id: 'contract-id', + contractNo: 'HT-2026-01', + products: [{ contractProductId: 'product-id', model: '98%' }] + } + ], + usageItems: [ + { + id: 'usage-id', + contractId: 'contract-id', + contractProductId: 'product-id', + stopId: 'stop-id', + stopProductId: 'stop-product-id', + planNo: 'PL-001', + stopType: 'delivery', + productName: '硫酸', + partyName: '本段客户', + address: '本段地址' + }, + { id: 'missing', contractId: 'missing', contractProductId: 'product-id', stopType: 'load' } + ] + } + const rows = new Function( + 'defineProps', + 'computed', + `${code}; return typeof usageRows === 'undefined' ? undefined : usageRows.value` + )(() => ({ mode: 'certificate', detail }), vue.computed) + assert.ok(rows, '追踪表应提供可读的业务列') + assert.deepEqual( + rows.map((r: any) => [r.contractNo, r.productModel, r.partyName, r.address]), + [ + ['HT-2026-01', '98%', '本段客户', '本段地址'], + ['—', '—', '—', '—'] + ] + ) + const table = sfc.template!.content.match(//)![0] + const props = [...table.matchAll(/ m[1]) + assert.ok(['contractNo', 'productModel', 'partyName', 'address'].every((p) => props.includes(p))) + assert.ok(!props.some((p) => /Id$/.test(p))) + assert.ok(!table.includes('row.stopId')) + const stopExpression = table.match(/\s*\{\{([\s\S]*?)\}\}/)![1] + assert.equal(new Function('row', `return (${stopExpression})`)(rows[0]), '送货') +}) diff --git a/web-admin/src/api/pl/quota-cert.ts b/web-admin/src/api/pl/quota-cert.ts index 8ceec1c31..cf7c1fa71 100644 --- a/web-admin/src/api/pl/quota-cert.ts +++ b/web-admin/src/api/pl/quota-cert.ts @@ -331,6 +331,8 @@ export interface ContractStatistics extends ContractOption { certificates: (CertificateStatisticsSummary & StatisticsBinding & { productNames: string[] })[] } export interface StatisticsUsageItem { + partyName?: string | null + address?: string | null id: string usageId: string certType: CertType diff --git a/web-admin/src/views/pl/quota-statistics/StatisticsDetail.vue b/web-admin/src/views/pl/quota-statistics/StatisticsDetail.vue index 6988bcf83..2e72e9528 100644 --- a/web-admin/src/views/pl/quota-statistics/StatisticsDetail.vue +++ b/web-admin/src/views/pl/quota-statistics/StatisticsDetail.vue @@ -62,17 +62,18 @@

计划段产品用量追踪

- + {{ row.stopType === 'load' ? '装货' : '送货' }} + - - + + + + @@ -99,4 +100,19 @@ const certificate = computed(() => props.mode === 'certificate' ? (props.detail as CertificateStatistics) : undefined ) + const usageRows = computed(() => + (certificate.value?.usageItems || []).map((item) => { + const contract = certificate.value?.contracts.find((row) => row.id === item.contractId) + const product = contract?.products.find( + (row) => row.contractProductId === item.contractProductId + ) + return { + ...item, + contractNo: contract?.contractNo || '—', + productModel: product?.model || '—', + partyName: item.partyName || '—', + address: item.address || '—' + } + }) + )