补齐证书齐备、证件折算、移动用证和合同绑定,并带上报销批量发放、付款备注以及个人开发流程升级,便于继续讨论已安排计划的调整规则。 Co-authored-by: Cursor <cursoragent@cursor.com>
898 lines
29 KiB
TypeScript
898 lines
29 KiB
TypeScript
import assert from 'node:assert/strict'
|
||
import { readFileSync } from 'node:fs'
|
||
import test from 'node:test'
|
||
import { parse } from '@vue/compiler-sfc'
|
||
import ts from 'typescript'
|
||
import * as vue from 'vue'
|
||
import * as model from '../src/views/pl/quota-cert/model'
|
||
import { useClientColumnFilters } from '../src/composables/useClientColumnFilters'
|
||
import { usePersistedListColumns } from '../src/composables/usePersistedListColumns'
|
||
import { getProductUnitLabel } from '../src/utils/crm-product-display'
|
||
import { buyPartyName, expiryWarning, salePartyName } from '../src/views/pl/quota-cert/cert-list-face'
|
||
import {
|
||
bindingsFromPurchaseCerts,
|
||
matchPurchaseCert
|
||
} from '../src/views/pl/quota-cert/contract-binder-rules'
|
||
|
||
function deferred() {
|
||
let resolve!: (value: any) => void
|
||
let reject!: (reason: any) => void
|
||
const promise = new Promise<any>((r, j) => {
|
||
resolve = r
|
||
reject = j
|
||
})
|
||
return { promise, resolve, reject }
|
||
}
|
||
function harness() {
|
||
const reads: string[] = [],
|
||
writes: any[] = [],
|
||
parses: any[] = [],
|
||
messages: string[] = []
|
||
let certData: any = {}
|
||
let saveError = false
|
||
let pendingSave: ReturnType<typeof deferred> | undefined
|
||
let pendingGet: ReturnType<typeof deferred> | undefined
|
||
const user = vue.reactive({ accessToken: 'synthetic', getUserInfo: {} })
|
||
const contractRequests: any[] = []
|
||
const productRequests: any[] = []
|
||
let confirmSwitch = true
|
||
let pendingOptions: ReturnType<typeof deferred> | undefined
|
||
const env = {
|
||
...Object.fromEntries(
|
||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||
),
|
||
...model,
|
||
useClientColumnFilters,
|
||
usePersistedListColumns,
|
||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||
useRoute: () => ({ query: {} }),
|
||
useRouter: () => ({ push() {}, replace() {} }),
|
||
useUserStore: () => user,
|
||
useDictStore: () => ({
|
||
getLabel: (_type: string, value: string) => (value === 'dun' ? '吨' : value || '—'),
|
||
getOptions: () => [{ value: 'dun', label: '吨', tagType: '' }]
|
||
}),
|
||
getProductUnitLabel,
|
||
buyPartyName,
|
||
expiryWarning,
|
||
salePartyName,
|
||
bindingsFromPurchaseCerts,
|
||
matchPurchaseCert,
|
||
onMounted: () => {},
|
||
onBeforeUnmount: () => {},
|
||
fetchGetMenuCodes: async () => ({ all: true }),
|
||
ElMessage: {
|
||
warning: (s: string) => messages.push(s),
|
||
success: (s: string) => messages.push(s)
|
||
},
|
||
ElMessageBox: {
|
||
confirm: async () => {
|
||
if (!confirmSwitch) throw new Error('cancel')
|
||
}
|
||
},
|
||
quotaCertApi: {
|
||
contracts: (keyword: string) => {
|
||
const pending = deferred()
|
||
contractRequests.push({ keyword, ...pending })
|
||
return pending.promise
|
||
},
|
||
products: (id: string) => {
|
||
const pending = deferred()
|
||
productRequests.push({ id, ...pending })
|
||
return pending.promise
|
||
},
|
||
certificateContracts: async () => [
|
||
{ id: 'old', contractNo: '历史合同', contractName: '真实标签' }
|
||
],
|
||
list: async (kind: string) => {
|
||
reads.push(kind)
|
||
return pendingOptions ? pendingOptions.promise : []
|
||
},
|
||
get: async (kind: string, id: string) =>
|
||
pendingGet
|
||
? pendingGet.promise
|
||
: {
|
||
...model.newCert(kind as any),
|
||
id,
|
||
certNo: 'EXISTING',
|
||
attachmentUrls: 'a.png',
|
||
...certData
|
||
},
|
||
save: async (kind: string, body: any, id?: string) => {
|
||
writes.push({ kind, body, id })
|
||
if (saveError) throw new Error('保存失败')
|
||
if (pendingSave) await pendingSave.promise
|
||
},
|
||
recognize: (kind: string, url: string) => {
|
||
const pending = deferred()
|
||
parses.push({ kind, url, ...pending })
|
||
return pending.promise
|
||
}
|
||
}
|
||
}
|
||
const script = parse(
|
||
readFileSync(new URL('../src/views/pl/quota-cert/index.vue', import.meta.url), 'utf8')
|
||
).descriptor.scriptSetup!.content
|
||
const source = ts.createSourceFile('page.ts', script, ts.ScriptTarget.Latest, true)
|
||
const statements = source.statements.filter((s) => !ts.isImportDeclaration(s))
|
||
const names = statements.flatMap((s) =>
|
||
ts.isVariableStatement(s)
|
||
? s.declarationList.declarations
|
||
.filter((d) => ts.isIdentifier(d.name))
|
||
.map((d) => d.name.getText(source))
|
||
: ts.isFunctionDeclaration(s) && s.name
|
||
? [s.name.text]
|
||
: []
|
||
)
|
||
const code = ts.transpile(statements.map((s) => s.getText(source)).join('\n'), {
|
||
target: ts.ScriptTarget.ESNext
|
||
})
|
||
const scope = vue.effectScope()
|
||
const state = scope.run(() =>
|
||
new Function(...Object.keys(env), `${code}; return {${names.join(',')}}`)(...Object.values(env))
|
||
)
|
||
state.formRef.value = { validate: async () => true, clearValidate: () => {} }
|
||
return {
|
||
state,
|
||
user,
|
||
deferSave: () => (pendingSave = deferred()),
|
||
deferGet: () => (pendingGet = deferred()),
|
||
contractRequests,
|
||
productRequests,
|
||
setCert: (value: any) => {
|
||
certData = value
|
||
},
|
||
failSave: () => {
|
||
saveError = true
|
||
},
|
||
reads,
|
||
writes,
|
||
parses,
|
||
messages,
|
||
deferOptions: () => {
|
||
pendingOptions = deferred()
|
||
return pendingOptions
|
||
},
|
||
cancelSwitch: () => {
|
||
confirmSwitch = false
|
||
},
|
||
stop: () => scope.stop()
|
||
}
|
||
}
|
||
|
||
test('each contract link keeps its own tab, including repeated contracts', () => {
|
||
const h = harness()
|
||
try {
|
||
const tabs = h.state.contractTabItems(
|
||
[
|
||
{ id: 'link-1', contractId: 'c1', products: [] },
|
||
{ id: 'link-2', contractId: 'c1', products: [] },
|
||
{ id: 'link-3', contractId: 'c2', products: [] }
|
||
],
|
||
[
|
||
{ id: 'c1', contractNo: 'HT-1', contractName: '重复合同' },
|
||
{ id: 'c2', contractNo: 'HT-2', contractName: '另一合同' }
|
||
]
|
||
)
|
||
assert.deepEqual(
|
||
tabs.map((tab: { key: string; label: string }) => [tab.key, tab.label]),
|
||
[
|
||
['link-1', 'HT-1(1)'],
|
||
['link-2', 'HT-1(2)'],
|
||
['link-3', 'HT-2']
|
||
]
|
||
)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('replacing certificate products keeps the other certificates on the contract', () => {
|
||
const result = model.replaceCertProducts(
|
||
[
|
||
{ certType: 'transport', certId: 't', products: [{ contractProductId: 'keep' }] },
|
||
{
|
||
certType: 'purchase',
|
||
certId: 'p',
|
||
id: 'link',
|
||
products: [{ id: 'old', contractProductId: 'a' }]
|
||
}
|
||
],
|
||
'purchase',
|
||
'p',
|
||
[{ contractProductId: 'b' }]
|
||
)
|
||
assert.equal(result[0].certId, 't')
|
||
assert.deepEqual(result[0].products, [{ contractProductId: 'keep' }])
|
||
assert.equal(result[1].id, 'link')
|
||
assert.deepEqual(result[1].products, [{ contractProductId: 'b' }])
|
||
})
|
||
|
||
test('certificate quantities display three decimal places', () => {
|
||
assert.equal(model.formatCertQty('1700.000000'), '1700.000')
|
||
assert.equal(model.formatCertQty('0.000000'), '0.000')
|
||
assert.equal(model.formatCertQty('1.23456'), '1.235')
|
||
assert.equal(model.formatCertQty('9.9995'), '10.000')
|
||
assert.equal(model.formatCertQty('-1.2345'), '-1.235')
|
||
assert.equal(model.formatCertQty('-0.0004'), '0.000')
|
||
assert.equal(model.formatCertQty(''), '—')
|
||
assert.equal(model.formatCertQty(null), '—')
|
||
})
|
||
|
||
test('new certificate defaults to review after image parsing', () => {
|
||
assert.equal(model.newCert('purchase').entryMode, 'image_parse')
|
||
assert.equal(model.newCert('transport').entryMode, 'image_parse')
|
||
})
|
||
|
||
test('type changes immediately while options load and stale options cannot enter a later form', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
const options = h.deferOptions()
|
||
const switching = s.changeFormKind('transport')
|
||
assert.equal(s.formKind.value, 'transport')
|
||
await s.changeFormKind('purchase')
|
||
options.resolve([{ id: 'old-option' }])
|
||
await switching
|
||
assert.equal(s.formKind.value, 'purchase')
|
||
assert.deepEqual(s.purchases.value, [])
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('new form type changes without closing dialog or changing the list, and drives save', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
assert.equal(s.formKind?.value, 'purchase')
|
||
await s.changeFormKind('transport')
|
||
assert.equal(s.formKind.value, 'transport')
|
||
assert.equal(s.kind.value, 'purchase')
|
||
assert.equal(s.formVisible.value, true)
|
||
assert.ok(s.formFields.value.some(([key]: string[]) => key === 'shipperName'))
|
||
assert.ok(!s.formFields.value.some(([key]: string[]) => key === 'buyerName'))
|
||
s.form.value.shipperName = '合成托运单位'
|
||
await s.save()
|
||
assert.equal(h.writes[0].kind, 'transport')
|
||
assert.equal(h.writes[0].body.shipperName, '合成托运单位')
|
||
assert.equal(h.writes[0].body.buyerName, undefined)
|
||
assert.equal(s.kind.value, 'transport')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('switch clears old certificate data after confirmation, preserving attachments and mode', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.form.value.certNo = 'PURCHASE-1'
|
||
s.form.value.entryMode = 'image_parse'
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.form.value.remark = '保留备注'
|
||
await s.changeFormKind('transport')
|
||
assert.equal(s.form.value.certNo, '')
|
||
assert.equal(s.form.value.attachmentUrls, 'one.png')
|
||
assert.equal(s.form.value.entryMode, 'image_parse')
|
||
assert.equal(s.form.value.remark, '保留备注')
|
||
assert.equal(s.form.value.buyerName, undefined)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('cancel type switch and existing certificate edit preserve data and original type', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.form.value.certNo = 'KEEP'
|
||
h.cancelSwitch()
|
||
s.selectedFormKind.value = 'transport'
|
||
await s.changeFormKind('transport')
|
||
assert.equal(s.formKind.value, 'purchase')
|
||
assert.equal(s.selectedFormKind.value, 'purchase')
|
||
assert.equal(s.form.value.certNo, 'KEEP')
|
||
await s.edit({ id: 'existing' })
|
||
await s.changeFormKind('transport')
|
||
assert.equal(s.formKind.value, 'purchase')
|
||
assert.equal(s.form.value.attachmentUrls, 'a.png')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('single attachment is automatically selected, parsing uses form type and preserves manual edits', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await s.changeFormKind('transport')
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await vue.nextTick()
|
||
assert.equal(s.recognitionImage.value, 'one.png')
|
||
s.form.value.certNo = 'MANUAL'
|
||
s.edited.add('certNo')
|
||
const parsing = s.recognize()
|
||
assert.equal(h.parses.length, 1)
|
||
assert.equal(h.parses[0].kind, 'transport')
|
||
assert.equal(h.parses[0].url, 'one.png')
|
||
h.parses[0].resolve({
|
||
candidates: { certNo: 'OCR', shipperName: '合成托运单位' },
|
||
invalidFields: {}
|
||
})
|
||
await parsing
|
||
assert.equal(s.form.value.certNo, 'MANUAL')
|
||
assert.equal(s.form.value.shipperName, '合成托运单位')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('unconfigured image parsing warns and keeps the manual entry', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.certNo = '手工证号'
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await vue.nextTick()
|
||
const parsing = s.recognize()
|
||
h.parses[0].reject(new Error('图片解析尚未配置,可继续手工录入'))
|
||
await parsing
|
||
assert.equal(s.form.value.certNo, '手工证号')
|
||
assert.equal(s.recognizing.value, false)
|
||
assert.ok(h.messages.includes('图片解析尚未配置,可继续手工录入'))
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('missing, removed, multiple new attachments and uploads in progress never start parsing', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = ''
|
||
const empty = s.recognize()
|
||
assert.equal(h.parses.length, 0)
|
||
await empty
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await vue.nextTick()
|
||
s.form.value.attachmentUrls = ''
|
||
await vue.nextTick()
|
||
assert.equal(s.recognitionImage.value, '')
|
||
await s.recognize()
|
||
s.form.value.attachmentUrls = 'one.png,two.png'
|
||
s.recognitionImage.value = 'one.png'
|
||
await s.recognize()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.certAttachments.value = { hasUploading: () => true }
|
||
await s.recognize()
|
||
assert.equal(h.parses.length, 0)
|
||
await s.save()
|
||
assert.equal(h.writes.length, 0)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('closed or switched forms reject delayed recognition results', async () => {
|
||
for (const action of ['close', 'switch']) {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await vue.nextTick()
|
||
const parsing = s.recognize()
|
||
assert.equal(h.parses.length, 1)
|
||
if (action === 'close') {
|
||
s.formVisible.value = false
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
} else await s.changeFormKind('transport')
|
||
h.parses[0].resolve({ candidates: { certNo: 'OLD' }, invalidFields: {} })
|
||
await parsing
|
||
assert.equal(s.form.value.certNo, '')
|
||
assert.equal(s.recognizing.value, false)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
}
|
||
})
|
||
|
||
test('多合同一次保存且失败保留输入;受限可见关系只展示并省略提交', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
const bindings = [
|
||
{
|
||
id: 'b',
|
||
contractId: 'old',
|
||
linkStatus: 'inactive',
|
||
remark: '保留',
|
||
orderNum: 4,
|
||
products: [
|
||
{
|
||
id: 'scope',
|
||
contractProductId: 'p',
|
||
scopeStatus: 'inactive',
|
||
remark: '范围',
|
||
orderNum: 2
|
||
}
|
||
]
|
||
}
|
||
]
|
||
try {
|
||
h.setCert({ contractBindings: bindings, bindingsEditable: true })
|
||
await s.edit({ id: 'existing' })
|
||
assert.deepEqual(s.form.value.contractBindings, bindings)
|
||
assert.equal(s.bindingContracts.value[0].contractNo, '历史合同')
|
||
s.addContractBinding('new')
|
||
assert.equal(s.form.value.contractBindings.length, 2)
|
||
h.failSave()
|
||
await assert.rejects(s.save(), /保存失败/)
|
||
assert.equal(h.writes.length, 1)
|
||
assert.equal(h.writes[0].body.contractBindings[0].products[0].id, 'scope')
|
||
assert.equal(s.formVisible.value, true)
|
||
assert.equal(s.form.value.contractBindings.length, 2)
|
||
s.formVisible.value = false
|
||
h.setCert({ contractBindings: bindings, bindingsEditable: false })
|
||
await s.edit({ id: 'limited' })
|
||
s.addContractBinding('forbidden')
|
||
assert.equal(s.form.value.contractBindings.length, 1)
|
||
await assert.rejects(s.save())
|
||
assert.equal('contractBindings' in h.writes[1].body, false)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('确定产品时保留原范围身份,已用证不能增删产品', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.addContractBinding('c')
|
||
s.form.value.contractBindings[0].products = [
|
||
{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }
|
||
]
|
||
s.acceptFormBinding({
|
||
contractId: 'c',
|
||
contractNo: 'HT',
|
||
contractName: '合同',
|
||
products: [
|
||
{ contractProductId: 'p', scopeStatus: 'inactive' },
|
||
{ contractProductId: 'p2', scopeStatus: 'active' }
|
||
]
|
||
})
|
||
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
|
||
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'inactive')
|
||
assert.equal(s.form.value.contractBindings[0].products[1].contractProductId, 'p2')
|
||
s.form.value.contractBindings[0].editLocked = true
|
||
s.acceptFormBinding({
|
||
contractId: 'c',
|
||
contractNo: 'HT',
|
||
contractName: '合同',
|
||
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
|
||
})
|
||
assert.equal(s.form.value.contractBindings[0].products.length, 2)
|
||
assert.ok(h.messages.some((message: string) => message.includes('不能增删产品')))
|
||
s.acceptFormBinding({
|
||
contractId: 'c',
|
||
contractNo: 'HT',
|
||
contractName: '合同',
|
||
products: [
|
||
{ contractProductId: 'p', scopeStatus: 'active' },
|
||
{ contractProductId: 'p2', scopeStatus: 'inactive' }
|
||
]
|
||
})
|
||
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
|
||
assert.equal(s.form.value.contractBindings[0].products[0].scopeStatus, 'active')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('移除后重选同一合同保留主关联和产品范围身份', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
h.setCert({
|
||
contractBindings: [
|
||
{
|
||
id: 'binding',
|
||
contractId: 'old',
|
||
linkStatus: 'inactive',
|
||
products: [{ id: 'scope', contractProductId: 'p', scopeStatus: 'inactive' }]
|
||
}
|
||
]
|
||
})
|
||
await s.edit({ id: 'cert' })
|
||
s.removeContractBinding(0)
|
||
s.addContractBinding('old')
|
||
assert.equal(s.form.value.contractBindings[0].id, 'binding')
|
||
assert.equal(s.form.value.contractBindings[0].products[0].id, 'scope')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('租户切换使保存和读取旧结果失效', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
const pending = h.deferSave()
|
||
const saving = s.save()
|
||
await vue.nextTick()
|
||
h.user.accessToken = ''
|
||
pending.resolve({})
|
||
await saving
|
||
assert.equal(h.messages.includes('证件已保存'), false)
|
||
assert.equal(s.formVisible.value, false)
|
||
const get = h.deferGet(),
|
||
editing = s.edit({ id: 'old' })
|
||
h.user.getUserInfo = { tenantNo: 'other' }
|
||
get.resolve({ ...model.newCert('purchase'), contractBindings: [] })
|
||
await editing
|
||
assert.equal(s.formVisible.value, false)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('新增多合同连同证面成功提交一次,移除全部提交明确空集合', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.acceptFormBinding({
|
||
contractId: 'one',
|
||
contractNo: 'ONE',
|
||
contractName: '合同一',
|
||
products: [{ contractProductId: 'p1', scopeStatus: 'active' }]
|
||
})
|
||
s.acceptFormBinding({
|
||
contractId: 'two',
|
||
contractNo: 'TWO',
|
||
contractName: '合同二',
|
||
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
|
||
})
|
||
s.form.value.certNo = 'MULTI'
|
||
await s.save()
|
||
assert.equal(h.writes.length, 1)
|
||
assert.equal(h.writes[0].body.certNo, 'MULTI')
|
||
assert.deepEqual(
|
||
h.writes[0].body.contractBindings.map((b: any) => b.contractId),
|
||
['one', 'two']
|
||
)
|
||
assert.equal(s.formVisible.value, true)
|
||
h.setCert({ contractBindings: [{ id: 'b', contractId: 'one', products: [] }] })
|
||
await s.edit({ id: 'existing' })
|
||
s.removeContractBinding(0)
|
||
await s.save()
|
||
assert.deepEqual(h.writes[1].body.contractBindings, [])
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('未填写合同不能保存,确定另一份合同不会改掉已有合同', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.addContractBinding()
|
||
assert.equal(s.form.value.contractBindings.length, 1)
|
||
assert.equal(s.form.value.contractBindings[0].contractId, '')
|
||
await s.save()
|
||
assert.equal(h.writes.length, 0)
|
||
s.acceptFormBinding({
|
||
contractId: 'c2',
|
||
contractNo: 'HT-2',
|
||
contractName: '另一合同',
|
||
products: [{ contractProductId: 'p2', scopeStatus: 'active' }]
|
||
})
|
||
assert.equal(s.form.value.contractBindings[1].contractId, 'c2')
|
||
assert.equal(s.form.value.contractBindings[1].products[0].contractProductId, 'p2')
|
||
assert.equal(s.form.value.contractBindings[0].contractId, '')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('referenced bindings and references cannot be edited or removed', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
h.setCert({
|
||
referencesEditable: false,
|
||
contractBindings: [
|
||
{
|
||
id: 'b',
|
||
contractId: 'c',
|
||
editLocked: true,
|
||
products: [{ id: 'p', contractProductId: 'p1' }]
|
||
}
|
||
],
|
||
purchaseRefs: [{ id: 'r', referenceType: 'unknown', referenceNo: 'ref' }]
|
||
})
|
||
s.kind.value = 'transport'
|
||
await s.edit({ id: 'cert' })
|
||
s.removeContractBinding(0)
|
||
assert.equal(s.form.value.contractBindings.length, 1)
|
||
s.acceptFormBinding({
|
||
contractId: 'c',
|
||
contractNo: 'HT',
|
||
contractName: '合同',
|
||
products: [{ contractProductId: 'other', scopeStatus: 'active' }]
|
||
})
|
||
assert.equal(s.form.value.contractBindings[0].products[0].contractProductId, 'p1')
|
||
s.removeReference(0)
|
||
s.addReference()
|
||
assert.equal(s.form.value.purchaseRefs.length, 1)
|
||
assert.ok(h.messages.some((m) => m.includes('不能新增或移除证面引用')))
|
||
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('linked purchase archive stays locked while an empty archive can be completed', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
h.setCert({
|
||
referencesEditable: false,
|
||
purchaseRefs: [
|
||
{ id: 'open', referenceType: 'unknown', referenceNo: 'G35', purchaseCertId: null },
|
||
{ id: 'linked', referenceType: 'purchase_permit', referenceNo: 'G1', purchaseCertId: 'p1' }
|
||
]
|
||
})
|
||
s.kind.value = 'transport'
|
||
await s.edit({ id: 'cert' })
|
||
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
|
||
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[1]), true)
|
||
s.purchases.value = [{ id: 'buy', certNo: 'G34260485131004', certType: 'purchase' }]
|
||
s.form.value.purchaseRefs[0].purchaseCertId = 'buy'
|
||
s.onPurchaseArchiveChange()
|
||
assert.equal(s.form.value.purchaseRefs[0].referenceNo, 'G35')
|
||
assert.equal(s.referenceRowLocked(s.form.value.purchaseRefs[0]), false)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('recognition cannot populate locked empty references while other empty fields remain fillable', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
h.setCert({ referencesEditable: false, purchaseRefs: [], attachmentUrls: 'one.png' })
|
||
s.kind.value = 'transport'
|
||
await s.edit({ id: 'cert' })
|
||
const parsing = s.recognize()
|
||
h.parses[0].resolve({
|
||
candidates: {
|
||
purchaseRefs: [{ referenceType: 'unknown', referenceNo: 'OCR-NEW' }],
|
||
shipperName: '合成托运单位'
|
||
},
|
||
invalidFields: {}
|
||
})
|
||
await parsing
|
||
assert.deepEqual(s.form.value.purchaseRefs, [])
|
||
assert.equal(s.form.value.shipperName, '合成托运单位')
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('successful edit clears attachment and remark and becomes a fresh open form', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit({ id: 'existing' })
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.form.value.remark = '本张备注'
|
||
await s.save()
|
||
assert.equal(h.writes[0].body.remark, '本张备注')
|
||
assert.equal(s.formVisible.value, true)
|
||
assert.equal(s.editingId.value, '')
|
||
assert.equal(s.form.value.attachmentUrls, '')
|
||
assert.equal(s.form.value.remark, '')
|
||
s.form.value.attachmentUrls = 'next.png'
|
||
await s.save()
|
||
assert.equal(h.writes[1].id, undefined)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('save and recognition reject missing or historical multiple attachments', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit({ id: 'existing' })
|
||
for (const urls of ['', 'a.png,b.png']) {
|
||
s.form.value.attachmentUrls = urls
|
||
await vue.nextTick()
|
||
s.recognitionImage.value = urls.split(',')[0]
|
||
await s.save()
|
||
const parsing = s.recognize()
|
||
for (const p of h.parses) p.resolve({ candidates: {}, invalidFields: {} })
|
||
await parsing
|
||
}
|
||
assert.equal(h.writes.length, 0)
|
||
assert.equal(h.parses.length, 0)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('new transport save keeps its type open while failures and recognition preserve the current remark', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
await s.changeFormKind('transport')
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
s.form.value.remark = '不得丢失'
|
||
await s.save()
|
||
assert.equal(s.formVisible.value, true)
|
||
assert.equal(s.kind.value, 'transport')
|
||
assert.equal(s.formKind.value, 'transport')
|
||
assert.equal(s.form.value.remark, '')
|
||
assert.deepEqual(s.form.value.purchaseRefs, [])
|
||
assert.deepEqual(s.form.value.contractBindings, [])
|
||
s.form.value.attachmentUrls = 'two.png'
|
||
s.form.value.remark = '失败保留'
|
||
s.recognizing.value = true
|
||
await s.save()
|
||
assert.equal(h.writes.length, 1)
|
||
assert.equal(s.form.value.remark, '失败保留')
|
||
s.recognizing.value = false
|
||
h.failSave()
|
||
await assert.rejects(s.save(), /保存失败/)
|
||
assert.equal(s.form.value.remark, '失败保留')
|
||
assert.equal(s.form.value.attachmentUrls, 'two.png')
|
||
assert.equal(s.formVisible.value, true)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('saving while transport purchase candidates load refreshes candidates for the next session', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
const old = h.deferOptions()
|
||
const switching = s.changeFormKind('transport')
|
||
const fresh = h.deferOptions()
|
||
s.form.value.attachmentUrls = 'one.png'
|
||
await s.save()
|
||
old.resolve([{ id: 'stale' }])
|
||
await switching
|
||
assert.deepEqual(s.purchases.value, [])
|
||
fresh.resolve([{ id: 'fresh' }])
|
||
await Promise.resolve()
|
||
await vue.nextTick()
|
||
assert.deepEqual(s.purchases.value, [{ id: 'fresh' }])
|
||
assert.equal(s.formVisible.value, true)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('transport recognition copies contracts and products from the matched purchase cert', async () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
await s.edit()
|
||
await s.changeFormKind('transport')
|
||
s.form.value.attachmentUrls = 'cert.pdf'
|
||
await vue.nextTick()
|
||
h.setCert({
|
||
contractBindings: [
|
||
{
|
||
id: 'purchase-link',
|
||
contractId: 'c1',
|
||
linkStatus: 'active',
|
||
products: [{ id: 'purchase-scope', contractProductId: 'p1', scopeStatus: 'active' }]
|
||
}
|
||
]
|
||
})
|
||
const options = h.deferOptions()
|
||
const parsing = s.recognize()
|
||
h.parses[0].resolve({
|
||
candidates: {
|
||
purchaseRefs: [{ referenceType: 'unknown', referenceNo: 'G34260671919592' }]
|
||
},
|
||
invalidFields: {}
|
||
})
|
||
options.resolve([{ id: 'buy-1', certNo: 'G34260671919592' }])
|
||
await parsing
|
||
assert.equal(s.form.value.purchaseRefs[0].purchaseCertId, 'buy-1')
|
||
assert.equal(s.form.value.contractBindings.length, 1)
|
||
assert.equal(s.form.value.contractBindings[0].contractId, 'c1')
|
||
assert.equal(s.form.value.contractBindings[0].products[0].contractProductId, 'p1')
|
||
assert.equal(s.form.value.contractBindings[0].products[0].id, undefined)
|
||
assert.equal(
|
||
s.bindingContracts.value.some((contract: { id: string }) => contract.id === 'old'),
|
||
true
|
||
)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|
||
|
||
test('deleting the certificate attachment clears face, bindings and references', () => {
|
||
const h = harness(),
|
||
s = h.state
|
||
try {
|
||
s.formKind.value = 'transport'
|
||
s.form.value = {
|
||
...model.newCert('transport'),
|
||
entryMode: 'manual',
|
||
attachmentUrls: 'one.png',
|
||
remark: '保留备注',
|
||
orderNum: 3,
|
||
status: 'active',
|
||
certNo: 'Y1',
|
||
productNameOnCert: '硫酸',
|
||
totalQty: '10',
|
||
vehicleNumbersJson: ['皖A1'],
|
||
purchaseRefs: [{ referenceNo: 'G1' }],
|
||
contractBindings: [{ contractId: 'c1', linkStatus: 'active', products: [{ id: 'p1' }] }],
|
||
bindingsEditable: true,
|
||
referencesEditable: false
|
||
}
|
||
s.edited.add('certNo')
|
||
s.bindingContracts.value = [{ id: 'c1', contractNo: 'HT-1' }]
|
||
s.lockedPlates.value = ['皖A1']
|
||
s.linkedRefIds.value = ['ref-1']
|
||
s.onCertificateAttachmentChange('one.png')
|
||
assert.equal(s.form.value.certNo, 'Y1')
|
||
assert.equal(s.bindingEpoch.value, 0)
|
||
s.onCertificateAttachmentChange('')
|
||
assert.equal(s.form.value.certNo, '')
|
||
assert.equal(s.form.value.productNameOnCert, '')
|
||
assert.equal(s.form.value.totalQty, '')
|
||
assert.deepEqual(s.form.value.vehicleNumbersJson, [])
|
||
assert.deepEqual(s.form.value.purchaseRefs, [])
|
||
assert.deepEqual(s.form.value.contractBindings, [])
|
||
assert.equal(s.form.value.remark, '保留备注')
|
||
assert.equal(s.form.value.entryMode, 'manual')
|
||
assert.equal(s.form.value.orderNum, 3)
|
||
assert.equal(s.form.value.status, 'active')
|
||
assert.equal(s.form.value.attachmentUrls, '')
|
||
assert.equal(s.form.value.referencesEditable, false)
|
||
assert.equal(s.edited.has('certNo'), false)
|
||
assert.deepEqual(s.bindingContracts.value, [])
|
||
assert.deepEqual(s.lockedPlates.value, [])
|
||
assert.deepEqual(s.linkedRefIds.value, [])
|
||
assert.equal(s.bindingEpoch.value, 1)
|
||
const archive = readFileSync(
|
||
new URL('../src/views/m/pl/plan-cert/archive-form.vue', import.meta.url),
|
||
'utf8'
|
||
)
|
||
assert.match(archive, /onCertificateAttachmentChange/)
|
||
assert.match(archive, /clearCertAfterAttachmentRemoved/)
|
||
} finally {
|
||
h.stop()
|
||
}
|
||
})
|