fix(intake): isolate SKU batch workbook imports
This commit is contained in:
parent
2cd254fee5
commit
d0ddbb6ccf
@ -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<NonIntegratedSkuItem> = {},
|
||||
) {
|
||||
return {
|
||||
...createEmptySkuItem(),
|
||||
id,
|
||||
batchNo,
|
||||
skuSequence,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createImportRow(
|
||||
overrides: Partial<NonIntegratedSkuExportRow> = {},
|
||||
): 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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<string, string> = {};
|
||||
const batchChangeTypes: Record<string, string> = {};
|
||||
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();
|
||||
});
|
||||
|
||||
@ -125,8 +125,15 @@ export function useSkuWorkbookController({
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user