提交
This commit is contained in:
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'])
|
||||
})
|
||||
})
|
||||
@@ -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('')
|
||||
|
||||
Reference in new issue
Block a user