diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs index d7d6a93..ec768bb 100644 --- a/src/modules/first-leg/utils/firstLegOrder.test.mjs +++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs @@ -185,10 +185,15 @@ test('counts one physical carton series for SKU lines in the same mixed-carton g const first = { ...validDraft().items[0], packingGroupNo: 'A', packageCount: 10, pieces: 20, unitsPerPackage: 2 } const second = { ...first, id: 'cargo-2', sku: 'SKU-002', pieces: 30, unitsPerPackage: 3 } - assert.equal(summarizeFirstLegCargo([first, second]).packageCount, 10) + assert.deepEqual(summarizeFirstLegCargo([first, second]), { + pieces: 50, + packageCount: 10, + grossWeightKg: first.grossWeightKg, + volumeCbm: first.volumeCbm, + }) const draft = { ...validDraft(), fulfillmentType: 'ONE_CLICK_DROPSHIP', items: [first, second] } - assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('混箱组合'))) + assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('同箱 SKU'))) second.packageCount = 9 - assert.ok(validateFirstLegOrderDraft(draft).includes('混箱组合 A 的箱数、包装类型和箱规必须一致')) + assert.ok(validateFirstLegOrderDraft(draft).includes('同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致')) }) diff --git a/src/modules/first-leg/utils/firstLegOrder.ts b/src/modules/first-leg/utils/firstLegOrder.ts index 708ce27..169679c 100644 --- a/src/modules/first-leg/utils/firstLegOrder.ts +++ b/src/modules/first-leg/utils/firstLegOrder.ts @@ -150,8 +150,10 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) { const first = groups.get(group) if (!first) groups.set(group, item) else if (first.packageCount !== item.packageCount || first.packageType !== item.packageType - || first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm) { - errors.push(`混箱组合 ${group} 的箱数、包装类型和箱规必须一致`) + || first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm + || first.grossWeightKg !== item.grossWeightKg || first.netWeightKg !== item.netWeightKg + || first.volumeCbm !== item.volumeCbm) { + errors.push(`同箱 SKU 的箱数、包装类型、箱规、重量和体积必须一致`) } }) } @@ -164,13 +166,14 @@ export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) { return items.reduce( (summary, item) => { const group = item.packingGroupNo.trim() - const packageCount = group && countedGroups.has(group) ? 0 : item.packageCount + const alreadyCounted = Boolean(group && countedGroups.has(group)) + const packageCount = alreadyCounted ? 0 : item.packageCount if (group) countedGroups.add(group) return ({ pieces: summary.pieces + item.pieces, packageCount: summary.packageCount + packageCount, - grossWeightKg: summary.grossWeightKg + item.grossWeightKg, - volumeCbm: summary.volumeCbm + item.volumeCbm, + grossWeightKg: summary.grossWeightKg + (alreadyCounted ? 0 : item.grossWeightKg), + volumeCbm: summary.volumeCbm + (alreadyCounted ? 0 : item.volumeCbm), }) }, { pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 }, diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 93336b5..eea643e 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -357,6 +357,13 @@ test('distills order creation into the confirmed prototype sections with server assert.doesNotMatch(source, /未填写行标识/) assert.match(source, /batchImportError[\s\S]*requestId[\s\S]*traceId/) assert.match(source, /添加行[\s\S]*删除所选行/) + assert.match(source, /添加 SKU/) + assert.match(source, /addSkuToPackingGroup/) + assert.match(source, /shared-carton-value">同箱/) + assert.match(source, /syncSkuQuantity/) + assert.match(source, /每个箱组默认装一个 SKU/) + assert.doesNotMatch(source, /v-model="item\.packingGroupNo"/) + assert.doesNotMatch(cargoTableSource, /]*>混箱组合/) assert.doesNotMatch(source, /货物明细导入模板\.xls/) for (const heading of ['SKU NO.', '中文品名', '箱规长 cm', '总毛重', '报关 HS CODE', '货件编号', '跟踪编号', 'UPC NO.']) { assert.ok(cargoTableSource.includes(heading)) diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index bbabecd..83de19d 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -215,6 +215,7 @@ const destinationWarehousesLoading = ref(false) const pageDataLoading = computed(() => supportingDataLoading.value || (!isCreatePage.value && store.loading)) const editorBaseline = ref('') let cargoSequence = 0 +let packingGroupSequence = 0 let inboundBillingRequestSequence = 0 let channelRequestSequence = 0 let estimateRequestSequence = 0 @@ -1332,6 +1333,71 @@ function addCargoItem() { resetEditorValidation() } +const sharedPackingFields = [ + 'packageType', 'packageCount', 'lengthCm', 'widthCm', 'heightCm', + 'grossWeightKg', 'netWeightKg', 'volumeCbm', +] as const + +function packingGroupFor(item: FirstLegCargoItem) { + return item.packingGroupNo.trim() +} + +function mixedSkuCount(item: FirstLegCargoItem) { + const group = packingGroupFor(item) + return group ? form.items.filter((candidate) => packingGroupFor(candidate) === group).length : 1 +} + +function isAdditionalMixedSku(item: FirstLegCargoItem, index: number) { + const group = packingGroupFor(item) + return Boolean(group && form.items.slice(0, index).some((candidate) => packingGroupFor(candidate) === group)) +} + +function syncSkuQuantity(item: FirstLegCargoItem) { + if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0 + && Number.isInteger(item.packageCount) && item.packageCount > 0) { + item.pieces = item.unitsPerPackage * item.packageCount + } +} + +function syncPackingGroupFields(source: FirstLegCargoItem) { + if (form.fulfillmentType !== 'ONE_CLICK_DROPSHIP') return + syncSkuQuantity(source) + const group = packingGroupFor(source) + if (!group) return + form.items.forEach((item) => { + if (item === source || packingGroupFor(item) !== group) return + sharedPackingFields.forEach((field) => { item[field] = source[field] as never }) + syncSkuQuantity(item) + }) +} + +function normalizeSingleSkuGroups() { + const counts = new Map() + form.items.forEach((item) => { + const group = packingGroupFor(item) + if (group) counts.set(group, (counts.get(group) ?? 0) + 1) + }) + form.items.forEach((item) => { + const group = packingGroupFor(item) + if (group && counts.get(group) === 1) item.packingGroupNo = '' + }) +} + +function addSkuToPackingGroup(index: number) { + const source = form.items[index] + if (!source) return + const group = packingGroupFor(source) || `PG-${Date.now().toString(36)}-${++packingGroupSequence}` + source.packingGroupNo = group + const added = createEmptyCargoItem(`new-cargo-${++cargoSequence}`) + added.packingGroupNo = group + sharedPackingFields.forEach((field) => { added[field] = source[field] as never }) + syncSkuQuantity(added) + let insertAt = index + 1 + while (insertAt < form.items.length && packingGroupFor(form.items[insertAt]!) === group) insertAt++ + form.items.splice(insertAt, 0, added) + resetEditorValidation() +} + function removeCargoItem(index: number) { if (form.items.length === 1) { ElMessage.warning('订单至少需要保留一条货物明细') @@ -1339,6 +1405,7 @@ function removeCargoItem(index: number) { } const [removed] = form.items.splice(index, 1) if (removed) releaseProductImagePreviews([removed]) + normalizeSingleSkuGroups() resetEditorValidation() } @@ -1503,6 +1570,7 @@ function removeSelectedCargoItems() { const selectedIds = new Set(selectedCargoIds.value) const removedItems = form.items.filter((item) => selectedIds.has(item.id)) form.items.splice(0, form.items.length, ...form.items.filter((item) => !selectedIds.has(item.id))) + normalizeSingleSkuGroups() releaseProductImagePreviews(removedItems) selectedCargoIds.value = [] resetEditorValidation() @@ -1969,32 +2037,31 @@ async function searchOrders(model: QueryModel) { 可逐行维护货物明细。
添加行删除所选行
-
+
- + - + - - + - + - - + + - +
#SKU NO.SKU必填混箱组合同号为同一批箱中文品名图片英文品名报关 HS CODE清关 HS CODE包装类型材质危险品木制品勾选后须商检SKU NO.SKU必填中文品名图片英文品名报关 HS CODE清关 HS CODE包装类型材质危险品木制品勾选后须商检 箱规长 cm箱规宽 cm箱规高 cm每箱数量箱数总毛重总体积总数量总净重 清关申报单价 USD清关申报金额 USD用途型号品牌产品规格 长×宽×高货件编号货件编号必填跟踪编号跟踪编号必填UPC NO.备注操作
{{ index + 1 }}
同箱
同箱
第 {{ index + 1 }} 条须商检
同上同上同上同箱同箱同箱{{ item.pieces }}同箱
××
删除添加 SKU删除
-
相同混箱组合号表示相同一批物理箱;组内箱数、包装类型和箱规必须一致,物理箱数只计算一次。SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填
+
每个箱组默认装一个 SKU;同一箱内有多个 SKU 时点击“添加 SKU”,箱数、箱规、重量和体积只填写一次。SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填
@@ -2430,6 +2497,12 @@ async function searchOrders(model: QueryModel) { .required-field-mark { margin-right: 2px; color: var(--el-color-danger); } .cargo-dimensions-column { width: 240px !important; } .cargo-operation-column { width: 76px !important; text-align: center !important; } +.cargo-table-wrap.is-dropship .cargo-operation-column { width: 148px !important; } +.cargo-sku-input { position: relative; width: 100%; } +.mixed-sku-badge { position: absolute; top: -10px; right: 2px; padding: 1px 5px; border-radius: 999px; background: #eaf6f2; color: var(--primary); font-size: 9px; font-weight: 650; line-height: 16px; white-space: nowrap; } +.mixed-sku-detail-row { background: #f8fbff; } +.shared-carton-value { display: inline-flex; min-height: 30px; align-items: center; color: #8a97a8; font-size: 12px; white-space: nowrap; } +.derived-sku-value { display: inline-flex; min-height: 30px; align-items: center; color: #172b4d; font-weight: 650; } .cargo-switch-column { width: 112px !important; text-align: center !important; vertical-align: middle !important; } .cargo-switch-cell { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 4px; } .cargo-switch-cell small { color: #b54708; font-size: 9px; line-height: 1.25; white-space: normal; }