| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- import assert from 'node:assert/strict'
- import test from 'node:test'
- import { execFileSync } from 'node:child_process'
- import { fileURLToPath } from 'node:url'
- import { prepareLineAliasAndBrandForSubmit } from '../src/utils/crm-product-brand'
- import {
- mirrorMobilePlanProducts,
- buildMobileProductLinks,
- hydrateMobileStopMeta,
- enrichMobileStopProducts
- } from '../src/views/m/pl/plan/mobilePlanPersistence'
- const line = (overrides: any = {}) => ({
- productId: 'p',
- productNo: 'P',
- productName: '标准名',
- unit: '吨',
- packageSpec: '桶',
- model: 'M',
- planQty: 1,
- ...overrides
- })
- test('actual mobile mirror consumes each old plan identity once and repairs duplicate historic links', () => {
- const rows = [
- line({
- planProductId: 'pp',
- contractProductId: 'c1',
- aliasName: '别名一',
- brandMode: 'required',
- brandId: 'b1'
- }),
- line({ planProductId: 'pp', contractProductId: 'c2', packageSpec: '袋', planQty: 2 })
- ]
- const result = mirrorMobilePlanProducts(rows, [
- { id: 'pp', productId: 'p', packageSpec: '桶', purchaseCertUrl: 'cert' }
- ] as any)
- assert.deepEqual(
- result.map((x) => x.id),
- ['pp', undefined]
- )
- assert.equal(result[0].purchaseCertUrl, 'cert')
- assert.equal(result[0].brandId, 'b1')
- assert.equal(
- result.reduce((n, x) => n + Number(x.quantity), 0),
- 3
- )
- })
- test('request keys link same product different contract packaging without client-created database ids', () => {
- const loads = [
- line({ contractProductId: 'c1' }),
- line({ contractProductId: 'c2', packageSpec: '袋', planQty: 2 })
- ]
- const deliveries = [
- {
- products: [
- line({
- contractProductId: 'sell2',
- packageSpec: '袋',
- brandMode: 'required',
- brandId: 'delivery-brand',
- aliasName: '送货别名'
- })
- ]
- }
- ]
- const plan = mirrorMobilePlanProducts(loads, [])
- const linked = buildMobileProductLinks(loads, deliveries, plan)
- assert.equal(linked.products.length, 2)
- assert.equal(linked.products[0].id, undefined)
- assert.notEqual(linked.products[0].clientKey, linked.products[1].clientKey)
- assert.equal(linked.deliveries[0][0].planProductClientKey, linked.products[1].clientKey)
- assert.equal(linked.deliveries[0][0].mobileProductMeta.brandId, 'delivery-brand')
- assert.equal(linked.deliveries[0][0].mobileProductMeta.aliasName, '送货别名')
- })
- test('existing valid associations remain and new associations follow Web row order', () => {
- const loads = [line({ planProductId: 'a' }), line({ planProductId: 'b' })]
- const plan = mirrorMobilePlanProducts(loads, [
- { id: 'a', productId: 'p', packageSpec: '桶' },
- { id: 'b', productId: 'p', packageSpec: '桶' }
- ] as any)
- assert.equal(
- buildMobileProductLinks(loads, [{ products: [line({ planProductId: 'b' })] }], plan)
- .deliveries[0][0].planProductId,
- 'b'
- )
- assert.equal(
- buildMobileProductLinks(loads, [{ products: [line()] }], plan).deliveries[0][0].planProductId,
- 'a'
- )
- })
- test('delivery packaging override links automatically and remains independent in the submit payload', async () => {
- const { mobileLoadCandidates, resolveMobileDeliveryLoad } = await import(
- '../src/views/m/pl/plan/mobilePlanPersistence'
- )
- const { buildStopProductPayload } = await import('../src/utils/pl-plan-stop-form')
- const load = line({ packageSpec: '25Kg/袋', productName: '片碱', planQty: 1 })
- const delivery = line({ packageSpec: '20kg/袋', productName: '片碱', planQty: 100 })
- const plans = mirrorMobilePlanProducts([load], [])
- assert.deepEqual(mobileLoadCandidates(delivery, [load]), [load])
- assert.equal(resolveMobileDeliveryLoad(delivery, [load], plans), 0)
- const links = buildMobileProductLinks([load], [{ products: [delivery] }], plans)
- const payload = {
- load: { ...buildStopProductPayload(load), ...links.load[0] },
- delivery: { ...buildStopProductPayload(delivery), ...links.deliveries[0][0] }
- }
- assert.equal(payload.load.planProductClientKey, payload.delivery.planProductClientKey)
- assert.equal(payload.load.packageSpec, '25Kg/袋')
- assert.equal(payload.delivery.packageSpec, '20kg/袋')
- assert.equal(payload.load.planQty, 1)
- assert.equal(payload.delivery.planQty, 100)
- })
- test('multiple load rows prefer an exact package match but permit explicit cross-package association', async () => {
- const { mobileLoadCandidates, mobileLoadProductKey, resolveMobileDeliveryLoad } = await import(
- '../src/views/m/pl/plan/mobilePlanPersistence'
- )
- const loads = [
- line({ packageSpec: '25Kg/袋', planProductId: 'pp1' }),
- line({ packageSpec: '20kg/袋', planProductId: 'pp2' })
- ]
- const plans = mirrorMobilePlanProducts(
- loads,
- loads.map((row) => ({ ...row, id: row.planProductId }))
- )
- assert.equal(resolveMobileDeliveryLoad(line({ packageSpec: '20kg/袋' }), loads, plans), 1)
- const row = line({ packageSpec: '50kg/袋' })
- assert.equal(mobileLoadCandidates(row, loads).length, 2)
- assert.equal(resolveMobileDeliveryLoad(row, loads, plans), 0)
- row.mobileLoadProductKey = mobileLoadProductKey(loads[1])
- assert.equal(resolveMobileDeliveryLoad(row, loads, plans), 1)
- const saved = line({ planProductId: 'pp2', packageSpec: '50kg/袋' })
- assert.equal(resolveMobileDeliveryLoad(saved, loads, plans), 1)
- loads[1].packageSpec = '散装'
- assert.equal(resolveMobileDeliveryLoad(saved, loads, plans), 1)
- for (const field of ['productId', 'model', 'unit']) {
- assert.equal(mobileLoadCandidates(line({ [field]: 'different' }), loads).length, 0)
- }
- })
- test('mobile hydration uses independent stop snapshots, not first plan product brand', () => {
- const stop = line({ id: 's', planProductId: 'pp', productName: '送货别名' })
- const rows = hydrateMobileStopMeta(
- [stop],
- [
- {
- ...stop,
- mobileProductMeta: {
- aliasName: '送货别名',
- brandMode: 'required',
- brandId: 'delivery',
- brandName: '送货品牌'
- }
- }
- ]
- )
- const result = enrichMobileStopProducts(rows, [
- {
- id: 'pp',
- productId: 'p',
- productName: '标准名',
- standardProductName: '标准名',
- aliasName: '装货别名',
- aliasOptions: ['装货别名', '送货别名'],
- brandMode: 'required',
- brandId: 'load'
- }
- ] as any)
- assert.equal(result[0].aliasName, '送货别名')
- assert.equal(result[0].brandId, 'delivery')
- })
- test('copy without source ids creates only request keys and keeps metadata', () => {
- const loads = [line({ aliasName: '复制别名' })]
- const linked = buildMobileProductLinks(
- loads,
- [{ products: [line()] }],
- mirrorMobilePlanProducts(loads, [])
- )
- assert.equal(linked.load[0].planProductId, undefined)
- assert.equal(linked.load[0].id, undefined)
- assert.equal(linked.load[0].mobileProductMeta.aliasName, '复制别名')
- })
- test('saved same-packaging link prefers its source and recovers automatically when it is removed', async () => {
- const { mobileLoadProductKey } = await import('../src/views/m/pl/plan/mobilePlanPersistence')
- const loads = [
- line({ contractProductId: '002', contractNo: 'HT002', planQty: 1 }),
- line({ contractProductId: '003', contractNo: 'HT003', planQty: 2 })
- ]
- const delivery = line({
- contractProductId: '005',
- planQty: 2,
- mobileLoadProductKey: mobileLoadProductKey(loads[1])
- })
- const plans = mirrorMobilePlanProducts(loads, [])
- assert.equal(
- buildMobileProductLinks(loads, [{ products: [delivery] }], plans).deliveries[0][0]
- .planProductClientKey,
- 'product-2'
- )
- assert.equal(
- buildMobileProductLinks([loads[0]], [{ products: [delivery] }], [plans[0]]).deliveries[0][0]
- .planProductClientKey,
- 'product-1'
- )
- })
- test('mobile product cards do not expose an internal load association field or picker', async () => {
- const { readFile } = await import('node:fs/promises')
- const source = await readFile(
- new URL('../src/views/m/pl/plan/MobilePlanProductCards.vue', import.meta.url),
- 'utf8'
- )
- assert.doesNotMatch(source, /关联装货产品|mobileLoadProductKey|loadAssociationIndex/)
- })
- test('copy carries two same-packaging associations and one-to-many delivery references through id removal', async () => {
- const persistence = await import('../src/views/m/pl/plan/mobilePlanPersistence')
- assert.equal(typeof persistence.preserveMobileCopyAssociations, 'function')
- const loads = [line({ planProductId: 'pp1', id: 'l1' }), line({ planProductId: 'pp2', id: 'l2' })]
- const deliveries = [
- { products: [line({ planProductId: 'pp2', id: 'd1' })] },
- { products: [line({ planProductId: 'pp2', id: 'd2' })] }
- ]
- persistence.preserveMobileCopyAssociations(loads, deliveries)
- const copiedLoads = loads.map((row) => ({ ...row, id: undefined, planProductId: undefined }))
- const copiedDelivery = deliveries.map((stop) => ({
- products: stop.products.map((row) => ({ ...row, id: undefined, planProductId: undefined }))
- }))
- const links = buildMobileProductLinks(
- copiedLoads,
- copiedDelivery,
- mirrorMobilePlanProducts(copiedLoads, [])
- )
- assert.deepEqual(
- links.deliveries.map((rows) => rows[0].planProductClientKey),
- ['product-2', 'product-2']
- )
- assert.ok(links.products.every((row) => !row.id))
- })
- test('historically collapsed same-packaging load IDs resolve using existing Web order', () => {
- const loads = [line({ planProductId: 'collapsed' }), line({ planProductId: 'collapsed' })]
- const plans = mirrorMobilePlanProducts(loads, [
- { id: 'collapsed', productId: 'p', packageSpec: '桶' }
- ] as any)
- assert.equal(
- buildMobileProductLinks(loads, [{ products: [line({ planProductId: 'collapsed' })] }], plans)
- .deliveries[0][0].planProductClientKey,
- 'product-1'
- )
- })
- test('mobile capability requires explicit version and rejects successful old-server responses', async () => {
- const { assertMobilePersistenceCapability } = await import(
- '../src/views/m/pl/plan/mobilePlanPersistence'
- )
- assert.equal(typeof assertMobilePersistenceCapability, 'function')
- assert.throws(() => assertMobilePersistenceCapability({}), /服务.*更新/)
- assert.throws(
- () => assertMobilePersistenceCapability({ mobileProductMetaVersion: 0 }),
- /服务.*更新/
- )
- assertMobilePersistenceCapability({ mobileProductMetaVersion: 1 })
- })
- test('incomplete draft keeps delivery product and independent metadata without creating a plan product', () => {
- const row = line({ aliasName: '待关联别名', brandMode: 'required', brandId: 'b' })
- const linked = buildMobileProductLinks([], [{ products: [row] }], [], { draft: true })
- assert.equal(linked.products.length, 0)
- assert.equal(linked.deliveries[0][0].unlinkedDraft, true)
- assert.equal(linked.deliveries[0][0].planProductClientKey, undefined)
- assert.equal(linked.deliveries[0][0].mobileProductMeta.brandId, 'b')
- assert.throws(
- () => buildMobileProductLinks([], [{ products: [row] }], []),
- /没有可匹配的装货产品/
- )
- const loads = [line()]
- const formal = buildMobileProductLinks(
- loads,
- [{ products: [row] }],
- mirrorMobilePlanProducts(loads, [])
- )
- assert.equal(formal.deliveries[0][0].unlinkedDraft, undefined)
- assert.equal(formal.deliveries[0][0].planProductClientKey, 'product-1')
- })
- for (const change of ['delete', 'packageSpec', 'model', 'unit']) {
- test(`saved association recovers automatically after load ${change} and keeps draft metadata`, () => {
- const loads = [
- line({ planProductId: 'pp1', id: 'l1' }),
- line({ planProductId: 'pp2', id: 'l2' })
- ]
- const previous = loads.map((row) => ({ ...row, id: row.planProductId }))
- const delivery = line({ id: 'd', planProductId: 'pp2', aliasName: '片碱' })
- const initial = mirrorMobilePlanProducts(loads, previous)
- assert.equal(
- buildMobileProductLinks(loads, [{ products: [delivery] }], initial).deliveries[0][0]
- .planProductId,
- 'pp2'
- )
- if (change === 'delete') loads.splice(1, 1)
- else loads[1][change] = 'changed'
- const plans = mirrorMobilePlanProducts(loads, initial)
- const linked = buildMobileProductLinks(loads, [{ products: [delivery] }], plans)
- .deliveries[0][0]
- assert.equal(linked.planProductId, change === 'packageSpec' ? 'pp2' : 'pp1')
- assert.equal(linked.mobileProductMeta.aliasName, '片碱')
- const draft = buildMobileProductLinks([], [{ products: [delivery] }], [], { draft: true })
- .deliveries[0][0]
- assert.equal(draft.unlinkedDraft, true)
- const reopened = hydrateMobileStopMeta([line({ id: 'd' })], [{ ...draft, id: 'd' }])[0]
- assert.throws(
- () => buildMobileProductLinks([], [{ products: [reopened] }], []),
- /没有可匹配的装货产品/
- )
- const recovered = buildMobileProductLinks(loads, [{ products: [reopened] }], plans)
- .deliveries[0][0]
- assert.equal(recovered.planProductId, 'pp1')
- assert.equal(recovered.unlinkedDraft, undefined)
- assert.equal(recovered.mobileProductMeta.aliasName, '片碱')
- })
- }
- // Execute the real pure Python resolver and read serializer, with the actual Pydantic class
- // extracted to avoid importing application/DB startup. No API, ORM or network is involved.
- function resolvedBackendMetadata(clear: boolean, productName = '新别名') {
- const backend = fileURLToPath(new URL('../../backend/', import.meta.url))
- const code = `
- import ast, importlib.util, json, sys, types
- from pathlib import Path
- from types import SimpleNamespace as Row
- from typing import Optional
- from pydantic import BaseModel, Field
- path = Path('app/services/pl_mobile_stop_meta.py')
- spec = importlib.util.spec_from_file_location('isolated_meta', path)
- m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
- node = next(n for n in ast.parse(Path('app/schemas/pl/plan.py').read_text()).body if isinstance(n, ast.ClassDef) and n.name == 'PlanStopProductMeta')
- scope = dict(BaseModel=BaseModel, Field=Field, Optional=Optional)
- exec(compile(ast.Module(body=[node], type_ignores=[]), '<actual-meta-schema>', 'exec'), scope)
- module = types.ModuleType('app.schemas.pl.plan'); module.PlanStopProductMeta = scope['PlanStopProductMeta']
- sys.modules['app.schemas.pl.plan'] = module
- old = Row(id='meta', tenant_no='t', plan_id='plan', stop_type='load', stop_id='load', stop_product_id='s', contract_id='c', contract_product_id='line', product_id='p', package_spec='桶', model='M', unit='吨', product_name='旧别名', alias_name='旧别名', brand_mode='any', brand_id='', brand_name='', source_product_name='旧别名', source_brand_mode='any', source_brand_id='', source_brand_name='')
- product = Row(**{k: getattr(old, k) for k in m.IDENTITY_FIELDS}, id='s', product_name=sys.argv[2], mobile_product_meta=None)
- plan = Row(product_name=sys.argv[2], brand_mode='any', brand_id='', brand_name='')
- if sys.argv[1] == 'clear':
- product.mobile_product_meta = Row(alias_name='', brand_mode='any', brand_id='', brand_name='')
- values = m.resolve_stop_meta([old], set(), 't', 'plan', 'load', 'load', product, plan)
- model = types.ModuleType('app.models.pl_mobile_stop_meta'); model.PlPlanStopProductMeta = Row
- utils = types.ModuleType('app.core.utils'); utils.generate_uuid = lambda: 'new-meta'
- sys.modules['app.models.pl_mobile_stop_meta'] = model
- sys.modules['app.core.utils'] = utils
- written = []
- m.write_stop_meta(Row(add=written.append), [old], set(), 't', 'plan', 'load', 'load', 's', product, plan, 'u', 'd', None)
- snapshot = written[0]
- assert snapshot.alias_name == values['alias_name']
- # A subsequent unrelated Web save must preserve the distinction too.
- product.mobile_product_meta = None
- values = m.resolve_stop_meta([snapshot], set(), 't', 'plan', 'load', 'load', product, plan)
- snapshot.__dict__.update(values)
- result = Row(load=Row(products=[Row(id='s')]), deliveries=[])
- m.attach_stop_meta(result, [snapshot])
- print(json.dumps(result.load.products[0].mobile_product_meta.model_dump(by_alias=True)))
- `
- return JSON.parse(
- execFileSync(`${backend}/venv/bin/python`, ['-c', code, clear ? 'clear' : 'web', productName], {
- cwd: backend,
- encoding: 'utf8'
- })
- )
- }
- for (const [clear, productName] of [
- [false, '新别名'],
- [false, '标准名'],
- [true, '新别名']
- ] as const) {
- test(`actual backend snapshot → hydrate → enrich → mirror → submit preserves ${clear ? 'mobile clear' : `Web edit to ${productName}`}`, () => {
- const meta = resolvedBackendMetadata(clear, productName)
- const expectedAlias = clear || productName === '标准名' ? '' : '新别名'
- const rows = hydrateMobileStopMeta(
- [line({ id: 's', planProductId: 'pp', productName })],
- [{ id: 's', mobileProductMeta: meta }]
- )
- const catalog = [
- { ...line(), id: 'pp', standardProductName: '标准名', aliasOptions: ['旧别名', '新别名'] }
- ]
- const enriched = enrichMobileStopProducts(rows, catalog as any, true)
- assert.equal(enriched[0].aliasName, expectedAlias)
- const mirrored = mirrorMobilePlanProducts(enriched, catalog as any)
- prepareLineAliasAndBrandForSubmit(mirrored[0])
- assert.equal(mirrored[0].productName, expectedAlias || '标准名')
- assert.equal(meta.aliasName, clear ? '' : null)
- const payload = buildMobileProductLinks(enriched, [], mirrored)
- assert.equal(payload.load[0].mobileProductMeta.aliasName, expectedAlias)
- })
- }
|