提交
This commit is contained in:
1 parent
b9ff9c9d67
commit
44749ae716
87 files changed
+35089
-21918
No files matched your search
@@ -0,0 +1,67 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import {
|
||||
CRM_INVOICE_PASTE_SAMPLE_ANQING,
|
||||
CRM_INVOICE_PASTE_SAMPLE_JIANGFENG,
|
||||
parseCrmInvoicePaste,
|
||||
mergeInvoiceRemark
|
||||
} from './crm-invoice-paste-recognize'
|
||||
|
||||
describe('parseCrmInvoicePaste · 安庆山峡样例', () => {
|
||||
it('maps core fields and packs extra into remarkParts', () => {
|
||||
const r = parseCrmInvoicePaste(CRM_INVOICE_PASTE_SAMPLE_ANQING)
|
||||
assert.equal(r.companyName, '安庆市山峡化工有限公司')
|
||||
assert.equal(r.taxNo, '9134080006084212XH')
|
||||
assert.equal(r.bankName, '中国建设银行安庆分行城中支行')
|
||||
assert.equal(r.bankAccount, '34050168860800000121')
|
||||
assert.equal(r.companyAddress, '安徽省安庆市高新区霞虹路5号')
|
||||
assert.equal(r.phoneFax, '0556-5348918')
|
||||
assert.ok(r.filledCount >= 6)
|
||||
assert.ok(r.remarkParts.some((p) => p.includes('银行行号') && p.includes('105368000056')))
|
||||
assert.ok(r.remarkParts.some((p) => p.includes('邮编') && p.includes('246000')))
|
||||
assert.ok(r.remarkParts.some((p) => p.includes('邮寄') && p.includes('汪璇')))
|
||||
// 邮寄块内地址不得覆盖单位地址
|
||||
assert.equal(r.companyAddress, '安徽省安庆市高新区霞虹路5号')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCrmInvoicePaste · 江峰生物样例', () => {
|
||||
it('handles spaced labels, spaced account, and 地址及电话', () => {
|
||||
const r = parseCrmInvoicePaste(CRM_INVOICE_PASTE_SAMPLE_JIANGFENG)
|
||||
assert.equal(r.companyName, '安徽江峰生物科技有限公司')
|
||||
assert.equal(r.taxNo, '91340200MA8PYJNB8X')
|
||||
assert.equal(r.bankName, '中国建设银行芜湖经开区支行')
|
||||
assert.equal(r.bankAccount, '34050167880800002481')
|
||||
assert.ok((r.companyAddress || '').includes('安徽省芜湖市'))
|
||||
assert.ok(!(r.companyAddress || '').includes('0553'))
|
||||
assert.equal(r.phoneFax, '0553-5201019')
|
||||
assert.ok(r.remarkParts.some((p) => p.includes('银行行号') && p.includes('105362050066')))
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseCrmInvoicePaste · 边界', () => {
|
||||
it('returns empty filledCount for blank text', () => {
|
||||
const r = parseCrmInvoicePaste(' \n ')
|
||||
assert.equal(r.filledCount, 0)
|
||||
assert.ok(r.warnings.length > 0)
|
||||
})
|
||||
|
||||
it('does not treat 名称 inside unrelated prose without colon as company', () => {
|
||||
const r = parseCrmInvoicePaste('请查收开票资料,谢谢')
|
||||
assert.equal(r.filledCount, 0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mergeInvoiceRemark', () => {
|
||||
it('appends new parts without duplicating existing', () => {
|
||||
const next = mergeInvoiceRemark('已有备注', ['银行行号:105', '邮编:246000'])
|
||||
assert.equal(next, '已有备注;银行行号:105;邮编:246000')
|
||||
const again = mergeInvoiceRemark(next, ['银行行号:105', '邮寄:张三'])
|
||||
assert.equal(again, '已有备注;银行行号:105;邮编:246000;邮寄:张三')
|
||||
})
|
||||
|
||||
it('returns existing when parts empty', () => {
|
||||
assert.equal(mergeInvoiceRemark('x', []), 'x')
|
||||
assert.equal(mergeInvoiceRemark('', ['a']), 'a')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,167 @@
|
||||
export const CRM_INVOICE_PASTE_SAMPLE_ANQING = `开票资料:
|
||||
公司名称:安庆市山峡化工有限公司
|
||||
纳税人识别号:9134080006084212XH
|
||||
开户行:中国建设银行安庆分行城中支行
|
||||
帐号:34050168860800000121
|
||||
银行行号:105368000056
|
||||
地址:安徽省安庆市高新区霞虹路5号
|
||||
电话/传真:0556-5348918
|
||||
邮编:246000
|
||||
发票邮寄地址:
|
||||
姓名:汪璇
|
||||
电话:18895395317
|
||||
地址:安徽省安庆市高新区霞虹路5号`
|
||||
|
||||
export const CRM_INVOICE_PASTE_SAMPLE_JIANGFENG = `单位名:安徽江峰生物科技有限公司
|
||||
税 号:91340200MA8PYJNB8X
|
||||
地址及电话:安徽省芜湖市皖江江北新兴产业集中区沈巷片区起步区皖跃路与和睦路交叉口西南角福州路4号
|
||||
银行开户行:中国建设银行芜湖经开区支行
|
||||
账 号:3405 0167 8808 0000 2481
|
||||
银行行号:1053 6205 0066
|
||||
电话:0553-5201019`
|
||||
|
||||
export interface CrmInvoicePasteResult {
|
||||
companyName?: string
|
||||
taxNo?: string
|
||||
companyAddress?: string
|
||||
legalRepresentative?: string
|
||||
agent?: string
|
||||
phoneFax?: string
|
||||
bankName?: string
|
||||
bankAccount?: string
|
||||
remarkParts: string[]
|
||||
warnings: string[]
|
||||
filledCount: number
|
||||
}
|
||||
|
||||
function normalizeSpaces(s: string): string {
|
||||
return (s || '').replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function splitMailBlock(text: string): { main: string; mail: string } {
|
||||
const idx = text.search(/发票邮寄地址\s*[::]?/)
|
||||
if (idx < 0) return { main: text, mail: '' }
|
||||
return {
|
||||
main: text.slice(0, idx),
|
||||
mail: text.slice(idx).replace(/^发票邮寄地址\s*[::]?/, '')
|
||||
}
|
||||
}
|
||||
|
||||
function pickLine(block: string, labelRe: RegExp): string {
|
||||
const lines = block.split(/\r?\n/)
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim()
|
||||
if (!line) continue
|
||||
const m = line.match(labelRe)
|
||||
if (m && m[1] != null) return normalizeSpaces(m[1])
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const PHONE_TAIL =
|
||||
/((?:0\d{2,3}-?\d{7,8})|(?:1\d{10}))\s*$/
|
||||
|
||||
export function parseCrmInvoicePaste(raw: string): CrmInvoicePasteResult {
|
||||
const warnings: string[] = []
|
||||
const text = (raw || '').replace(/\r\n/g, '\n').trim()
|
||||
if (!text) {
|
||||
return { remarkParts: [], warnings: ['请先粘贴开票资料文本'], filledCount: 0 }
|
||||
}
|
||||
|
||||
const { main, mail } = splitMailBlock(text)
|
||||
const remarkParts: string[] = []
|
||||
|
||||
const companyName = pickLine(
|
||||
main,
|
||||
/^(?:公司名称|单位名称|单位名|名称)\s*[::]\s*(.+)$/
|
||||
)
|
||||
const taxNo = pickLine(
|
||||
main,
|
||||
/^(?:纳税人识别号|纳税识别号|统一社会信用代码|税\s*号|税号)\s*[::]\s*([0-9A-Za-z]+)\s*$/
|
||||
)
|
||||
const bankName = pickLine(
|
||||
main,
|
||||
/^(?:银行开户行|开户银行|开户行)\s*[::]\s*(.+)$/
|
||||
)
|
||||
let bankAccount = pickLine(
|
||||
main,
|
||||
/^(?:银行账号|账\s*号|帐\s*号|账号|帐号)\s*[::]\s*([0-9\s]+)\s*$/
|
||||
)
|
||||
if (bankAccount) bankAccount = bankAccount.replace(/\s+/g, '')
|
||||
|
||||
let companyAddress = ''
|
||||
let phoneFax = ''
|
||||
|
||||
const addrPhone = pickLine(main, /^(?:地址及电话)\s*[::]\s*(.+)$/)
|
||||
if (addrPhone) {
|
||||
const m = addrPhone.match(PHONE_TAIL)
|
||||
if (m && m.index != null) {
|
||||
phoneFax = m[1]
|
||||
companyAddress = normalizeSpaces(addrPhone.slice(0, m.index))
|
||||
} else {
|
||||
companyAddress = addrPhone
|
||||
}
|
||||
} else {
|
||||
companyAddress = pickLine(main, /^(?:单位地址|地\s*址|地址)\s*[::]\s*(.+)$/)
|
||||
}
|
||||
|
||||
if (!phoneFax) {
|
||||
phoneFax =
|
||||
pickLine(main, /^(?:电话\/传真|电话传真|传真)\s*[::]\s*(.+)$/) ||
|
||||
pickLine(main, /^(?:电\s*话|电话)\s*[::]\s*(.+)$/)
|
||||
}
|
||||
|
||||
const legalRepresentative = pickLine(
|
||||
main,
|
||||
/^(?:法定代表人|法人代表|法人)\s*[::]\s*(.+)$/
|
||||
)
|
||||
const agent = pickLine(main, /^(?:委托代理人|代理人)\s*[::]\s*(.+)$/)
|
||||
|
||||
const bankCode = pickLine(main, /^(?:银行行号|联行号|行号)\s*[::]\s*([0-9\s]+)\s*$/)
|
||||
if (bankCode) remarkParts.push(`银行行号:${bankCode.replace(/\s+/g, '')}`)
|
||||
|
||||
const zip = pickLine(main, /^(?:邮\s*编|邮编)\s*[::]\s*(\d{6})\s*$/)
|
||||
if (zip) remarkParts.push(`邮编:${zip}`)
|
||||
|
||||
if (mail.trim()) {
|
||||
const name = pickLine(mail, /^(?:姓\s*名|姓名)\s*[::]\s*(.+)$/)
|
||||
const phone = pickLine(mail, /^(?:电\s*话|电话|手机)\s*[::]\s*(.+)$/)
|
||||
const addr = pickLine(mail, /^(?:地\s*址|地址)\s*[::]\s*(.+)$/)
|
||||
const bits = [name && `收件人 ${name}`, phone, addr].filter(Boolean) as string[]
|
||||
if (bits.length) remarkParts.push(`邮寄:${bits.join(' / ')}`)
|
||||
}
|
||||
|
||||
const fields = {
|
||||
companyName: companyName || undefined,
|
||||
taxNo: taxNo || undefined,
|
||||
companyAddress: companyAddress || undefined,
|
||||
legalRepresentative: legalRepresentative || undefined,
|
||||
agent: agent || undefined,
|
||||
phoneFax: phoneFax || undefined,
|
||||
bankName: bankName || undefined,
|
||||
bankAccount: bankAccount || undefined
|
||||
}
|
||||
|
||||
const filledCount = Object.values(fields).filter(Boolean).length
|
||||
if (!companyName) warnings.push('未识别到单位名称')
|
||||
if (!taxNo) warnings.push('未识别到税号')
|
||||
if (filledCount === 0) warnings.push('未能识别任何开票字段,请检查格式或手动填写')
|
||||
|
||||
return { ...fields, remarkParts, warnings, filledCount }
|
||||
}
|
||||
|
||||
export function mergeInvoiceRemark(existing: string | undefined, parts: string[]): string {
|
||||
const base = (existing || '').trim()
|
||||
const extras = (parts || []).map((p) => p.trim()).filter(Boolean)
|
||||
if (!extras.length) return base
|
||||
let next = base
|
||||
for (const p of extras) {
|
||||
if (!next) {
|
||||
next = p
|
||||
continue
|
||||
}
|
||||
if (next.includes(p)) continue
|
||||
next = `${next};${p}`
|
||||
}
|
||||
return next
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
import {
|
||||
collectSelfVehicleRequiredCertErrors,
|
||||
collectCarrierVehicleRequiredCertErrors,
|
||||
ensureSelfVehicleCertSlots,
|
||||
ensureCarrierVehicleCertSlots
|
||||
} from './pl-cert-slots'
|
||||
import {
|
||||
isHuolalaTrailerNo,
|
||||
shouldRequireVehicleTrailerCerts,
|
||||
shouldShowVehicleTrailerCertSlots,
|
||||
PL_VEHICLE_CERT_PART_TRAILER
|
||||
} from './pl-vehicle-cert'
|
||||
|
||||
function completeSelfHeadCerts() {
|
||||
return [
|
||||
{ certName: '行驶证', expireDate: '2030-01-01', vehiclePart: 'head' },
|
||||
{ certName: '营运证', expireDate: '2030-01-01', vehiclePart: 'head' },
|
||||
{ certName: '保险单', expireDate: '2030-01-01', vehiclePart: 'head' }
|
||||
]
|
||||
}
|
||||
|
||||
describe('trailer cert rule helpers', () => {
|
||||
it('detects 货拉拉 and gates trailer slot/require', () => {
|
||||
assert.equal(isHuolalaTrailerNo('货拉拉'), true)
|
||||
assert.equal(isHuolalaTrailerNo(' 货拉拉 '), true)
|
||||
assert.equal(isHuolalaTrailerNo('皖LP359'), false)
|
||||
assert.equal(shouldShowVehicleTrailerCertSlots('货拉拉'), false)
|
||||
assert.equal(shouldShowVehicleTrailerCertSlots('皖LP359'), true)
|
||||
assert.equal(shouldRequireVehicleTrailerCerts('self', '货拉拉'), false)
|
||||
assert.equal(shouldRequireVehicleTrailerCerts('self', '皖LP359'), true)
|
||||
assert.equal(shouldRequireVehicleTrailerCerts('carrier', '皖LP359'), false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ensure trailer cert slots', () => {
|
||||
it('hides trailer slots for 货拉拉', () => {
|
||||
const selfSlots = ensureSelfVehicleCertSlots([], '货拉拉')
|
||||
assert.equal(
|
||||
selfSlots.some(c => c.vehiclePart === PL_VEHICLE_CERT_PART_TRAILER),
|
||||
false
|
||||
)
|
||||
const carrierSlots = ensureCarrierVehicleCertSlots([], '货拉拉')
|
||||
assert.equal(
|
||||
carrierSlots.some(c => c.vehiclePart === PL_VEHICLE_CERT_PART_TRAILER),
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('shows required trailer slots for self normal trailer', () => {
|
||||
const slots = ensureSelfVehicleCertSlots([], '皖LP359')
|
||||
const trailer = slots.filter(c => c.vehiclePart === PL_VEHICLE_CERT_PART_TRAILER)
|
||||
assert.ok(trailer.length > 0)
|
||||
assert.ok(trailer.every(c => c._required === true))
|
||||
})
|
||||
|
||||
it('shows optional trailer slots for carrier normal trailer', () => {
|
||||
const slots = ensureCarrierVehicleCertSlots([], '皖LP359')
|
||||
const trailer = slots.filter(c => c.vehiclePart === PL_VEHICLE_CERT_PART_TRAILER)
|
||||
assert.ok(trailer.length > 0)
|
||||
assert.ok(trailer.every(c => c._required === false))
|
||||
})
|
||||
})
|
||||
|
||||
describe('collect trailer cert errors', () => {
|
||||
it('skips trailer validation for self 货拉拉', () => {
|
||||
const errors = collectSelfVehicleRequiredCertErrors(completeSelfHeadCerts(), '货拉拉')
|
||||
assert.deepEqual(errors, [])
|
||||
})
|
||||
|
||||
it('requires trailer certs for self normal trailer', () => {
|
||||
const errors = collectSelfVehicleRequiredCertErrors(completeSelfHeadCerts(), '皖LP359')
|
||||
assert.ok(errors.some(e => e.includes('挂车')))
|
||||
})
|
||||
|
||||
it('does not require trailer certs for carrier', () => {
|
||||
const errors = collectCarrierVehicleRequiredCertErrors([], '皖LP359')
|
||||
assert.deepEqual(errors, [])
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,9 @@ import {
|
||||
type PlVehicleCertCheckType,
|
||||
getVehicleHeadRequiredCertNames,
|
||||
getVehicleTrailerRequiredCertNames,
|
||||
normalizeVehicleCertPart
|
||||
normalizeVehicleCertPart,
|
||||
shouldRequireVehicleTrailerCerts,
|
||||
shouldShowVehicleTrailerCertSlots
|
||||
} from '@/utils/pl-vehicle-cert'
|
||||
|
||||
export type CertRowWithMeta = CertRow & { _key?: string; _required?: boolean }
|
||||
@@ -75,6 +77,11 @@ export function collectVehicleRequiredCertErrors(
|
||||
trailerNo: string | null | undefined,
|
||||
vehicleType: PlVehicleCertCheckType
|
||||
): string[] {
|
||||
// 三方车辆证件不强制
|
||||
if (vehicleType === 'carrier') {
|
||||
return []
|
||||
}
|
||||
|
||||
const headCerts = certList.filter(
|
||||
c => normalizeVehicleCertPart(c.vehiclePart) === PL_VEHICLE_CERT_PART_HEAD
|
||||
)
|
||||
@@ -87,7 +94,7 @@ export function collectVehicleRequiredCertErrors(
|
||||
getVehicleHeadRequiredCertNames(vehicleType),
|
||||
'主车'
|
||||
),
|
||||
...(trailerNo?.trim()
|
||||
...(shouldRequireVehicleTrailerCerts(vehicleType, trailerNo)
|
||||
? collectRequiredCertSlotErrors(
|
||||
trailerCerts,
|
||||
getVehicleTrailerRequiredCertNames(vehicleType),
|
||||
@@ -131,10 +138,11 @@ function findCertIndexForRequired(
|
||||
export function mergeCertListWithRequiredSlots(
|
||||
existing: CertRowWithMeta[],
|
||||
requiredNames: readonly string[],
|
||||
options?: { vehiclePart?: string; stripManagedNames?: readonly string[] }
|
||||
options?: { vehiclePart?: string; stripManagedNames?: readonly string[]; markRequired?: boolean }
|
||||
): CertRowWithMeta[] {
|
||||
const vehiclePart = options?.vehiclePart
|
||||
const stripManagedNames = options?.stripManagedNames
|
||||
const markRequired = options?.markRequired !== false
|
||||
const normalizedPart = vehiclePart ? normalizeVehicleCertPart(vehiclePart) : null
|
||||
|
||||
let source = existing
|
||||
@@ -167,13 +175,13 @@ export function mergeCertListWithRequiredSlots(
|
||||
...next[idx],
|
||||
certName: requiredName,
|
||||
vehiclePart: normalizedPart || next[idx].vehiclePart || PL_VEHICLE_CERT_PART_HEAD,
|
||||
_required: true
|
||||
_required: markRequired
|
||||
}
|
||||
}
|
||||
return createEmptyCertRow(
|
||||
requiredName,
|
||||
normalizedPart || PL_VEHICLE_CERT_PART_HEAD,
|
||||
true
|
||||
markRequired
|
||||
)
|
||||
})
|
||||
|
||||
@@ -187,18 +195,26 @@ export function mergeCertListWithRequiredSlots(
|
||||
function ensureVehicleCertSlotsByNames(
|
||||
certList: CertRowWithMeta[],
|
||||
trailerNo: string | null | undefined,
|
||||
headRequired: readonly string[],
|
||||
trailerRequired: readonly string[]
|
||||
headNames: readonly string[],
|
||||
trailerNames: readonly string[],
|
||||
options?: { headRequired?: boolean; trailerRequired?: boolean }
|
||||
): CertRowWithMeta[] {
|
||||
let merged = mergeCertListWithRequiredSlots(certList, headRequired, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD
|
||||
const headRequired = options?.headRequired !== false
|
||||
const trailerRequired = Boolean(options?.trailerRequired)
|
||||
let merged = mergeCertListWithRequiredSlots(certList, headNames, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_HEAD,
|
||||
markRequired: headRequired
|
||||
})
|
||||
if (trailerNo?.trim()) {
|
||||
merged = mergeCertListWithRequiredSlots(merged, trailerRequired, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER
|
||||
if (shouldShowVehicleTrailerCertSlots(trailerNo)) {
|
||||
merged = mergeCertListWithRequiredSlots(merged, trailerNames, {
|
||||
vehiclePart: PL_VEHICLE_CERT_PART_TRAILER,
|
||||
markRequired: trailerRequired
|
||||
})
|
||||
} else {
|
||||
merged = stripTrailerRequiredSlots(merged, trailerRequired)
|
||||
// 无挂车号或货拉拉:移除全部挂车侧证书行
|
||||
merged = merged.filter(
|
||||
cert => normalizeVehicleCertPart(cert.vehiclePart) !== PL_VEHICLE_CERT_PART_TRAILER
|
||||
)
|
||||
}
|
||||
return merged
|
||||
}
|
||||
@@ -211,7 +227,11 @@ export function ensureSelfVehicleCertSlots(
|
||||
certList,
|
||||
trailerNo,
|
||||
PL_SELF_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
PL_SELF_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
{
|
||||
headRequired: true,
|
||||
trailerRequired: shouldRequireVehicleTrailerCerts('self', trailerNo)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -223,7 +243,12 @@ export function ensureCarrierVehicleCertSlots(
|
||||
certList,
|
||||
trailerNo,
|
||||
PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES,
|
||||
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES
|
||||
PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES,
|
||||
{
|
||||
// 三方主车/挂车证件均展示槽位但不强制
|
||||
headRequired: false,
|
||||
trailerRequired: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { isPlInternalCustomerName } from '@/utils/pl-internal-customer'
|
||||
|
||||
export type PlanContractDisplayLine = {
|
||||
side: string
|
||||
no: string
|
||||
pending?: boolean
|
||||
}
|
||||
|
||||
type PlanContractLike = {
|
||||
contractId?: string
|
||||
contractNo?: string
|
||||
}
|
||||
|
||||
type PlanContractParties = {
|
||||
supplierName?: string | null
|
||||
customerName?: string | null
|
||||
supplierContracts?: PlanContractLike[]
|
||||
demanderContracts?: PlanContractLike[]
|
||||
}
|
||||
|
||||
/** 单侧:有编号显示编号;非内置缺合同「待补充」;内置缺合同「—」 */
|
||||
export function planContractSideLines(
|
||||
side: '供' | '需',
|
||||
contracts: PlanContractLike[] | undefined,
|
||||
customerName?: string | null
|
||||
): PlanContractDisplayLine[] {
|
||||
const lines: PlanContractDisplayLine[] = []
|
||||
for (const item of contracts || []) {
|
||||
const no = item.contractNo || item.contractId
|
||||
if (no) lines.push({ side, no: String(no) })
|
||||
}
|
||||
if (lines.length) return lines
|
||||
if (isPlInternalCustomerName(customerName)) {
|
||||
return [{ side, no: '—' }]
|
||||
}
|
||||
return [{ side, no: '待补充', pending: true }]
|
||||
}
|
||||
|
||||
/** 合同列:供/需各至少一行 */
|
||||
export function planContractLines(plan: PlanContractParties): PlanContractDisplayLine[] {
|
||||
return [
|
||||
...planContractSideLines('供', plan.supplierContracts, plan.supplierName),
|
||||
...planContractSideLines('需', plan.demanderContracts, plan.customerName)
|
||||
]
|
||||
}
|
||||
@@ -25,8 +25,29 @@ export const PL_CARRIER_VEHICLE_HEAD_REQUIRED_CERT_NAMES = ['行驶证', '营运
|
||||
|
||||
export const PL_CARRIER_VEHICLE_TRAILER_REQUIRED_CERT_NAMES = ['行驶证', '营运证'] as const
|
||||
|
||||
/** 特殊挂车号:不展示挂车证件槽位、不校验挂车证件 */
|
||||
export const PL_HUOLALA_TRAILER_NO = '货拉拉'
|
||||
|
||||
export type PlVehicleCertCheckType = 'self' | 'carrier'
|
||||
|
||||
export function isHuolalaTrailerNo(trailerNo?: string | null): boolean {
|
||||
return (trailerNo || '').trim() === PL_HUOLALA_TRAILER_NO
|
||||
}
|
||||
|
||||
/** 有挂车号且非货拉拉时展示挂车证件区 */
|
||||
export function shouldShowVehicleTrailerCertSlots(trailerNo?: string | null): boolean {
|
||||
const text = (trailerNo || '').trim()
|
||||
return Boolean(text) && !isHuolalaTrailerNo(text)
|
||||
}
|
||||
|
||||
/** 仅山峡自有 + 正常挂车号时强制挂车证件 */
|
||||
export function shouldRequireVehicleTrailerCerts(
|
||||
vehicleType: PlVehicleCertCheckType,
|
||||
trailerNo?: string | null
|
||||
): boolean {
|
||||
return vehicleType === 'self' && shouldShowVehicleTrailerCertSlots(trailerNo)
|
||||
}
|
||||
|
||||
export const PL_VEHICLE_CERT_NAME_OPTIONS = [
|
||||
'行驶证',
|
||||
'营运证',
|
||||
@@ -126,7 +147,11 @@ export function hasVehicleCertMissing(options: {
|
||||
}): boolean {
|
||||
const { vehicleType, certList = [], trailerNo } = options
|
||||
const { headCerts, trailerCerts } = splitVehicleCertsByPart(certList)
|
||||
const hasTrailer = Boolean(trailerNo?.trim())
|
||||
|
||||
// 三方车辆证件不强制,列表缺失后缀仅对自有生效
|
||||
if (vehicleType === 'carrier') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
getMissingVehiclePartCertNames(headCerts, PL_VEHICLE_CERT_PART_HEAD, vehicleType).length > 0
|
||||
@@ -135,7 +160,7 @@ export function hasVehicleCertMissing(options: {
|
||||
}
|
||||
|
||||
if (
|
||||
hasTrailer &&
|
||||
shouldRequireVehicleTrailerCerts(vehicleType, trailerNo) &&
|
||||
getMissingVehiclePartCertNames(trailerCerts, PL_VEHICLE_CERT_PART_TRAILER, vehicleType)
|
||||
.length > 0
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user