feat(intake): 完善正式入库定厂与寄样流程

This commit is contained in:
WuYongJie 2026-05-20 12:54:49 +08:00
parent 05b34bcec1
commit 314c4d1ba0
8 changed files with 1843 additions and 752 deletions

View File

@ -501,6 +501,7 @@ const NON_INTEGRATED_NODE_OWNER_LABELS: Partial<
包装细节: "结构设计师",
产品定厂: "采购负责人",
样包测量: "采购/核价组",
成品寄样: "品牌/采购",
产品核价: "采购/核价组",
产品限价: "企划负责人",
工厂质检: "质检组",
@ -1910,6 +1911,16 @@ function getPopulatedSampleMeasurementRowCountForList(
).length;
}
function getPopulatedFinishedSampleShipmentRowCountForList(
product: NonIntegratedProductRecord,
) {
return (product.finishedSampleShipmentRows ?? []).filter((row) =>
[row.sampleQuantity, row.shipped, row.trackingNumber].some(
(value) => value.trim().length > 0,
),
).length;
}
function getPopulatedProductPricingRowCountForList(
product: NonIntegratedProductRecord,
) {
@ -2046,6 +2057,9 @@ function getDocumentWorkspaceCountByCategory(
样包测量:
uploadFileCountByCategory["样包测量"] +
getPopulatedSampleMeasurementRowCountForList(product),
成品寄样:
uploadFileCountByCategory["成品寄样"] +
getPopulatedFinishedSampleShipmentRowCountForList(product),
产品核价:
uploadFileCountByCategory["产品核价"] +
getPopulatedProductPricingRowCountForList(product),
@ -2170,9 +2184,14 @@ function getCurrentNodeOwnerCandidates(
}
if (
["产品定厂", "样包测量", "产品核价", "工厂质检", "检测报告"].includes(
category,
)
[
"产品定厂",
"样包测量",
"成品寄样",
"产品核价",
"工厂质检",
"检测报告",
].includes(category)
) {
return getUniqueNonEmptyValues([
product.purchaser,

View File

@ -328,6 +328,7 @@ function createProductRecord(
planningLayoutRows: [],
productLimitRows: [],
sampleMeasurementRows: [],
finishedSampleShipmentRows: [],
uploadFiles: [],
documentWorkspaceNotes: {},
documentWorkspaceConfirmations:
@ -1554,6 +1555,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-包装细节",
"document-workspace-trigger-产品定厂",
"document-workspace-trigger-样包测量",
"document-workspace-trigger-成品寄样",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
"document-workspace-trigger-工厂质检",
@ -1635,6 +1637,7 @@ describe("NonIntegratedDetailClient", () => {
"document-workspace-trigger-包装细节",
"document-workspace-trigger-产品定厂",
"document-workspace-trigger-样包测量",
"document-workspace-trigger-成品寄样",
"document-workspace-trigger-产品核价",
"document-workspace-trigger-产品限价",
"document-workspace-trigger-工厂质检",
@ -1771,13 +1774,9 @@ describe("NonIntegratedDetailClient", () => {
expect(screen.getByRole("button", { name: "删除" })).toBeInTheDocument();
await actor.click(screen.getByLabelText("二级类目"));
expect(
within(openingReferenceWorkspace).getByRole("listbox"),
).toHaveTextContent("桌类");
expect(screen.getByRole("listbox")).toHaveTextContent("桌类");
await actor.click(screen.getByLabelText("商品类目"));
const productCategoryOptions = within(
openingReferenceWorkspace,
).getByRole("listbox");
const productCategoryOptions = screen.getByRole("listbox");
expect(productCategoryOptions).toHaveTextContent("书桌");
expect(productCategoryOptions).not.toHaveTextContent("鞋柜");
@ -2467,6 +2466,248 @@ describe("NonIntegratedDetailClient", () => {
expect(screen.queryByDisplayValue("ZWJ021801N396V01")).not.toBeInTheDocument();
});
it("recalculates imported SKU barcodes when product factory supplier changes", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "ZWJ0218-imported-factory-supplier",
spu: "ZWJ0218",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
skuItems: [
createSkuItem({
id: "sku-imported-factory-supplier-1",
skuSequence: 1,
model: "ZWJ0218K01",
skuCode: "ZWJ021801",
barcode: "ZWJ021801N396V01",
manufacturer: "曹县嘉欢",
manufacturerCode: "N396",
source: "sku_workbook",
sourceTable: "sku_workbook",
contentLocked: true,
}),
],
factoryRows: [
{
skuId: "sku-factory-editable-fields-1",
confirmed: false,
model: "ZWJ0218K01",
skuCode: "ZWJ021801",
setCount: "9",
barcode: "ZWJ021801N396V01",
specName: "旧定厂暂存规格",
size: "",
color: "",
material: "",
installMethod: "拆装",
supplierName: "旧定厂暂存供应商",
supplierCode: "OLD001",
warehouse: "",
minimumOrderQuantity: "",
leadTime: "",
assemblyMode: "",
productionType: "",
installationService: "",
supplierCategory: "",
purchaseOrderMethod: "",
executeStandard: "",
},
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="ZWJ0218-imported-factory-supplier"
/>,
);
await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂"));
const factoryWorkspace = screen.getByTestId("product-factory-workspace");
const supplierInput = within(factoryWorkspace).getByRole("combobox", {
name: "供应商名称",
});
await actor.clear(supplierInput);
await actor.type(supplierInput, "兰考华兰家具");
await actor.tab();
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"ZWJ0218-imported-factory-supplier",
);
expect(savedProduct?.skuItems[0]?.manufacturer).toBe("兰考华兰家具");
expect(savedProduct?.skuItems[0]?.manufacturerCode).toBe("N1280");
expect(savedProduct?.skuItems[0]?.barcode).toBe("ZWJ021801N1280V01");
expect(savedProduct?.factoryRows[0]?.supplierName).toBe("兰考华兰家具");
expect(savedProduct?.factoryRows[0]?.supplierCode).toBe("N1280");
expect(savedProduct?.factoryRows[0]?.barcode).toBe("ZWJ021801N1280V01");
});
});
it("lets product factory maintain set count and install method after SKU confirmation", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "ZWJ0218-factory-editable-fields",
spu: "ZWJ0218",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
skuItems: [
createSkuItem({
id: "sku-factory-editable-fields-1",
skuSequence: 1,
model: "ZWJ0218K01",
skuCode: "ZWJ021801",
setCount: "1",
barcode: "ZWJ021801N396V01",
manufacturer: "曹县嘉欢",
manufacturerCode: "N396",
installMethod: "整装",
source: "sku_workbook",
sourceTable: "sku_workbook",
contentLocked: true,
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="ZWJ0218-factory-editable-fields"
/>,
);
await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂"));
const factoryWorkspace = screen.getByTestId("product-factory-workspace");
const setCountInput =
within(factoryWorkspace).getByPlaceholderText("请输入套件量");
expect(setCountInput).not.toHaveAttribute("readonly");
expect(setCountInput).toHaveValue("1");
fireEvent.change(setCountInput, { target: { value: "3" } });
fireEvent.blur(setCountInput);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"ZWJ0218-factory-editable-fields",
);
expect(savedProduct?.skuItems.map((item) => item.setCount)).toEqual([
"3",
"3",
"3",
]);
expect(savedProduct?.skuItems.map((item) => item.skuCode)).toEqual([
"ZWJ021801A",
"ZWJ021801B",
"ZWJ021801C",
]);
expect(savedProduct?.factoryRows.map((row) => row.skuCode)).toEqual([
"ZWJ021801A",
"ZWJ021801B",
"ZWJ021801C",
]);
expect(savedProduct?.factoryRows[0]?.specName).toContain("A包");
});
const supplierInputs = within(factoryWorkspace).getAllByRole("combobox", {
name: "供应商名称",
});
expect(supplierInputs).toHaveLength(3);
expect(supplierInputs[1]).not.toHaveAttribute("readonly");
fireEvent.change(supplierInputs[1]!, {
target: { value: "兰考华兰家具" },
});
const warehouseInputs = within(factoryWorkspace).getAllByLabelText("仓库");
expect(warehouseInputs).toHaveLength(3);
expect(warehouseInputs[2]).not.toHaveAttribute("readonly");
fireEvent.change(warehouseInputs[2]!, { target: { value: "郑州仓" } });
const minimumOrderQuantityInputs =
within(factoryWorkspace).getAllByPlaceholderText("请输入最低起订量");
expect(minimumOrderQuantityInputs).toHaveLength(3);
expect(minimumOrderQuantityInputs[1]).not.toHaveAttribute("readonly");
fireEvent.change(minimumOrderQuantityInputs[1]!, {
target: { value: "50" },
});
const leadTimeInputs = within(factoryWorkspace).getAllByLabelText("交期");
expect(leadTimeInputs).toHaveLength(3);
expect(leadTimeInputs[2]).not.toHaveAttribute("readonly");
fireEvent.change(leadTimeInputs[2]!, {
target: { value: "2026-06-20" },
});
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"ZWJ0218-factory-editable-fields",
);
expect(savedProduct?.skuItems.map((item) => item.manufacturer)).toEqual([
"兰考华兰家具",
"兰考华兰家具",
"兰考华兰家具",
]);
expect(savedProduct?.skuItems.map((item) => item.manufacturerCode)).toEqual([
"N1280",
"N1280",
"N1280",
]);
expect(savedProduct?.factoryRows.map((row) => row.supplierName)).toEqual([
"兰考华兰家具",
"兰考华兰家具",
"兰考华兰家具",
]);
expect(savedProduct?.factoryRows.map((row) => row.warehouse)).toEqual([
"郑州仓",
"郑州仓",
"郑州仓",
]);
expect(
savedProduct?.factoryRows.map((row) => row.minimumOrderQuantity),
).toEqual(["50", "50", "50"]);
expect(savedProduct?.factoryRows.map((row) => row.leadTime)).toEqual([
"2026年06月20日",
"2026年06月20日",
"2026年06月20日",
]);
});
const installMethodSelect = within(factoryWorkspace).getAllByRole("combobox", {
name: "安装方式",
})[0];
expect(installMethodSelect).not.toBeDisabled();
expect(installMethodSelect).toHaveTextContent("整装");
await actor.click(installMethodSelect);
const installMethodOptions = screen.getByRole("listbox");
expect(installMethodOptions.parentElement).toBe(document.body);
expect(installMethodOptions).toHaveStyle({ position: "fixed" });
await actor.click(screen.getByRole("option", { name: "拆装" }));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"ZWJ0218-factory-editable-fields",
);
expect(savedProduct?.skuItems.map((item) => item.installMethod)).toEqual([
"拆装",
"拆装",
"拆装",
]);
expect(savedProduct?.factoryRows.map((row) => row.installMethod)).toEqual([
"拆装",
"拆装",
"拆装",
]);
});
});
it("persists the SKU national standard compliance selection", async () => {
const actor = userEvent.setup();
@ -2549,14 +2790,14 @@ describe("NonIntegratedDetailClient", () => {
});
});
it("requires complete SKU info before showing the SKU confirmation action", async () => {
it("removes SKU content confirmation actions and keeps SKU fields editable before factory confirmation", async () => {
const actor = userEvent.setup();
window.localStorage.setItem(
NON_INTEGRATED_STORAGE_KEY,
JSON.stringify([
createProductRecord({
id: "DNZ0001-sku-confirmation-incomplete",
id: "DNZ0001-sku-confirmation-removed",
skuItems: [
createSkuItem({
boardMaterialType: "",
@ -2577,7 +2818,7 @@ describe("NonIntegratedDetailClient", () => {
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-sku-confirmation-incomplete"
productId="DNZ0001-sku-confirmation-removed"
currentUserRole="SUPER_ADMIN"
currentUserBusinessRoles={["DESIGN"]}
/>,
@ -2588,17 +2829,23 @@ describe("NonIntegratedDetailClient", () => {
expect(
screen.queryByTestId("non-integrated-confirm-sku-content"),
).not.toBeInTheDocument();
await actor.click(screen.getByTestId("non-integrated-check-sku-content"));
expect(
screen.getByText(/SKU信息未填写完整.*第1行缺少:板材类型/),
).toBeInTheDocument();
screen.queryByTestId("non-integrated-check-sku-content"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("non-integrated-reopen-sku-content"),
).not.toBeInTheDocument();
const skuRow = screen.getByDisplayValue("SZ0230K01").closest("tr");
expect(skuRow).not.toBeNull();
expect(
within(skuRow!).getByRole("combobox", { name: "SZ0230K01 板材类型" }),
).toBeEnabled();
});
it("locks only the confirmed SKU batch after a second confirmation", async () => {
it("locks SKU content after product factory confirmation and unlocks after reopening factory", async () => {
const actor = userEvent.setup();
const productId = "DNZ0001-sku-confirmation-complete";
const productId = "DNZ0001-sku-lock-by-factory";
saveNonIntegratedProduct(
createProductRecord({
@ -2606,6 +2853,7 @@ describe("NonIntegratedDetailClient", () => {
spu: "DNZ0001",
skuItems: [
createSkuItem({
id: "sku-lock-by-factory-1",
boardMaterialType: "ENF级",
executeStandard: "GB/T 3324",
nationalStandardCompliant: "是",
@ -2617,6 +2865,32 @@ describe("NonIntegratedDetailClient", () => {
skuBatchCount: 1,
skuBatchChangeTypes: { 1: "新品" },
skuBatchFlowNos: { 1: "DD20260519001" },
factoryRows: [
{
skuId: "sku-lock-by-factory-1",
confirmed: true,
model: "SZ0230K01",
skuCode: "DNZ000101",
setCount: "1",
barcode: "DNZ000101GYS001V01",
specName: "款式一 140/60/75",
size: "140/60/75",
color: "胡桃色",
material: "橡胶木",
installMethod: "拆装",
supplierName: "测试供应商",
supplierCode: "GYS001",
warehouse: "郑州仓",
minimumOrderQuantity: "50",
leadTime: "30天",
assemblyMode: "",
productionType: "常规生产",
installationService: "无",
supplierCategory: "主供",
purchaseOrderMethod: "系统下单",
executeStandard: "GB/T 3324",
},
],
}),
);
@ -2631,197 +2905,29 @@ describe("NonIntegratedDetailClient", () => {
await actor.click(screen.getByTestId("document-workspace-basic-info-trigger"));
const confirmButton = screen.getByTestId(
"non-integrated-confirm-sku-content",
);
expect(confirmButton).toHaveTextContent("确认SKU信息");
await actor.click(confirmButton);
expect(
screen.getByText(
/批次1 SKU 信息已检查完整.*再次点击“确认SKU信息”后/,
),
).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByTestId("non-integrated-confirm-sku-content"),
).toHaveTextContent(/^再次确认SKU信息$/);
});
const secondConfirmButton = screen.getByTestId(
"non-integrated-confirm-sku-content",
);
await actor.click(secondConfirmButton);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.skuBatchConfirmedAt?.["1"]).toEqual(
expect.any(String),
);
});
expect(await screen.findByText("已确认SKU信息")).toBeInTheDocument();
expect(
screen.queryByTestId("non-integrated-confirm-sku-content"),
).not.toBeInTheDocument();
expect(screen.getByDisplayValue("SZ0230K01")).toHaveAttribute("readonly");
expect(
screen.queryByTestId("non-integrated-add-sku"),
).not.toBeInTheDocument();
expect(screen.queryByText("上传资料")).not.toBeInTheDocument();
expect(screen.getByText("下载资料")).toBeInTheDocument();
expect(screen.getByText("上传图片")).toBeInTheDocument();
expect(screen.getByText("新增批次")).toBeInTheDocument();
});
expect(screen.queryByText("上传图片")).not.toBeInTheDocument();
it("hides the SKU confirmation prompt after five seconds", async () => {
jest.useFakeTimers();
const actor = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
try {
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-sku-confirmation-prompt-timeout",
skuItems: [
createSkuItem({
boardMaterialType: "",
executeStandard: "GB/T 3324",
nationalStandardCompliant: "是",
manufacturer: "测试供应商",
manufacturerCode: "GYS001",
salesStatus: "销售",
}),
],
skuBatchCount: 1,
skuBatchChangeTypes: { 1: "新品" },
skuBatchFlowNos: { 1: "DD20260519001" },
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-sku-confirmation-prompt-timeout"
currentUserRole="SUPER_ADMIN"
currentUserBusinessRoles={["DESIGN"]}
/>,
);
await actor.click(
screen.getByTestId("document-workspace-basic-info-trigger"),
);
await actor.click(screen.getByTestId("non-integrated-check-sku-content"));
expect(
screen.getByText(/SKU信息未填写完整.*第1行缺少:板材类型/),
).toBeInTheDocument();
act(() => {
jest.advanceTimersByTime(5000);
});
expect(
screen.queryByText(/SKU信息未填写完整.*第1行缺少:板材类型/),
).not.toBeInTheDocument();
} finally {
jest.useRealTimers();
}
});
it("lets admins reopen confirmed SKU content before sample measurement is approved", async () => {
const actor = userEvent.setup();
const productId = "DNZ0001-sku-reopen-before-measurement";
saveNonIntegratedProduct(
createProductRecord({
id: productId,
spu: "DNZ0001",
skuItems: [
createSkuItem({
boardMaterialType: "ENF级",
executeStandard: "GB/T 3324",
nationalStandardCompliant: "是",
manufacturer: "测试供应商",
manufacturerCode: "GYS001",
salesStatus: "销售",
}),
],
skuBatchCount: 1,
skuBatchChangeTypes: { 1: "新品" },
skuBatchFlowNos: { 1: "DD20260519001" },
skuBatchConfirmedAt: { 1: "2026-05-19T10:00:00.000Z" },
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId={productId}
currentUserRole="SUPER_ADMIN"
currentUserBusinessRoles={["DESIGN"]}
/>,
);
await actor.click(screen.getByTestId("document-workspace-basic-info-trigger"));
expect(screen.getByTestId("non-integrated-reopen-sku-content")).toBeEnabled();
await actor.click(screen.getByTestId("non-integrated-reopen-sku-content"));
await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂"));
await actor.click(screen.getByTestId("product-factory-reopen-confirmation"));
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(productId);
expect(savedProduct?.skuBatchConfirmedAt?.["1"]).toBeUndefined();
expect(savedProduct?.skuItems[0]?.model).toBe("SZ0230K01");
expect(savedProduct?.factoryRows?.[0]?.confirmed).toBe(false);
});
expect(
screen.getByTestId("non-integrated-confirm-sku-content"),
).toHaveTextContent("确认SKU信息");
});
it("prevents admins from reopening SKU content after sample measurement is approved", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-sku-reopen-after-measurement",
spu: "DNZ0001",
skuItems: [
createSkuItem({
boardMaterialType: "ENF级",
executeStandard: "GB/T 3324",
nationalStandardCompliant: "是",
manufacturer: "测试供应商",
manufacturerCode: "GYS001",
salesStatus: "销售",
}),
],
skuBatchCount: 1,
skuBatchChangeTypes: { 1: "新品" },
skuBatchFlowNos: { 1: "DD20260519001" },
skuBatchConfirmedAt: { 1: "2026-05-19T10:00:00.000Z" },
documentWorkspaceConfirmations: {
...createDefaultDocumentWorkspaceConfirmations(),
样包测量: true,
},
documentWorkspaceApprovedAt: {
样包测量: "2026-05-19T11:00:00.000Z",
},
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-sku-reopen-after-measurement"
currentUserRole="SUPER_ADMIN"
currentUserBusinessRoles={["DESIGN"]}
/>,
);
await actor.click(screen.getByTestId("document-workspace-basic-info-trigger"));
expect(screen.getByTestId("non-integrated-reopen-sku-content")).toBeDisabled();
expect(screen.getByDisplayValue("SZ0230K01")).not.toHaveAttribute(
"readonly",
);
expect(screen.getByTestId("non-integrated-add-sku")).toBeInTheDocument();
expect(screen.getByText("上传资料")).toBeInTheDocument();
expect(screen.getByText("上传图片")).toBeInTheDocument();
});
it("hides redundant fields from the product detail summary", async () => {
@ -3040,6 +3146,20 @@ describe("NonIntegratedDetailClient", () => {
expect(screen.getByText("SZ023001")).toBeInTheDocument();
expect(screen.getByText("SZ023001N1280V01")).toBeInTheDocument();
expect(screen.getByText("兰考华兰家具")).toBeInTheDocument();
const specNameCell = screen.getByTestId(
"sample-measurement-readonly-specName-sku-sample-1",
);
expect(specNameCell).toHaveClass(
"whitespace-nowrap",
"resize-x",
"overflow-hidden",
"text-ellipsis",
);
expect(specNameCell).toHaveStyle({
minWidth: "180px",
width: "180px",
});
expect(specNameCell).toHaveAttribute("title");
await actor.type(
screen.getByTestId("sample-measurement-lengthCm-sku-sample-1"),
@ -3100,6 +3220,224 @@ describe("NonIntegratedDetailClient", () => {
).toHaveAttribute("readonly");
});
it("lets link users edit finished sample quantities only", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-finished-sample-link-user",
spu: "SZ0230",
skuItems: [
createSkuItem({
id: "sku-finished-link-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
manufacturer: "兰考华兰家具",
installMethod: "拆装",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-finished-sample-link-user"
initialDocumentWorkspaceCategory="成品寄样"
currentUserRole="USER"
currentUserName="品牌同事"
/>,
);
const panel = screen.getByTestId("document-workspace-panel");
expect(screen.getByTestId("finished-sample-shipment-workspace")).toBeInTheDocument();
expect(panel).toHaveTextContent("图片");
expect(panel).toHaveTextContent("规格码");
expect(panel).toHaveTextContent("供应商名称");
expect(panel).toHaveTextContent("安装方式");
const quantityInput = screen.getByTestId(
"finished-sample-shipment-sampleQuantity-sku-finished-link-1",
);
expect(quantityInput).not.toHaveAttribute("readonly");
expect(
screen.getByTestId("finished-sample-shipment-shipped-sku-finished-link-1"),
).toBeDisabled();
expect(
screen.getByTestId(
"finished-sample-shipment-trackingNumber-sku-finished-link-1",
),
).toHaveAttribute("readonly");
expect(screen.getByTestId("finished-sample-shipment-confirm")).toBeDisabled();
await actor.type(quantityInput, "2");
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-finished-sample-link-user",
);
expect(savedProduct?.finishedSampleShipmentRows).toEqual([
{
skuId: "sku-finished-link-1",
sampleQuantity: "2",
shipped: "",
trackingNumber: "",
},
]);
});
});
it("lets purchasing users fill and confirm finished sample shipments", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-finished-sample-purchase",
spu: "SZ0230",
purchaser: "采购同事",
purchaseOwner: "采购同事",
skuItems: [
createSkuItem({
id: "sku-finished-purchase-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
setCount: "1",
specName: "款式一 140/60/75",
manufacturer: "兰考华兰家具",
installMethod: "拆装",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-finished-sample-purchase"
initialDocumentWorkspaceCategory="成品寄样"
currentUserRole="USER"
currentUserName="采购同事"
currentUserBusinessRoles={["PURCHASING"]}
currentUserDepartmentPaths={["供应链中心 > 采购部"]}
/>,
);
await actor.type(
screen.getByTestId(
"finished-sample-shipment-sampleQuantity-sku-finished-purchase-1",
),
"3",
);
await selectInlineOption(
actor,
screen.getByTestId(
"finished-sample-shipment-shipped-sku-finished-purchase-1",
),
"是",
);
await actor.type(
screen.getByTestId(
"finished-sample-shipment-trackingNumber-sku-finished-purchase-1",
),
"SF123456789",
);
const confirmButton = screen.getByTestId("finished-sample-shipment-confirm");
expect(confirmButton).toBeEnabled();
await actor.click(confirmButton);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-finished-sample-purchase",
);
expect(savedProduct?.finishedSampleShipmentRows).toEqual([
{
skuId: "sku-finished-purchase-1",
sampleQuantity: "3",
shipped: "是",
trackingNumber: "SF123456789",
},
]);
expect(savedProduct?.documentWorkspaceConfirmations?.成品寄样).toBe(true);
expect(savedProduct?.documentWorkspaceApprovedAt?.成品寄样).toEqual(
expect.any(String),
);
});
});
it("lets admins fill finished sample shipment shipping fields", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "SZ0230-finished-sample-admin",
spu: "SZ0230",
skuItems: [
createSkuItem({
id: "sku-finished-admin-1",
model: "SZ0230K01",
skuCode: "SZ023001",
barcode: "SZ023001N1280V01",
manufacturer: "兰考华兰家具",
installMethod: "拆装",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="SZ0230-finished-sample-admin"
initialDocumentWorkspaceCategory="成品寄样"
currentUserRole="SUPER_ADMIN"
/>,
);
expect(
screen.getByTestId("finished-sample-shipment-shipped-sku-finished-admin-1"),
).toBeEnabled();
expect(
screen.getByTestId(
"finished-sample-shipment-trackingNumber-sku-finished-admin-1",
),
).not.toHaveAttribute("readonly");
await actor.type(
screen.getByTestId(
"finished-sample-shipment-sampleQuantity-sku-finished-admin-1",
),
"1",
);
await selectInlineOption(
actor,
screen.getByTestId("finished-sample-shipment-shipped-sku-finished-admin-1"),
"是",
);
await actor.type(
screen.getByTestId(
"finished-sample-shipment-trackingNumber-sku-finished-admin-1",
),
"SFADMIN001",
);
await waitFor(() => {
const savedProduct = getNonIntegratedProductById(
"SZ0230-finished-sample-admin",
);
expect(savedProduct?.finishedSampleShipmentRows?.[0]).toMatchObject({
sampleQuantity: "1",
shipped: "是",
trackingNumber: "SFADMIN001",
});
});
});
it("lets purchasing users edit only purchasing document nodes", async () => {
const actor = userEvent.setup();
const openingReferenceCategory = "开品参考";
@ -3624,6 +3962,41 @@ describe("NonIntegratedDetailClient", () => {
});
});
it("shows the product factory reopen action disabled for admins while factory rows are pending", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-product-factory-reopen-pending",
skuItems: [
createSkuItem({
id: "sku-factory-reopen-pending-1",
barcode: "SZ023001N1280V01",
manufacturer: "兰考华兰家具",
manufacturerCode: "N1280",
installMethod: "拆装",
executeStandard: "GB/T 3324",
}),
],
}),
);
render(
<NonIntegratedDetailClient
mode="edit"
productId="DNZ0001-product-factory-reopen-pending"
currentUserRole="SUPER_ADMIN"
currentUserBusinessRoles={["DESIGN"]}
/>,
);
await actor.click(screen.getByTestId("document-workspace-trigger-产品定厂"));
expect(
screen.getByTestId("product-factory-reopen-confirmation"),
).toBeDisabled();
});
it("prevents admins from reopening product factory after sample measurement is approved", async () => {
const actor = userEvent.setup();
@ -3693,13 +4066,12 @@ describe("NonIntegratedDetailClient", () => {
).toBeDisabled();
});
it("renders planning layout rows from SKU data without SKU edit actions", async () => {
it("renders planning layout rows from SKU data without SKU confirmation or sales-status hiding", async () => {
const actor = userEvent.setup();
saveNonIntegratedProduct(
createProductRecord({
id: "DNZ0001-planning-layout",
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
skuItems: [
createSkuItem({
id: "sku-planning-layout-1",
@ -3741,8 +4113,8 @@ describe("NonIntegratedDetailClient", () => {
const planningWorkspace = screen.getByTestId("planning-layout-workspace");
expect(planningWorkspace).toHaveTextContent("SZ0230K01");
expect(planningWorkspace).toHaveTextContent("SZ023001N1280V01");
expect(planningWorkspace).not.toHaveTextContent("SZ0230K02");
expect(planningWorkspace).not.toHaveTextContent("SZ023002N1280V01");
expect(planningWorkspace).toHaveTextContent("SZ0230K02");
expect(planningWorkspace).toHaveTextContent("SZ023002N1280V01");
expect(planningWorkspace).toHaveTextContent("GB/T3324-2024");
expect(planningWorkspace).toHaveTextContent("是");
expect(

View File

@ -65,6 +65,7 @@ function createProduct(overrides?: Partial<NonIntegratedProductRecord>): NonInte
planningLayoutRows: [],
productLimitRows: [],
sampleMeasurementRows: [],
finishedSampleShipmentRows: [],
uploadFiles: [],
documentWorkspaceConfirmations: createDefaultDocumentWorkspaceConfirmations(),
createdAt: '2026-04-15T10:00:00.000Z',

View File

@ -44,6 +44,7 @@ export const NON_INTEGRATED_DOCUMENT_WORKSPACE_CATEGORIES = [
"包装细节",
"产品定厂",
"样包测量",
"成品寄样",
"产品核价",
"产品限价",
"工厂质检",
@ -68,6 +69,7 @@ export const NON_INTEGRATED_WORKFLOW_NODE_CATEGORIES = [
"包装细节",
"产品定厂",
"样包测量",
"成品寄样",
"产品核价",
"产品限价",
"工厂质检",
@ -269,6 +271,7 @@ const NON_INTEGRATED_UPLOAD_CATEGORY_MAP: Record<string, string> = {
包装细节: "包装细节",
产品定厂: "产品定厂",
样包测量: "样包测量",
成品寄样: "成品寄样",
产品核价: "产品核价",
产品限价: "产品限价",
工厂质检: "工厂质检",
@ -389,6 +392,13 @@ export type NonIntegratedSampleMeasurementRow = {
productShape: string;
};
export type NonIntegratedFinishedSampleShipmentRow = {
skuId: string;
sampleQuantity: string;
shipped: string;
trackingNumber: string;
};
export type NonIntegratedFirstQualityRow = {
skuId: string;
qualityBatch: string;
@ -598,6 +608,7 @@ export type NonIntegratedProductRecord = {
planningLayoutRows: NonIntegratedPlanningLayoutRow[];
productLimitRows: NonIntegratedProductLimitRow[];
sampleMeasurementRows: NonIntegratedSampleMeasurementRow[];
finishedSampleShipmentRows: NonIntegratedFinishedSampleShipmentRow[];
firstQualityRows?: NonIntegratedFirstQualityRow[];
uploadFiles: NonIntegratedUploadFile[];
documentWorkspaceNotes?: Record<string, string>;
@ -1343,6 +1354,25 @@ function normalizeSampleMeasurementRow(
};
}
function normalizeFinishedSampleShipmentRow(
item: Partial<NonIntegratedFinishedSampleShipmentRow>,
): NonIntegratedFinishedSampleShipmentRow | null {
if (typeof item.skuId !== "string") return null;
return {
skuId: item.skuId,
sampleQuantity:
typeof item.sampleQuantity === "string"
? item.sampleQuantity
: typeof item.sampleQuantity === "number" &&
Number.isFinite(item.sampleQuantity)
? String(item.sampleQuantity)
: "",
shipped: normalizeString(item.shipped),
trackingNumber: normalizeString(item.trackingNumber),
};
}
function normalizeFirstQualityRow(
item: Partial<NonIntegratedFirstQualityRow>,
): NonIntegratedFirstQualityRow | null {
@ -1965,6 +1995,14 @@ function toProductRecord(
entry !== null,
)
: [],
finishedSampleShipmentRows: Array.isArray(item.finishedSampleShipmentRows)
? item.finishedSampleShipmentRows
.map((entry) => normalizeFinishedSampleShipmentRow(entry))
.filter(
(entry): entry is NonIntegratedFinishedSampleShipmentRow =>
entry !== null,
)
: [],
firstQualityRows: Array.isArray(item.firstQualityRows)
? item.firstQualityRows
.map((entry) => normalizeFirstQualityRow(entry))
@ -2323,6 +2361,7 @@ export function createCopiedNonIntegratedProduct(
})),
productLimitRows: [],
sampleMeasurementRows: [],
finishedSampleShipmentRows: [],
uploadFiles: sourceProduct.uploadFiles.map((file) => ({ ...file })),
documentWorkspaceNotes: {
...(sourceProduct.documentWorkspaceNotes ?? {}),

View File

@ -73,6 +73,7 @@ describe('non-integrated intake permissions', () => {
expect(canEditNonIntegratedWorkspace(baseProduct, planningOwner, '开品参考')).toBe(true)
expect(canEditNonIntegratedWorkspace(baseProduct, planningOwner, '产品限价')).toBe(true)
expect(canEditNonIntegratedWorkspace(baseProduct, otherPlanner, '开品参考')).toBe(false)
expect(canEditNonIntegratedWorkspace(baseProduct, brandCampaignUser, '成品寄样')).toBe(true)
expect(canEditAnyNonIntegratedWorkspace(baseProduct, brandCampaignUser)).toBe(false)
})

View File

@ -53,6 +53,7 @@ const INSTALLATION_WORKSPACE_CATEGORY = '安装方式'
const PACKAGING_WORKSPACE_CATEGORY = '包装细节'
const PRODUCT_FACTORY_WORKSPACE_CATEGORY = '产品定厂'
const SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY = '样包测量'
const FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY = '成品寄样'
const PRODUCT_PRICING_WORKSPACE_CATEGORY = '产品核价'
const PRODUCT_LIMIT_WORKSPACE_CATEGORY = '产品限价'
const FACTORY_QUALITY_WORKSPACE_CATEGORY = '工厂质检'
@ -83,9 +84,14 @@ const SPACE_OWNER_WORKSPACE_CATEGORIES = new Set<string>([
const PURCHASE_OWNER_WORKSPACE_CATEGORIES = new Set<string>([
PRODUCT_FACTORY_WORKSPACE_CATEGORY,
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
PRODUCT_PRICING_WORKSPACE_CATEGORY,
])
const OPEN_LINK_EDIT_WORKSPACE_CATEGORIES = new Set<string>([
FINISHED_SAMPLE_SHIPMENT_WORKSPACE_CATEGORY,
])
const PRICING_GROUP_WORKSPACE_CATEGORIES = new Set<string>([
SAMPLE_MEASUREMENT_WORKSPACE_CATEGORY,
PRODUCT_PRICING_WORKSPACE_CATEGORY,
@ -357,6 +363,7 @@ export function canEditNonIntegratedWorkspace(
const normalizedCategory = normalizeWorkspaceCategory(category)
if (!normalizedCategory) return false
if (isIntakeNonIntegratedFullEditor(user)) return true
if (OPEN_LINK_EDIT_WORKSPACE_CATEGORIES.has(normalizedCategory)) return true
if (
DESIGN_WORKSPACE_CATEGORIES.has(normalizedCategory) &&
@ -410,6 +417,7 @@ export function canEditAnyNonIntegratedWorkspace(
if (isIntakeNonIntegratedFullEditor(user)) return true
return Array.from(EDITABLE_WORKSPACE_CATEGORIES).some((category) =>
!OPEN_LINK_EDIT_WORKSPACE_CATEGORIES.has(category) &&
canEditNonIntegratedWorkspace(product, user, category),
)
}

View File

@ -489,6 +489,7 @@ function buildSourceProductPayload(
planningLayoutRows: [],
productLimitRows: [],
sampleMeasurementRows: [],
finishedSampleShipmentRows: [],
firstQualityRows: [],
uploadFiles: [],
documentWorkspaceConfirmations: createDefaultDocumentWorkspaceConfirmations(true),