Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign
This commit is contained in:
commit
d5a1427fff
@ -4691,13 +4691,16 @@ export default function DraftGroupsFormalClient({
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-[#edf0f1] bg-white px-4 py-4">
|
||||
<Link
|
||||
href={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
window.location.assign(
|
||||
selectedSeriesProductBasicInfoHref ||
|
||||
(selectedDraftGroup
|
||||
? `/intake?draftGroupId=${selectedDraftGroup.id}&productDevelopmentKind=series`
|
||||
: '/intake?productDevelopmentKind=series')
|
||||
}
|
||||
: '/intake?productDevelopmentKind=series'),
|
||||
)
|
||||
}}
|
||||
className="flex items-center justify-between rounded-[10px] border border-[#e2e7e8] bg-[#f8fafa] px-3 py-3 transition hover:border-[#aebfc4] hover:bg-white"
|
||||
>
|
||||
<span>
|
||||
@ -4708,7 +4711,7 @@ export default function DraftGroupsFormalClient({
|
||||
</span>
|
||||
</span>
|
||||
<ArrowRight className="h-4 w-4 text-[#47646b]" />
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@ -7815,6 +7815,82 @@ describe("NonIntegratedDetailClient", () => {
|
||||
expect(costPriceInput).toHaveValue("");
|
||||
});
|
||||
|
||||
it("enables modular parent sku push without cost price", async () => {
|
||||
const actor = userEvent.setup();
|
||||
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
id: "MKH-DNZ0384-jst-combine",
|
||||
spu: "MKH-DNZ0384",
|
||||
productDevelopmentKind: "series",
|
||||
skuBatchConfirmedAt: CONFIRMED_SKU_BATCH_AT,
|
||||
skuItems: [
|
||||
createSkuItem({
|
||||
id: "modular-parent-1",
|
||||
skuSequence: 1,
|
||||
model: "MKH-DNZ0384K01",
|
||||
skuCode: "MKH-DNZ0384260600001",
|
||||
barcode: "MKH-DNZ0384260600001",
|
||||
source: "draft_group_modular_sku",
|
||||
sourceTable: "draft_group_pre_sku",
|
||||
sourceSkuId: "pre-sku-01",
|
||||
componentDetails: [
|
||||
{
|
||||
code: "DNZ038502",
|
||||
barcode: "DNZ038502N1437V01",
|
||||
name: "款式一-140/60/2.5 胡桃色",
|
||||
quantity: "1",
|
||||
role: "BOARD",
|
||||
assetType: "BOARD",
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
factoryRows: [
|
||||
{
|
||||
skuId: "modular-parent-1",
|
||||
confirmed: true,
|
||||
model: "MKH-DNZ0384K01",
|
||||
skuCode: "MKH-DNZ0384260600001",
|
||||
setCount: "1",
|
||||
barcode: "MKH-DNZ0384260600001",
|
||||
specName: "MKH-DNZ0384260600001",
|
||||
size: "",
|
||||
color: "",
|
||||
material: "",
|
||||
installMethod: "",
|
||||
supplierName: "测试供应商",
|
||||
supplierCode: "N1280",
|
||||
warehouse: "测试仓",
|
||||
shippingMethod: "",
|
||||
minimumOrderQuantity: "",
|
||||
leadTime: "",
|
||||
assemblyMode: "",
|
||||
productionType: "成品",
|
||||
installationService: "",
|
||||
supplierCategory: "",
|
||||
purchaseOrderMethod: "",
|
||||
executeStandard: "GB/T3324-2024",
|
||||
},
|
||||
],
|
||||
pricingRows: [],
|
||||
}),
|
||||
);
|
||||
|
||||
render(
|
||||
<NonIntegratedDetailClient
|
||||
mode="edit"
|
||||
productId="MKH-DNZ0384-jst-combine"
|
||||
currentUserRole="ADMIN"
|
||||
/>,
|
||||
);
|
||||
|
||||
await actor.click(screen.getByTestId("document-workspace-trigger-产品核价"));
|
||||
|
||||
expect(screen.getByRole("button", { name: "确认推送" })).toBeEnabled();
|
||||
expect(screen.getByRole("button", { name: "批量确认推送" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("only shows external intake users the data and system-entry workspaces", () => {
|
||||
saveNonIntegratedProduct(
|
||||
createProductRecord({
|
||||
|
||||
@ -465,6 +465,20 @@ export function isNonIntegratedModularSkuItem(item: {
|
||||
);
|
||||
}
|
||||
|
||||
export function isDraftGroupModularParentSkuItem(item: {
|
||||
source?: unknown;
|
||||
skuCode?: unknown;
|
||||
barcode?: unknown;
|
||||
componentDetails?: unknown;
|
||||
}) {
|
||||
return (
|
||||
normalizeSkuSourceMarker(item.source) === DRAFT_GROUP_MODULAR_SKU_SOURCE &&
|
||||
Boolean(resolveDraftGroupModularSkuIdentity(item)) &&
|
||||
Array.isArray(item.componentDetails) &&
|
||||
item.componentDetails.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveNonIntegratedSkuProductTag(item: {
|
||||
productTag?: unknown;
|
||||
source?: unknown;
|
||||
|
||||
@ -18,6 +18,7 @@ import { isProductFactoryRowComplete } from "./productFactoryHelpers";
|
||||
import {
|
||||
isProductLimitRowComplete,
|
||||
isProductPricingRowComplete,
|
||||
isProductPricingRowReadyForJstPush,
|
||||
isProductPricingRowSystemEntered,
|
||||
type PlanningLayoutSkuRow,
|
||||
type ProductLimitDisplayRow,
|
||||
@ -118,7 +119,7 @@ export function useNonIntegratedWorkflowCompletion({
|
||||
displayedProductFactoryRows.every(isProductFactoryRowComplete);
|
||||
const allProductPricingRowsComplete =
|
||||
productPricingRows.length > 0 &&
|
||||
productPricingRows.every(isProductPricingRowComplete);
|
||||
productPricingRows.every((row) => isProductPricingRowReadyForJstPush(row, row));
|
||||
const displayedSampleMeasurementRowsComplete =
|
||||
displayedProductSampleMeasurementRows.length > 0 &&
|
||||
displayedProductSampleMeasurementRows.every(
|
||||
@ -139,7 +140,9 @@ export function useNonIntegratedWorkflowCompletion({
|
||||
);
|
||||
const displayedProductPricingRowsComplete =
|
||||
displayedProductPricingRows.length > 0 &&
|
||||
displayedProductPricingRows.every(isProductPricingRowComplete);
|
||||
displayedProductPricingRows.every((row) =>
|
||||
isProductPricingRowReadyForJstPush(row, row),
|
||||
);
|
||||
const allProductPricingRowsSystemEntered =
|
||||
productPricingRows.length > 0 &&
|
||||
productPricingRows.every(isProductPricingRowSystemEntered);
|
||||
@ -153,7 +156,7 @@ export function useNonIntegratedWorkflowCompletion({
|
||||
displayedProductPricingRowsComplete &&
|
||||
displayedProductPricingRows.some(
|
||||
(row) =>
|
||||
isProductPricingRowComplete(row) &&
|
||||
isProductPricingRowReadyForJstPush(row, row) &&
|
||||
!isProductPricingRowSystemEntered(row),
|
||||
);
|
||||
const productPricingSystemEnteredSkuIdSet = useMemo(
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import type { NonIntegratedPricingRow } from "../nonIntegratedStorage";
|
||||
import {
|
||||
isDraftGroupModularParentSkuItem,
|
||||
type NonIntegratedPricingRow,
|
||||
} from "../nonIntegratedStorage";
|
||||
import {
|
||||
isProductPricingRowReadyForJstPush,
|
||||
isProductPricingRowSystemEntered,
|
||||
PRODUCT_PRICING_JST_PUSH_ENABLED,
|
||||
type ProductPricingDisplayRow,
|
||||
@ -150,7 +154,9 @@ export function ProductPricingWorkspace({
|
||||
{displayedRows.map((row) => {
|
||||
const isSystemEntered = isProductPricingRowSystemEntered(row);
|
||||
const pricingReadyForFuturePush =
|
||||
row.costPrice.trim().length > 0;
|
||||
isProductPricingRowReadyForJstPush(row, row);
|
||||
const isModularParentRow =
|
||||
isDraftGroupModularParentSkuItem(row);
|
||||
const virtualInventoryStatusMeta =
|
||||
getVirtualInventoryStatusMeta(row);
|
||||
|
||||
@ -250,6 +256,8 @@ export function ProductPricingWorkspace({
|
||||
? "确认推送当前 SKU 到聚水潭"
|
||||
: pricingReadyForFuturePush
|
||||
? "聚水潭接口未接入,暂不可确认推送"
|
||||
: isModularParentRow
|
||||
? "当前模块化父商品将直接录入聚水潭组合装"
|
||||
: "请先填写成本价"
|
||||
}
|
||||
className="inline-flex h-7 min-w-[52px] items-center justify-center rounded-full bg-[#1f8bff] px-3 text-xs font-apple-medium text-white shadow-sm transition hover:bg-[#1478df] disabled:cursor-not-allowed disabled:bg-apple-bg-tertiary disabled:text-apple-label-tertiary disabled:shadow-none"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import {
|
||||
isDraftGroupModularParentSkuItem,
|
||||
type NonIntegratedPlanningLayoutRow,
|
||||
type NonIntegratedPricingRow,
|
||||
type NonIntegratedProductLimitRow,
|
||||
@ -25,6 +26,8 @@ export type ProductPricingDisplayRow = NonIntegratedPricingRow & {
|
||||
setCount: string;
|
||||
specName: string;
|
||||
supplierName: string;
|
||||
source?: string;
|
||||
componentDetails?: NonIntegratedSkuItem["componentDetails"];
|
||||
systemEnteredAt: string;
|
||||
jstVirtualInventoryScheduledAt: string;
|
||||
jstVirtualInventoryPushedAt: string;
|
||||
@ -107,6 +110,8 @@ export function createProductPricingRowFromSku(
|
||||
setCount: sku.setCount.trim() || "1",
|
||||
specName: sku.specName.trim(),
|
||||
supplierName: sku.manufacturer.trim() || "-",
|
||||
source: sku.source,
|
||||
componentDetails: sku.componentDetails,
|
||||
costPrice: existingRow?.costPrice?.trim() ?? "",
|
||||
productTag: existingRow?.productTag?.trim() ?? "",
|
||||
systemEnteredAt: existingRow?.systemEnteredAt?.trim() ?? "",
|
||||
@ -177,6 +182,20 @@ export function isProductPricingRowComplete(row: NonIntegratedPricingRow) {
|
||||
return row.costPrice.trim().length > 0;
|
||||
}
|
||||
|
||||
export function isProductPricingRowReadyForJstPush(
|
||||
row: NonIntegratedPricingRow,
|
||||
sku?: Pick<
|
||||
NonIntegratedSkuItem,
|
||||
"source" | "skuCode" | "barcode" | "componentDetails"
|
||||
>,
|
||||
) {
|
||||
if (sku && isDraftGroupModularParentSkuItem(sku)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isProductPricingRowComplete(row);
|
||||
}
|
||||
|
||||
export function isProductPricingRowSystemEntered(
|
||||
row: Pick<NonIntegratedPricingRow, "systemEnteredAt">,
|
||||
) {
|
||||
@ -349,7 +368,7 @@ export function canConfirmProductPricingRows(rows: NonIntegratedPricingRow[]) {
|
||||
rows.length > 0 &&
|
||||
rows.every(
|
||||
(row) =>
|
||||
isProductPricingRowComplete(row) &&
|
||||
isProductPricingRowReadyForJstPush(row) &&
|
||||
isProductPricingRowSystemEntered(row),
|
||||
)
|
||||
);
|
||||
|
||||
@ -43,6 +43,7 @@ import {
|
||||
createPlanningLayoutClipboardValue,
|
||||
getPlanningLayoutDraftKey,
|
||||
getPlanningLayoutFieldTextValue,
|
||||
isProductPricingRowReadyForJstPush,
|
||||
isProductPricingRowComplete,
|
||||
isProductPricingRowSystemEntered,
|
||||
parsePlanningLayoutListInput,
|
||||
@ -189,7 +190,10 @@ export function useProductWorkflowRowController({
|
||||
if (isReadOnly || isProductPricingNodeLocked) return;
|
||||
const readyRows = rows.filter(
|
||||
(row) =>
|
||||
isProductPricingRowComplete(row) &&
|
||||
isProductPricingRowReadyForJstPush(
|
||||
row,
|
||||
workflowSkuItems.find((item) => item.id === row.skuId),
|
||||
) &&
|
||||
!isProductPricingRowSystemEntered(row),
|
||||
);
|
||||
if (readyRows.length === 0) {
|
||||
|
||||
@ -791,6 +791,148 @@ describe('JushuitanProductPricingPushService', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('模块化父商品缺少核价与成本价时仍可录入组合商品', async () => {
|
||||
const fetchMock = createSuccessFetchMock()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
const combineSku = createSku({
|
||||
id: 'modular-combine-no-pricing',
|
||||
source: 'draft_group_modular_sku',
|
||||
skuCode: 'MKH-DNZ0384260600003',
|
||||
barcode: 'MKH-DNZ0384260600003',
|
||||
model: 'MKH-DNZ0384260600003',
|
||||
specName: 'MKH-DNZ0384260600003',
|
||||
setCount: '1',
|
||||
componentDetails: [
|
||||
{
|
||||
code: 'DNZ038502N1437V01',
|
||||
name: 'DNZ038502N1437V01',
|
||||
quantity: '1',
|
||||
role: 'BOARD',
|
||||
assetType: 'BOARD',
|
||||
},
|
||||
{
|
||||
code: 'DNZ038401N1440V01',
|
||||
name: 'DNZ038401N1440V01',
|
||||
quantity: '1',
|
||||
role: 'COMPONENT',
|
||||
assetType: 'COMPONENT',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const product = createProduct([combineSku], [], '模块化')
|
||||
|
||||
const result = await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product,
|
||||
targetSkuIds: [combineSku.id],
|
||||
})
|
||||
|
||||
const itemSkuCallIndex = findCallIndex(fetchMock, '/open/jushuitan/itemsku/upload')
|
||||
const combineSkuCallIndex = findCallIndex(
|
||||
fetchMock,
|
||||
'/open/jushuitan/item/combinesku/upload',
|
||||
)
|
||||
|
||||
expect(itemSkuCallIndex).toBe(-1)
|
||||
expect(combineSkuCallIndex).toBeGreaterThanOrEqual(0)
|
||||
expect(result.normalItemCount).toBe(0)
|
||||
expect(result.combineItemCount).toBe(1)
|
||||
expect(readBiz(fetchMock, combineSkuCallIndex)).toMatchObject({
|
||||
sku_id: 'MKH-DNZ0384260600003',
|
||||
i_id: 'MKH-DNZ0384260600003',
|
||||
name: 'MKH-DNZ0384260600003',
|
||||
items: [
|
||||
{ src_sku_id: 'DNZ038502N1437V01', qty: 1, sale_price: 0 },
|
||||
{ src_sku_id: 'DNZ038401N1440V01', qty: 1, sale_price: 0 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('模块化父商品推送组合商品时子商品使用部件条码而不是货号', async () => {
|
||||
const fetchMock = createSuccessFetchMock()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
const combineSku = createSku({
|
||||
id: 'modular-combine-barcode-only',
|
||||
source: 'draft_group_modular_sku',
|
||||
skuCode: 'MKH-DNZ0384260600004',
|
||||
barcode: 'MKH-DNZ0384260600004',
|
||||
model: 'MKH-DNZ0384260600004',
|
||||
specName: 'MKH-DNZ0384260600004',
|
||||
setCount: '1',
|
||||
componentDetails: [
|
||||
{
|
||||
code: 'DBZ-SPJ01',
|
||||
barcode: 'DBZ-SPJ01N1280V01',
|
||||
name: '部件一',
|
||||
quantity: '1',
|
||||
role: 'PACKAGING',
|
||||
assetType: 'PACKAGING',
|
||||
},
|
||||
{
|
||||
code: 'DBZ-SPJ02',
|
||||
barcode: 'DBZ-SPJ02N1280V01',
|
||||
name: '部件二',
|
||||
quantity: '1',
|
||||
role: 'PACKAGING',
|
||||
assetType: 'PACKAGING',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct([combineSku], [], '模块化'),
|
||||
targetSkuIds: [combineSku.id],
|
||||
})
|
||||
|
||||
const combineSkuCallIndex = findCallIndex(
|
||||
fetchMock,
|
||||
'/open/jushuitan/item/combinesku/upload',
|
||||
)
|
||||
|
||||
expect(readBiz(fetchMock, combineSkuCallIndex)).toMatchObject({
|
||||
sku_id: 'MKH-DNZ0384260600004',
|
||||
items: [
|
||||
{ src_sku_id: 'DBZ-SPJ01N1280V01', qty: 1, sale_price: 0 },
|
||||
{ src_sku_id: 'DBZ-SPJ02N1280V01', qty: 1, sale_price: 0 },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('模块化父商品部件缺少条码时直接报错,不再用货号兜底', async () => {
|
||||
const fetchMock = createSuccessFetchMock()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
const combineSku = createSku({
|
||||
id: 'modular-combine-missing-barcode',
|
||||
source: 'draft_group_modular_sku',
|
||||
skuCode: 'MKH-DNZ0384260600005',
|
||||
barcode: 'MKH-DNZ0384260600005',
|
||||
model: 'MKH-DNZ0384260600005',
|
||||
specName: 'MKH-DNZ0384260600005',
|
||||
setCount: '1',
|
||||
componentDetails: [
|
||||
{
|
||||
code: 'DBZ-SPJ01',
|
||||
name: '部件一',
|
||||
quantity: '1',
|
||||
role: 'PACKAGING',
|
||||
assetType: 'PACKAGING',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
await expect(
|
||||
new JushuitanProductPricingPushService().pushProductPricing({
|
||||
product: createProduct([combineSku], [], '模块化'),
|
||||
targetSkuIds: [combineSku.id],
|
||||
}),
|
||||
).rejects.toThrow('组合商品缺少部件条码')
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('多包规格整组内任一包缺资料时不推送', async () => {
|
||||
const fetchMock = jest.fn()
|
||||
global.fetch = fetchMock as unknown as typeof fetch
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
NonIntegratedSampleMeasurementRow,
|
||||
NonIntegratedSkuItem,
|
||||
} from '@/app/intake/non-integrated/nonIntegratedStorage'
|
||||
import { isDraftGroupModularParentSkuItem } from '@/app/intake/non-integrated/core/nonIntegratedStorage'
|
||||
import { SPU_RULE_LIBRARY_SEED_RULES } from '@/app/intake/product-development/spuRuleLibrarySeed'
|
||||
|
||||
type JushuitanCredentials = {
|
||||
@ -578,16 +579,17 @@ function resolveTargetRows(input: JushuitanProductPricingPushInput) {
|
||||
.map((sku): NormalizedPushRow => {
|
||||
const factoryRow = factoryRowBySkuId.get(sku.id)
|
||||
const pricingRow = pricingRowBySkuId.get(sku.id)
|
||||
const isModularParentSku = isDraftGroupModularParentSkuItem(sku)
|
||||
if (!factoryRow) throw new Error(`聚水潭推送缺少定厂资料:${sku.skuCode}`)
|
||||
if (!pricingRow) throw new Error(`聚水潭推送缺少核价资料:${sku.skuCode}`)
|
||||
if (!parseNumber(pricingRow.costPrice)) {
|
||||
if (!pricingRow && !isModularParentSku) throw new Error(`聚水潭推送缺少核价资料:${sku.skuCode}`)
|
||||
if (!isModularParentSku && !parseNumber(pricingRow?.costPrice ?? '')) {
|
||||
throw new Error(`聚水潭推送缺少成本价:${sku.skuCode}`)
|
||||
}
|
||||
|
||||
return {
|
||||
sku,
|
||||
factoryRow,
|
||||
pricingRow,
|
||||
pricingRow: pricingRow ?? { skuId: sku.id, costPrice: '', productTag: '' },
|
||||
measurementRow: measurementRowBySkuId.get(sku.id),
|
||||
}
|
||||
})
|
||||
@ -1129,11 +1131,20 @@ function buildModularCombineItems(
|
||||
.map((row) => {
|
||||
const combineSkuId = row.sku.skuCode.trim().toUpperCase()
|
||||
const items = (row.sku.componentDetails ?? [])
|
||||
.map((component) => ({
|
||||
src_sku_id: component.code.trim().toUpperCase(),
|
||||
.map((component) => {
|
||||
const componentBarcode = component.barcode?.trim().toUpperCase() ?? ''
|
||||
if (!componentBarcode) {
|
||||
throw new Error(
|
||||
`组合商品缺少部件条码:${component.name.trim() || component.code.trim() || combineSkuId}`,
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
src_sku_id: componentBarcode,
|
||||
qty: resolveComponentQuantity(component),
|
||||
sale_price: 0,
|
||||
}))
|
||||
}
|
||||
})
|
||||
.filter((item) => item.src_sku_id)
|
||||
|
||||
if (items.length === 0) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user