Merge remote-tracking branch 'origin/codex/xuxiake-mainline' into codex/wuyongjie-ai-render-texture-resign

This commit is contained in:
WuYongJie 2026-06-17 18:38:47 +08:00
commit d5a1427fff
9 changed files with 304 additions and 24 deletions

View File

@ -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={
selectedSeriesProductBasicInfoHref ||
(selectedDraftGroup
? `/intake?draftGroupId=${selectedDraftGroup.id}&productDevelopmentKind=series`
: '/intake?productDevelopmentKind=series')
}
<button
type="button"
onClick={() => {
window.location.assign(
selectedSeriesProductBasicInfoHref ||
(selectedDraftGroup
? `/intake?draftGroupId=${selectedDraftGroup.id}&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>

View File

@ -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({

View File

@ -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;

View File

@ -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(

View File

@ -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,7 +256,9 @@ 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"
>

View File

@ -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),
)
);

View File

@ -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) {

View File

@ -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

View File

@ -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(),
qty: resolveComponentQuantity(component),
sale_price: 0,
}))
.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) {