diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.test.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.test.ts new file mode 100644 index 00000000..527dc956 --- /dev/null +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.test.ts @@ -0,0 +1,151 @@ +import type { NonIntegratedSkuExportRow } from "@/lib/non-integrated-sku-export"; + +import type { NonIntegratedSkuItem } from "../../nonIntegratedStorage"; +import { createFormValuesFromProduct } from "./productFormValueHelpers"; +import { createEmptySkuItem } from "./skuBrandHelpers"; +import { + buildSkuItemsFromImportedWorkbookRows, + getSkuWorkbookImportBatchMetadata, +} from "./skuDerivationHelpers"; + +function createSkuItem( + id: string, + batchNo: number, + skuSequence: number, + overrides: Partial = {}, +) { + return { + ...createEmptySkuItem(), + id, + batchNo, + skuSequence, + ...overrides, + }; +} + +function createImportRow( + overrides: Partial = {}, +): NonIntegratedSkuExportRow { + return { + spu: "XG0012", + model: "XG0012", + skuCode: "XG001254", + barcode: "XG001254N160V01", + setCount: "1", + specName: "测试规格", + size: "", + color: "胡桃色", + material: "实木", + boardMaterialType: "", + versionNo: "V01", + attributeCode: "", + drawingId: "", + installMethod: "", + executeStandard: "", + nationalStandardCompliant: "", + manufacturer: "", + manufacturerCode: "N160", + minimumOrderQuantity: "", + salesStatus: "", + batchNo: "批次2", + batchChangeType: "新增SKU", + dingTalkFlowNo: "DD-2", + ...overrides, + }; +} + +const formValues = { + ...createFormValuesFromProduct(null), + spu: "XG0012", +}; + +describe("SKU workbook batch isolation", () => { + it("keeps historical batches unchanged and creates independent IDs for a new batch", () => { + const batchOneItems = [ + createSkuItem("batch-1-sku-1", 1, 1, { + skuCode: "XG001251", + barcode: "XG001251N160V01", + }), + createSkuItem("batch-1-sku-2", 1, 2, { + skuCode: "XG001252", + barcode: "XG001252N160V01", + }), + ]; + const factoryConfirmedIds = new Set(batchOneItems.map((item) => item.id)); + + const result = buildSkuItemsFromImportedWorkbookRows({ + currentItems: batchOneItems, + formValues, + rows: [ + createImportRow(), + createImportRow({ + skuCode: "XG001255", + barcode: "XG001255N160V01", + }), + ], + targetBatchNo: 2, + }); + + expect(result.slice(0, 2)).toEqual(batchOneItems); + expect(result.slice(0, 2)[0]).toBe(batchOneItems[0]); + expect(result.slice(0, 2)[1]).toBe(batchOneItems[1]); + expect(result.slice(2)).toHaveLength(2); + expect(result.slice(2).every((item) => item.batchNo === 2)).toBe(true); + expect( + result.slice(2).every((item) => !factoryConfirmedIds.has(item.id)), + ).toBe(true); + }); + + it("reuses IDs only from the batch being re-imported", () => { + const historicalItem = createSkuItem("batch-1-sku-1", 1, 1); + const currentBatchItem = createSkuItem("batch-2-sku-1", 2, 2, { + skuCode: "OLD-SKU", + }); + + const result = buildSkuItemsFromImportedWorkbookRows({ + currentItems: [historicalItem, currentBatchItem], + formValues, + rows: [createImportRow({ skuCode: "UPDATED-SKU" })], + targetBatchNo: 2, + }); + + expect(result[0]).toBe(historicalItem); + expect(result[1].id).toBe(currentBatchItem.id); + expect(result[1].id).not.toBe(historicalItem.id); + expect(result[1].skuCode).toBe("UPDATED-SKU"); + }); + + it("rejects a workbook whose explicit batch differs from the active batch", () => { + expect(() => + getSkuWorkbookImportBatchMetadata({ + currentBatchCount: 2, + rows: [createImportRow({ batchNo: "批次1" })], + targetBatchNo: 2, + }), + ).toThrow("当前正在导入批次2,上传资料中包含批次1"); + }); + + it("rejects an explicit batch value that cannot be identified", () => { + expect(() => + getSkuWorkbookImportBatchMetadata({ + currentBatchCount: 2, + rows: [createImportRow({ batchNo: "第二批" })], + targetBatchNo: 2, + }), + ).toThrow("上传资料中的创建批次无法识别:第二批"); + }); + + it("uses the active batch for blank batch cells and updates only its metadata", () => { + expect( + getSkuWorkbookImportBatchMetadata({ + currentBatchCount: 2, + rows: [createImportRow({ batchNo: "" })], + targetBatchNo: 2, + }), + ).toEqual({ + batchChangeTypes: { "2": "新增SKU" }, + batchFlowNos: { "2": "DD-2" }, + nextBatchCount: 2, + }); + }); +}); diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.ts index 46a00ce9..67341c2a 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/skuDerivationHelpers.ts @@ -278,23 +278,29 @@ export function buildSkuItemsFromImportedWorkbookRows({ currentItems, formValues, rows, + targetBatchNo, }: { currentItems: NonIntegratedSkuItem[]; formValues: FormValues; rows: NonIntegratedSkuExportRow[]; + targetBatchNo: number; }) { - const nextItems = currentItems.map((item, index) => - rows[index] - ? buildSkuItemFromImportedRow( - rows[index], - item, - Number.isFinite(item.skuSequence) && item.skuSequence > 0 - ? item.skuSequence - : index + 1, - formValues, - ) - : item, - ); + if (rows.length === 0) return currentItems; + + const normalizedTargetBatchNo = Math.max(1, Math.floor(targetBatchNo)); + const isTargetBatchItem = (item: NonIntegratedSkuItem) => + (Number.isFinite(item.batchNo) && item.batchNo > 0 + ? Math.floor(item.batchNo) + : 1) === normalizedTargetBatchNo; + const targetBatchItems = currentItems.filter(isTargetBatchItem); + const firstTargetBatchIndex = currentItems.findIndex(isTargetBatchItem); + const preservedItems = currentItems.filter((item) => !isTargetBatchItem(item)); + const insertionIndex = + firstTargetBatchIndex < 0 + ? preservedItems.length + : currentItems + .slice(0, firstTargetBatchIndex) + .filter((item) => !isTargetBatchItem(item)).length; let nextSequence = currentItems.reduce( (maxSequence, item, index) => @@ -306,38 +312,84 @@ export function buildSkuItemsFromImportedWorkbookRows({ ), 0, ) + 1; - - for (let index = currentItems.length; index < rows.length; index += 1) { - nextItems.push( - buildSkuItemFromImportedRow( - rows[index], - undefined, - nextSequence, - formValues, - ), + const importedTargetBatchItems = rows.map((row, index) => { + const existingItem = targetBatchItems[index]; + const hasExistingSequence = + existingItem !== undefined && + Number.isFinite(existingItem.skuSequence) && + existingItem.skuSequence > 0; + const importedItem = buildSkuItemFromImportedRow( + row, + existingItem, + hasExistingSequence ? existingItem.skuSequence : nextSequence, + formValues, ); - nextSequence += 1; - } - return nextItems.length > 0 ? nextItems : currentItems; + if (!hasExistingSequence) nextSequence += 1; + + return { + ...importedItem, + batchNo: normalizedTargetBatchNo, + }; + }); + + return [ + ...preservedItems.slice(0, insertionIndex), + ...importedTargetBatchItems, + ...preservedItems.slice(insertionIndex), + ]; } export function getSkuWorkbookImportBatchMetadata({ - fallbackBatchNo, + currentBatchCount, rows, + targetBatchNo, }: { - fallbackBatchNo: number; + currentBatchCount: number; rows: NonIntegratedSkuExportRow[]; + targetBatchNo: number; }) { + const normalizedTargetBatchNo = Math.max(1, Math.floor(targetBatchNo)); + const explicitBatchValues = rows + .map((row) => row.batchNo.trim()) + .filter((batchNo) => batchNo.length > 0); + const invalidBatchValues = Array.from( + new Set(explicitBatchValues.filter((batchNo) => !/\d+/.test(batchNo))), + ); + if (invalidBatchValues.length > 0) { + throw new Error( + `上传资料中的创建批次无法识别:${invalidBatchValues.join("、")}。`, + ); + } + + const explicitBatchNos = explicitBatchValues.map((batchNo) => + parseImportedSkuBatchNo(batchNo, normalizedTargetBatchNo), + ); + const mismatchedBatchNos = Array.from( + new Set( + explicitBatchNos.filter( + (batchNo) => batchNo !== normalizedTargetBatchNo, + ), + ), + ); + if (mismatchedBatchNos.length > 0) { + throw new Error( + `当前正在导入批次${normalizedTargetBatchNo},上传资料中包含${mismatchedBatchNos + .map((batchNo) => `批次${batchNo}`) + .join("、")},请使用当前批次的资料。`, + ); + } + const batchFlowNos: Record = {}; const batchChangeTypes: Record = {}; - const batchNos = rows.map((row) => - parseImportedSkuBatchNo(row.batchNo, fallbackBatchNo), + const batchKey = String(normalizedTargetBatchNo); + const nextBatchCount = Math.max( + currentBatchCount, + normalizedTargetBatchNo, + 1, ); - const nextBatchCount = Math.max(fallbackBatchNo, 1, ...batchNos); - rows.forEach((row, index) => { - const batchKey = String(batchNos[index] ?? 1); + rows.forEach((row) => { batchFlowNos[batchKey] = row.dingTalkFlowNo.trim(); batchChangeTypes[batchKey] = row.batchChangeType.trim(); }); diff --git a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts index d82e19cc..d1d4d4b8 100644 --- a/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts +++ b/furniture-3d-system/src/app/intake/non-integrated/workspaces/basic-info/useSkuWorkbookController.ts @@ -125,8 +125,15 @@ export function useSkuWorkbookController({ event: ChangeEvent, ) { if (isReadOnly) return; - if (activeSkuBatchNo && isSkuBatchFactoryConfirmed(activeSkuBatchNo)) { - setSaveMessage(`批次${activeSkuBatchNo}已定厂,不能上传SKU资料。`); + const targetBatchNo = activeSkuBatchNo; + if (targetBatchNo === null) { + setSaveMessage("请先切到具体批次后再上传SKU资料。"); + setSaveMessageType("error"); + event.target.value = ""; + return; + } + if (isSkuBatchFactoryConfirmed(targetBatchNo)) { + setSaveMessage(`批次${targetBatchNo}已定厂,不能上传SKU资料。`); setSaveMessageType("error"); event.target.value = ""; return; @@ -142,20 +149,22 @@ export function useSkuWorkbookController({ if (rows.length === 0) { throw new Error("上传资料中没有可导入的 SKU 行"); } + + const importBatchMetadata = getSkuWorkbookImportBatchMetadata({ + currentBatchCount: skuBatchCount, + rows, + targetBatchNo, + }); if (!(await ensureFormalSpuReadyForSkuEdit())) { throw new Error("请先生成正式SPU后再导入SKU资料"); } - const importBatchMetadata = getSkuWorkbookImportBatchMetadata({ - fallbackBatchNo: skuBatchCount, - rows, - }); - setSkuItems((current) => { return buildSkuItemsFromImportedWorkbookRows({ currentItems: current, formValues, rows, + targetBatchNo, }); }); setSkuBatchCount(importBatchMetadata.nextBatchCount);