This commit is contained in:
wuzilong committed 2026-09-09 13:03:02 +08:00
1 parent 4529166e16
commit 05559def40
268 files changed
+15319 -15336

No files matched your search

@@ -0,0 +1,49 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
/**
* 合同编辑弹窗组件实例会复用(el-dialog destroy-on-close 只清 DOM)。
* 打开下一份合同时必须无条件用字段重建附件列表。
*
* 错误:`if (field) list = build(field)` → 无附件合同残留上一份合同的 list
* 正确:始终 `list = build(field)`(空字段 → 空列表)
*/
function syncAttachmentList(
previous: string[],
field: string | null | undefined,
build: (f: string | null | undefined) => string[]
): string[] {
return build(field)
}
function buggySyncAttachmentList(
previous: string[],
field: string | null | undefined,
build: (f: string | null | undefined) => string[]
): string[] {
if (field) return build(field)
return previous
}
const build = (field: string | null | undefined) => {
const raw = (field || '').trim()
if (!raw || raw === '[]') return []
return ['file-from-field']
}
describe('contract form attachment list sync', () => {
it('buggy conditional keeps previous list when next contract has no attachments', () => {
const previous = ['contract-a-attachment']
const next = buggySyncAttachmentList(previous, '', build)
assert.deepEqual(next, ['contract-a-attachment'])
})
it('fixed sync always replaces previous list when field is empty', () => {
const previous = ['contract-a-attachment']
assert.deepEqual(syncAttachmentList(previous, '', build), [])
assert.deepEqual(syncAttachmentList(previous, null, build), [])
assert.deepEqual(syncAttachmentList(previous, undefined, build), [])
assert.deepEqual(syncAttachmentList(previous, '[]', build), [])
assert.deepEqual(syncAttachmentList(previous, '[{"ossId":"x"}]', build), ['file-from-field'])
})
})
+32 -2
View File
@@ -1,5 +1,6 @@
/** 送(提)货单:按送货客户聚合(一车多卸) */
import type { Plan, PlanDelivery, PlanProduct, PlanStopProduct } from '@/api/pl'
import { mergeBrandQtys } from '@/utils/pl-slip-product-brand'
export interface DeliveryPrintCustomerOption {
customerId: string
@@ -59,13 +60,31 @@ export function listDeliveryPrintCustomers(plan?: Pick<Plan, 'deliveries'> | nul
}))
}
function stopProductToPlanProduct(product: PlanStopProduct, planId: string): PlanProduct {
function stopProductToPlanProduct(
product: PlanStopProduct,
planId: string,
planProducts: PlanProduct[] = []
): PlanProduct {
const planProductId = String(product.planProductId || '').trim()
const productId = String(product.productId || '').trim()
const matched =
planProducts.find((item) => planProductId && item.id === planProductId) ||
planProducts.find((item) => productId && item.productId === productId)
const stopBrandQtys = product.brandQtys
const brandQtys =
stopBrandQtys && stopBrandQtys.length ? stopBrandQtys : matched?.brandQtys
return {
id: product.id || product.planProductId || product.productId,
planId,
productId: product.productId,
productNo: product.productNo,
productName: product.productName,
brandMode: matched?.brandMode,
brandId: matched?.brandId,
brandName: matched?.brandName,
brandQtys,
model: product.model,
unit: product.unit,
packageSpec: product.packageSpec,
@@ -103,6 +122,14 @@ function mergePlanProducts(products: PlanProduct[]): PlanProduct[] {
existing.outboundEstimateQty = sum(existing.outboundEstimateQty, product.outboundEstimateQty)
existing.outboundQty = sum(existing.outboundQty, product.outboundQty)
existing.inboundQty = sum(existing.inboundQty, product.inboundQty)
existing.brandQtys = mergeBrandQtys(existing.brandQtys, product.brandQtys)
const nameA = String(existing.brandName || '').trim()
const nameB = String(product.brandName || '').trim()
if (nameA && nameB && nameA !== nameB) {
existing.brandName = undefined
} else if (!nameA && nameB) {
existing.brandName = product.brandName
}
if (!String(existing.remark || '').trim() && String(product.remark || '').trim()) {
existing.remark = product.remark
}
@@ -124,9 +151,12 @@ export function applyDeliveryCustomerToPlan(plan: Plan, customerId: string): Pla
throw new Error('未找到匹配的送货客户')
}
const primary = deliveries[0]
const planProducts = plan.products || []
const products = mergePlanProducts(
deliveries.flatMap((delivery) =>
(delivery.products || []).map((product) => stopProductToPlanProduct(product, plan.id))
(delivery.products || []).map((product) =>
stopProductToPlanProduct(product, plan.id, planProducts)
)
)
)
return {
@@ -3,6 +3,7 @@ import type { SlipPlanProduct } from '@/utils/pl-batch-print'
import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
import { getPlSlipSealSrc, type PlSlipCompanyKey } from '@/utils/pl-slip-company'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
import { expandPlanProductsForSlipPrint } from '@/utils/pl-slip-product-brand'
/** 送(提)货单打印默认公司信息(可在富文本中修改) */
export const DELIVERY_SLIP_DEFAULTS = {
@@ -206,7 +207,8 @@ function buildProductRows(
return `<tr><td class="delivery-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
}
const dataRows = products
const expanded = expandPlanProductsForSlipPrint(products, 'outbound')
const dataRows = expanded
.map((product) => {
const rowPlanNo = (product as SlipPlanProduct).slipPlanNo || planNo
const planNoText = formatDeliverySlipProductGridCellContent(rowPlanNo)
@@ -2,6 +2,7 @@ import type { Plan, PlanProduct } from '@/api/pl'
import type { SlipPlanProduct } from '@/utils/pl-batch-print'
import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
import { expandPlanProductsForSlipPrint } from '@/utils/pl-slip-product-brand'
/** 入库单打印默认公司信息(可在富文本中修改) */
export const INBOUND_SLIP_DEFAULTS = {
@@ -197,7 +198,8 @@ function buildProductRows(
return `<tr><td class="inbound-slip-product-cell" colspan="8" style="${PRODUCT_CELL}">暂无产品信息</td></tr>`
}
const dataRows = products
const expanded = expandPlanProductsForSlipPrint(products, 'inbound')
const dataRows = expanded
.map((product) => {
const rowPlanNo = (product as SlipPlanProduct).slipPlanNo || planNo
const planNoText = formatInboundSlipProductGridCellContent(rowPlanNo)
@@ -0,0 +1,130 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import type { PlanProduct } from '../api/pl'
import {
expandPlanProductsForSlipPrint,
formatSlipProductNameWithBrand,
mergeBrandQtys
} from './pl-slip-product-brand'
function baseProduct(overrides: Partial<PlanProduct> = {}): PlanProduct {
return {
id: 'pp1',
planId: 'plan1',
productId: 'p1',
productNo: 'NO1',
productName: '片碱',
unit: '吨',
isExplosive: 0,
isToxic: 0,
quantity: 20,
inboundQty: 20,
outboundQty: 20,
...overrides
}
}
describe('formatSlipProductNameWithBrand', () => {
it('appends brand in parentheses', () => {
assert.equal(formatSlipProductNameWithBrand('片碱', '红四方'), '片碱(红四方)')
})
it('skips empty brand', () => {
assert.equal(formatSlipProductNameWithBrand('片碱', ''), '片碱')
assert.equal(formatSlipProductNameWithBrand('片碱', null), '片碱')
})
it('does not double-append same brand suffix', () => {
assert.equal(formatSlipProductNameWithBrand('片碱(红四方)', '红四方'), '片碱(红四方)')
})
})
describe('expandPlanProductsForSlipPrint', () => {
it('keeps row when no brand', () => {
const rows = expandPlanProductsForSlipPrint([baseProduct()], 'inbound')
assert.equal(rows.length, 1)
assert.equal(rows[0].productName, '片碱')
assert.equal(rows[0].inboundQty, 20)
})
it('appends single brandName without splitting', () => {
const rows = expandPlanProductsForSlipPrint([baseProduct({ brandName: '红四方' })], 'inbound')
assert.equal(rows.length, 1)
assert.equal(rows[0].productName, '片碱(红四方)')
assert.equal(rows[0].inboundQty, 20)
})
it('splits brandQtys into rows with brand qty', () => {
const rows = expandPlanProductsForSlipPrint(
[
baseProduct({
brandQtys: [
{ brandId: 'b1', brandName: '红四方', qty: 10 },
{ brandId: 'b2', brandName: '君正', qty: 5 }
]
})
],
'outbound'
)
assert.equal(rows.length, 2)
assert.equal(rows[0].productName, '片碱(红四方)')
assert.equal(rows[0].outboundQty, 10)
assert.equal(rows[0].quantity, 10)
assert.equal(rows[1].productName, '片碱(君正)')
assert.equal(rows[1].outboundQty, 5)
})
it('ignores brandQtys with zero qty or empty name', () => {
const rows = expandPlanProductsForSlipPrint(
[
baseProduct({
brandName: '红四方',
brandQtys: [
{ brandId: 'b1', brandName: '红四方', qty: 0 },
{ brandId: 'b2', brandName: '', qty: 3 }
]
})
],
'inbound'
)
assert.equal(rows.length, 1)
assert.equal(rows[0].productName, '片碱(红四方)')
})
it('preserves slipPlanNo when splitting', () => {
const rows = expandPlanProductsForSlipPrint(
[
{
...baseProduct({
brandQtys: [
{ brandId: 'b1', brandName: 'A', qty: 1 },
{ brandId: 'b2', brandName: 'B', qty: 2 }
]
}),
slipPlanNo: 'JH001'
} as PlanProduct & { slipPlanNo?: string }
],
'inbound'
)
assert.equal((rows[0] as { slipPlanNo?: string }).slipPlanNo, 'JH001')
assert.equal((rows[1] as { slipPlanNo?: string }).slipPlanNo, 'JH001')
})
})
describe('mergeBrandQtys', () => {
it('sums qty by brandId', () => {
const merged = mergeBrandQtys(
[{ brandId: 'b1', brandName: '红四方', qty: 10 }],
[{ brandId: 'b1', brandName: '红四方', qty: 5 }]
)
assert.deepEqual(merged, [{ brandId: 'b1', brandName: '红四方', qty: 15 }])
})
it('keeps distinct brands', () => {
const merged = mergeBrandQtys(
[{ brandId: 'b1', brandName: '红四方', qty: 10 }],
[{ brandId: 'b2', brandName: '君正', qty: 5 }]
)
assert.equal(merged?.length, 2)
})
})
@@ -0,0 +1,113 @@
import type { PlanArchiveBrandQtyItem, PlanProduct } from '@/api/pl'
/** 打印单据产品名:有品牌则 `产品名(品牌)` */
export function formatSlipProductNameWithBrand(
productName: string,
brandName?: string | null
): string {
const name = String(productName || '').trim()
const brand = String(brandName || '').trim()
if (!brand) return name
const suffix = `(${brand})`
if (name.endsWith(suffix)) return name
return `${name}${suffix}`
}
function listValidBrandQtys(
brandQtys?: PlanArchiveBrandQtyItem[] | null
): PlanArchiveBrandQtyItem[] {
return (brandQtys || []).filter((row) => {
const qty = Number(row.qty)
const brand = String(row.brandName || '').trim()
return Number.isFinite(qty) && qty > 0 && !!brand
})
}
function applyBrandQty(
product: PlanProduct,
qty: number,
qtyMode: 'inbound' | 'outbound'
): PlanProduct {
const next: PlanProduct = {
...product,
quantity: qty,
brandQtys: undefined
}
if (qtyMode === 'inbound') {
next.inboundQty = qty
} else {
next.outboundQty = qty
}
return next
}
/**
* 打印前展开产品行:
* - 有效 brandQtys → 按品牌拆行,名称带品牌,数量用该品牌 qty
* - 仅有 brandName → 单行拼接名称
* - 皆无 → 原样浅拷贝
*/
export function expandPlanProductsForSlipPrint(
products: PlanProduct[] = [],
qtyMode: 'inbound' | 'outbound'
): PlanProduct[] {
const result: PlanProduct[] = []
for (const product of products) {
const brandRows = listValidBrandQtys(product.brandQtys)
if (brandRows.length) {
for (const row of brandRows) {
const brandName = String(row.brandName || '').trim()
const qty = Number(row.qty)
result.push(
applyBrandQty(
{
...product,
productName: formatSlipProductNameWithBrand(product.productName, brandName),
brandName,
brandId: row.brandId
},
qty,
qtyMode
)
)
}
continue
}
const brandName = String(product.brandName || '').trim()
if (brandName) {
result.push({
...product,
productName: formatSlipProductNameWithBrand(product.productName, brandName)
})
continue
}
result.push({ ...product })
}
return result
}
/** 合并同产品多段的品牌数量(按 brandId,无则按品牌名) */
export function mergeBrandQtys(
a?: PlanArchiveBrandQtyItem[] | null,
b?: PlanArchiveBrandQtyItem[] | null
): PlanArchiveBrandQtyItem[] | undefined {
const list = [...(a || []), ...(b || [])]
if (!list.length) return undefined
const map = new Map<string, PlanArchiveBrandQtyItem>()
for (const row of list) {
const brandId = String(row.brandId || '').trim()
const brandName = String(row.brandName || '').trim()
const key = brandId ? `id:${brandId}` : brandName ? `name:${brandName}` : ''
if (!key) continue
const existing = map.get(key)
if (!existing) {
map.set(key, { ...row, qty: Number(row.qty) || 0 })
continue
}
existing.qty = Number(existing.qty || 0) + Number(row.qty || 0)
if (!String(existing.brandName || '').trim() && brandName) {
existing.brandName = row.brandName
}
}
return [...map.values()]
}
@@ -1,6 +1,7 @@
import type { Plan, PlanProduct } from '@/api/pl'
import { formatRemarkForSlipHtml } from '@/utils/pl-batch-print'
import { applyPdfStableTableCellBorders } from '@/utils/pl-slip-pdf-render'
import { expandPlanProductsForSlipPrint } from '@/utils/pl-slip-product-brand'
/** 分装单打印默认公司信息(可在富文本中修改) */
export const SUBPACK_SLIP_DEFAULTS = {
@@ -211,11 +212,12 @@ function buildSubpackSlipProductRow(
}
function buildProductRows(products: PlanProduct[] = [], ctx?: SubpackSlipRenderContext): string {
const rowCount = Math.max(products.length, SUBPACK_SLIP_PRODUCT_MIN_ROWS)
const expanded = expandPlanProductsForSlipPrint(products, 'outbound')
const rowCount = Math.max(expanded.length, SUBPACK_SLIP_PRODUCT_MIN_ROWS)
const rows: string[] = []
for (let index = 0; index < rowCount; index += 1) {
rows.push(buildSubpackSlipProductRow(index + 1, products[index], ctx))
rows.push(buildSubpackSlipProductRow(index + 1, expanded[index], ctx))
}
return rows.join('')