fix: read MKH details from official BOM tables
This commit is contained in:
parent
3bf7b9f208
commit
8e5d427b3a
@ -11,12 +11,19 @@
|
||||
// AND other_3 = N'简尚-崔桥工厂品牌仓' (当前 4408 条)
|
||||
// · 形态 MKH + 款号 + 序列,例 MKHDNZ0409260600198
|
||||
//
|
||||
// ② 配件明细(BOM)—— 业务中台组合商品表(主来源)
|
||||
// ② 配件明细(BOM)—— 聚水潭商品 BOM(主来源)
|
||||
// rpt.jst_skuBomInfo / rpt.jst_skuBomItem
|
||||
// · jst_skuBomInfo.sku_id = MKH 成品编码
|
||||
// · jst_skuBomItem.map_outer_sku_id = 配件编码,rm_qty = 配件数量
|
||||
// · 两表当前分别约 4,257 / 49,012 行;远端副本暂未提供 sku_id 索引,
|
||||
// 因此只读取必要列并在进程内短时缓存,避免分页和下钻重复扫描。
|
||||
//
|
||||
// ③ 业务中台组合商品表(降级来源)
|
||||
// JSDataMiddlePlatform.dbo.jst_comboProductItem
|
||||
// · combine_sku_id = MKH 成品编码,src_sku_id = 配件编码,qty = 配件数量
|
||||
// · 当前实测 1,406 个 MKH 成品、3,022 条配件关系、159 个不同配件编码
|
||||
//
|
||||
// ③ 产品资料系统数据库(PostgreSQL / furniture3d_prod)作为补充来源
|
||||
// ④ 产品资料系统数据库(PostgreSQL / furniture3d_prod)作为补充来源
|
||||
// public."IntakeNonIntegratedProduct".payload -> 'skuItems' -> 'componentDetails'
|
||||
// · 每一项形如 { code, name, role/assetType, barcode, quantity }
|
||||
// · 例:MKH-DNZ0384 → [{ barcode: DNZ038501N1437V01, quantity: 1, role: 板材 },
|
||||
@ -30,10 +37,10 @@
|
||||
// payload 里的 bomDetails / factoryRows / pricingRows 属成本与工厂行,结构不同
|
||||
// (无 SKU 条码),也不能直接当配件 BOM。
|
||||
//
|
||||
// ④ 配件仓位 —— 业务库 SKU 档案中的库位字段;
|
||||
// ⑤ 配件仓位 —— 业务库 SKU 档案中的库位字段;
|
||||
// 业务库 jst_sku.bin / other_bin 全库为空,故多数配件显示「未维护」。
|
||||
//
|
||||
// ⑤ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。
|
||||
// ⑥ 配件库存 —— 业务库 rpt.jst_inventoryInfo 按 sku_id 汇总。
|
||||
|
||||
const cloudDatabase = require('../config/cloudDatabase');
|
||||
const productDb = require('../config/productInformationDatabase');
|
||||
@ -41,8 +48,11 @@ const productDb = require('../config/productInformationDatabase');
|
||||
const DEFAULT_PAGE_SIZE = 50;
|
||||
const MAX_PAGE_SIZE = 200;
|
||||
const BOM_CACHE_TTL_MS = 2 * 60 * 1000;
|
||||
const OFFICIAL_BOM_DETAIL_CACHE_TTL_MS = 15 * 60 * 1000;
|
||||
|
||||
let businessBomCache = null;
|
||||
let officialBomIndexCache = null;
|
||||
const officialBomDetailCache = new Map();
|
||||
let productBomCache = null;
|
||||
|
||||
// MKH 商品编码 = 模块化成品
|
||||
@ -158,7 +168,158 @@ function groupBusinessBomRows(rows = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从业务中台读取 MKH 组合商品 BOM。该表是 MKH 下钻明细的主数据源。
|
||||
* 将聚水潭正式商品 BOM 明细整理成按成品编码查找的结构。
|
||||
* 同一 BOM 中重复的配件编码会合并数量;外部配件编码为空时忽略该行,
|
||||
* 避免把聚水潭内部数字 ID 当成可展示、可查库存的配件编码。
|
||||
*/
|
||||
function groupOfficialBomRows(rows = []) {
|
||||
const grouped = new Map();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const skuCode = toText(row.skuId);
|
||||
const skuKey = normalizeCode(skuCode);
|
||||
const componentCode = toText(row.componentSkuId);
|
||||
const componentKey = normalizeCode(componentCode);
|
||||
if (!skuCode || !skuKey || !componentCode || !componentKey) return;
|
||||
|
||||
if (!grouped.has(skuKey)) {
|
||||
grouped.set(skuKey, {
|
||||
skuCode,
|
||||
components: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
const entry = grouped.get(skuKey);
|
||||
const previous = entry.components.get(componentKey);
|
||||
const qty = toNumber(row.qty) ?? 1;
|
||||
if (previous) {
|
||||
previous.qty += qty;
|
||||
if (!previous.name) previous.name = toText(row.componentName) || componentCode;
|
||||
return;
|
||||
}
|
||||
|
||||
entry.components.set(componentKey, {
|
||||
barcode: componentCode,
|
||||
code: componentCode,
|
||||
name: toText(row.componentName) || componentCode,
|
||||
role: null,
|
||||
qty,
|
||||
});
|
||||
});
|
||||
|
||||
const bySku = new Map();
|
||||
const entries = [];
|
||||
grouped.forEach((entry, skuKey) => {
|
||||
const components = Array.from(entry.components.values());
|
||||
bySku.set(skuKey, components);
|
||||
entries.push({
|
||||
skuCode: entry.skuCode,
|
||||
spu: null,
|
||||
productName: entry.skuCode,
|
||||
components,
|
||||
});
|
||||
});
|
||||
|
||||
return { bySku, bySpuPrefix: [], entries };
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取聚水潭正式 BOM 主表中的成品编码索引。
|
||||
* 主表只有约四千行,只传输 sku_id;用于全局统计与来源优先级判断。
|
||||
*/
|
||||
async function loadOfficialBomIndex() {
|
||||
const now = Date.now();
|
||||
if (officialBomIndexCache?.value && officialBomIndexCache.expiresAt > now) {
|
||||
return officialBomIndexCache.value;
|
||||
}
|
||||
if (officialBomIndexCache?.pending) return officialBomIndexCache.pending;
|
||||
|
||||
const pending = cloudDatabase.query(
|
||||
`SELECT h.sku_id AS skuId
|
||||
FROM rpt.jst_skuBomInfo h WITH (NOLOCK)
|
||||
WHERE h.sku_id LIKE N'MKH%'`,
|
||||
{},
|
||||
{ requestTimeout: 180000 }
|
||||
).then((rows) => {
|
||||
const bySku = new Map();
|
||||
rows.forEach((row) => {
|
||||
const key = normalizeCode(row.skuId);
|
||||
if (key) bySku.set(key, []);
|
||||
});
|
||||
return { bySku, bySpuPrefix: [], entries: [] };
|
||||
});
|
||||
|
||||
officialBomIndexCache = { pending, value: null, expiresAt: 0 };
|
||||
try {
|
||||
const value = await pending;
|
||||
officialBomIndexCache = { pending: null, value, expiresAt: Date.now() + BOM_CACHE_TTL_MS };
|
||||
return value;
|
||||
} catch (error) {
|
||||
officialBomIndexCache = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前页面或单品所需 SKU 批量读取正式 BOM 明细。
|
||||
* 单次最多接收 MAX_PAGE_SIZE 个 SKU,并缓存每个 SKU 的结果(包括空结果),
|
||||
* 避免把远端约 4.9 万条明细在每次缓存刷新时全部传回应用服务器。
|
||||
*/
|
||||
async function loadOfficialBomForSkus(skuIds = []) {
|
||||
const requested = Array.from(new Map(
|
||||
skuIds
|
||||
.map((skuId) => [normalizeCode(skuId), toText(skuId)])
|
||||
.filter(([key, value]) => key && value)
|
||||
).entries()).slice(0, MAX_PAGE_SIZE);
|
||||
const now = Date.now();
|
||||
const missing = requested.filter(([key]) => {
|
||||
const cached = officialBomDetailCache.get(key);
|
||||
return !cached || cached.expiresAt <= now;
|
||||
});
|
||||
|
||||
if (missing.length > 0) {
|
||||
const params = {};
|
||||
const placeholders = missing.map(([, skuId], index) => {
|
||||
params[`officialSku${index}`] = skuId;
|
||||
return `@officialSku${index}`;
|
||||
});
|
||||
const rows = await cloudDatabase.query(
|
||||
`SELECT
|
||||
i.sku_id AS skuId,
|
||||
i.map_outer_sku_id AS componentSkuId,
|
||||
i.map_name AS componentName,
|
||||
i.rm_qty AS qty
|
||||
FROM rpt.jst_skuBomItem i WITH (NOLOCK)
|
||||
WHERE i.sku_id IN (${placeholders.join(', ')})
|
||||
AND NULLIF(LTRIM(RTRIM(i.map_outer_sku_id)), N'') IS NOT NULL
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM rpt.jst_skuBomInfo h WITH (NOLOCK)
|
||||
WHERE h.sku_id = i.sku_id
|
||||
)`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
);
|
||||
const grouped = groupOfficialBomRows(rows);
|
||||
const expiresAt = Date.now() + OFFICIAL_BOM_DETAIL_CACHE_TTL_MS;
|
||||
missing.forEach(([key]) => {
|
||||
officialBomDetailCache.set(key, {
|
||||
components: grouped.bySku.get(key) || [],
|
||||
expiresAt,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const bySku = new Map();
|
||||
requested.forEach(([key]) => {
|
||||
const cached = officialBomDetailCache.get(key);
|
||||
if (cached?.components?.length) bySku.set(key, cached.components);
|
||||
});
|
||||
return { bySku, bySpuPrefix: [], entries: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* 从业务中台读取 MKH 组合商品 BOM,作为正式商品 BOM 缺失时的降级来源。
|
||||
*/
|
||||
async function loadBusinessBom() {
|
||||
const now = Date.now();
|
||||
@ -312,7 +473,10 @@ function matchComponents(bom, skuId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function matchBomSources(businessBom, productBom, skuId) {
|
||||
function matchBomSources(officialBom, businessBom, productBom, skuId) {
|
||||
const officialMatch = matchComponents(officialBom, skuId);
|
||||
if (officialMatch) return { ...officialMatch, dataSource: 'official_bom' };
|
||||
|
||||
const businessMatch = matchComponents(businessBom, skuId);
|
||||
if (businessMatch) return { ...businessMatch, dataSource: 'business_bom' };
|
||||
|
||||
@ -322,9 +486,14 @@ function matchBomSources(businessBom, productBom, skuId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function summarizeConfiguredBoms(businessBom, productBom) {
|
||||
const summary = { business: 0, product: 0, total: 0 };
|
||||
function summarizeConfiguredBoms(officialBom, businessBom, productBom) {
|
||||
const summary = { official: 0, business: 0, product: 0, total: 0 };
|
||||
(businessBom?.existingSkuKeys || new Set()).forEach((skuKey) => {
|
||||
if (matchComponents(officialBom, skuKey)) {
|
||||
summary.official += 1;
|
||||
summary.total += 1;
|
||||
return;
|
||||
}
|
||||
if (matchComponents(businessBom, skuKey)) {
|
||||
summary.business += 1;
|
||||
summary.total += 1;
|
||||
@ -358,34 +527,36 @@ async function getMkhBomList(query = {}) {
|
||||
const sortOrder = String(query.sortOrder || 'ASC').toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
|
||||
const orderSql = sortField === 'skuId' ? `s.sku_id ${sortOrder}` : `s.sku_id ASC`;
|
||||
|
||||
const [businessBom, productBom] = await Promise.all([
|
||||
const offset = (page - 1) * pageSize;
|
||||
const [businessRows, totalRows, officialBomIndex, businessBom, productBom] = await Promise.all([
|
||||
cloudDatabase.query(
|
||||
`SELECT
|
||||
s.sku_id AS skuId,
|
||||
s.name AS productName,
|
||||
s.category AS category,
|
||||
s.other_10 AS material
|
||||
${whereSql}
|
||||
ORDER BY ${orderSql}
|
||||
OFFSET ${offset} ROWS FETCH NEXT ${pageSize} ROWS ONLY`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
cloudDatabase.query(
|
||||
`SELECT COUNT_BIG(*) AS total ${whereSql}`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
),
|
||||
loadOfficialBomIndex(),
|
||||
loadBusinessBom(),
|
||||
loadPgBom(),
|
||||
]);
|
||||
const bomSummary = summarizeConfiguredBoms(businessBom, productBom);
|
||||
const offset = (page - 1) * pageSize;
|
||||
const businessRows = await cloudDatabase.query(
|
||||
`SELECT
|
||||
s.sku_id AS skuId,
|
||||
s.name AS productName,
|
||||
s.category AS category,
|
||||
s.other_10 AS material
|
||||
${whereSql}
|
||||
ORDER BY ${orderSql}
|
||||
OFFSET ${offset} ROWS FETCH NEXT ${pageSize} ROWS ONLY`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
);
|
||||
|
||||
const [totalRow] = await cloudDatabase.query(
|
||||
`SELECT COUNT_BIG(*) AS total ${whereSql}`,
|
||||
params,
|
||||
{ requestTimeout: 180000 }
|
||||
);
|
||||
const officialBom = await loadOfficialBomForSkus(businessRows.map((row) => row.skuId));
|
||||
const bomSummary = summarizeConfiguredBoms(officialBomIndex, businessBom, productBom);
|
||||
const [totalRow] = totalRows;
|
||||
|
||||
const mapped = businessRows.map((row) => {
|
||||
const skuId = toText(row.skuId);
|
||||
const matched = matchBomSources(businessBom, productBom, skuId);
|
||||
const matched = matchBomSources(officialBom, businessBom, productBom, skuId);
|
||||
const components = matched?.components || [];
|
||||
|
||||
return {
|
||||
@ -406,6 +577,7 @@ async function getMkhBomList(query = {}) {
|
||||
rows: mapped,
|
||||
total: Number(totalRow?.total || 0),
|
||||
bomRowCount: bomSummary.total,
|
||||
bomOfficialRowCount: bomSummary.official,
|
||||
bomBusinessRowCount: bomSummary.business,
|
||||
bomProductDbRowCount: bomSummary.product,
|
||||
bomExtraCount: 0,
|
||||
@ -448,11 +620,12 @@ async function getMkhBomDetail(mkhSkuId) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const [businessBom, productBom] = await Promise.all([
|
||||
const [officialBom, businessBom, productBom] = await Promise.all([
|
||||
loadOfficialBomForSkus([skuId]),
|
||||
loadBusinessBom(),
|
||||
loadPgBom(),
|
||||
]);
|
||||
const matched = matchBomSources(businessBom, productBom, skuId);
|
||||
const matched = matchBomSources(officialBom, businessBom, productBom, skuId);
|
||||
const components = matched?.components || [];
|
||||
|
||||
const codes = components.map((item) => item.barcode).filter(Boolean);
|
||||
@ -519,6 +692,7 @@ module.exports = {
|
||||
getMkhBomDetail,
|
||||
_private: {
|
||||
normalizeCode,
|
||||
groupOfficialBomRows,
|
||||
groupBusinessBomRows,
|
||||
matchComponents,
|
||||
matchBomSources,
|
||||
|
||||
@ -5,6 +5,46 @@ const assert = require('node:assert/strict');
|
||||
|
||||
const { _private } = require('../src/services/mkhBomService');
|
||||
|
||||
const emptyBom = () => ({ bySku: new Map(), bySpuPrefix: [], entries: [] });
|
||||
|
||||
test('official Jushuitan BOM rows use external component codes and consolidate duplicates', () => {
|
||||
const bom = _private.groupOfficialBomRows([
|
||||
{
|
||||
skuId: 'MKHDNZ0409260600022',
|
||||
componentSkuId: 'DNZ039804N1410V01',
|
||||
componentName: '桌面',
|
||||
qty: '1.000000',
|
||||
},
|
||||
{
|
||||
skuId: 'MKHDNZ0409260600022',
|
||||
componentSkuId: 'DNZ040928N1440V01',
|
||||
componentName: 'K脚架',
|
||||
qty: '1.000000',
|
||||
},
|
||||
{
|
||||
skuId: 'MKHDNZ0409260600022',
|
||||
componentSkuId: 'DNZ040928N1440V01',
|
||||
componentName: 'K脚架',
|
||||
qty: '1.000000',
|
||||
},
|
||||
{
|
||||
skuId: 'MKHDNZ0409260600022',
|
||||
componentSkuId: ' ',
|
||||
componentName: '无外部编码的内部物料',
|
||||
qty: 1,
|
||||
},
|
||||
]);
|
||||
|
||||
const matched = _private.matchComponents(bom, 'MKHDNZ0409260600022');
|
||||
assert.deepEqual(
|
||||
matched.components.map((item) => [item.barcode, item.name, item.qty]),
|
||||
[
|
||||
['DNZ039804N1410V01', '桌面', 1],
|
||||
['DNZ040928N1440V01', 'K脚架', 2],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
test('business MKH BOM rows are grouped for drill-down and match codes without separators', () => {
|
||||
const bom = _private.groupBusinessBomRows([
|
||||
{
|
||||
@ -58,11 +98,30 @@ test('business BOM takes precedence while product database BOM remains a fallbac
|
||||
entries: [],
|
||||
};
|
||||
|
||||
const matched = _private.matchBomSources(businessBom, productBom, 'MKHA1');
|
||||
const matched = _private.matchBomSources(emptyBom(), businessBom, productBom, 'MKHA1');
|
||||
assert.equal(matched.dataSource, 'business_bom');
|
||||
assert.equal(matched.components[0].barcode, 'BUSINESS-PART');
|
||||
});
|
||||
|
||||
test('official Jushuitan BOM takes precedence over all fallback sources', () => {
|
||||
const officialBom = _private.groupOfficialBomRows([
|
||||
{ skuId: 'MKH-A-1', componentSkuId: 'OFFICIAL-PART', qty: 2 },
|
||||
]);
|
||||
const businessBom = _private.groupBusinessBomRows([
|
||||
{ comboSkuId: 'MKH-A-1', componentSkuId: 'BUSINESS-PART', qty: 1 },
|
||||
]);
|
||||
const productBom = {
|
||||
bySku: new Map([['MKHA1', [{ barcode: 'PRODUCT-PART', qty: 1 }]]]),
|
||||
bySpuPrefix: [],
|
||||
entries: [],
|
||||
};
|
||||
|
||||
const matched = _private.matchBomSources(officialBom, businessBom, productBom, 'MKHA1');
|
||||
assert.equal(matched.dataSource, 'official_bom');
|
||||
assert.equal(matched.components[0].barcode, 'OFFICIAL-PART');
|
||||
assert.equal(matched.components[0].qty, 2);
|
||||
});
|
||||
|
||||
test('BOM summary excludes combinations outside the designated warehouse SKU set', () => {
|
||||
const businessBom = _private.groupBusinessBomRows([
|
||||
{ comboSkuId: 'MKH-A-1', componentSkuId: 'BUSINESS-PART', qty: 1 },
|
||||
@ -75,8 +134,13 @@ test('BOM summary excludes combinations outside the designated warehouse SKU set
|
||||
entries: [],
|
||||
};
|
||||
|
||||
assert.deepEqual(_private.summarizeConfiguredBoms(businessBom, productBom), {
|
||||
business: 1,
|
||||
const officialBom = _private.groupOfficialBomRows([
|
||||
{ skuId: 'MKH-A-1', componentSkuId: 'OFFICIAL-PART', qty: 1 },
|
||||
]);
|
||||
|
||||
assert.deepEqual(_private.summarizeConfiguredBoms(officialBom, businessBom, productBom), {
|
||||
official: 1,
|
||||
business: 0,
|
||||
product: 1,
|
||||
total: 2,
|
||||
});
|
||||
|
||||
@ -75,7 +75,7 @@ const PrintPreview = ({ data, onClose }) => {
|
||||
{items.length === 0 ? (
|
||||
<tr>
|
||||
<td className="mkh-print-sku">{text(data.skuId)}</td>
|
||||
<td colSpan={3} className="mkh-muted">该 MKH 商品的配件明细尚未录入「产品资料系统数据库」</td>
|
||||
<td colSpan={3} className="mkh-muted">该 MKH 商品尚无可用的聚水潭商品 BOM 明细</td>
|
||||
</tr>
|
||||
) : null}
|
||||
</tbody>
|
||||
@ -83,7 +83,7 @@ const PrintPreview = ({ data, onClose }) => {
|
||||
|
||||
<div className="mkh-print-foot">
|
||||
<span>
|
||||
MKH 商品编码与组合配件取自业务库;「产品资料系统数据库」中维护的配件 BOM 作为补充;
|
||||
MKH 商品编码取自指定仓库商品主档;配件优先取聚水潭商品 BOM,业务组合 BOM 与产品资料系统作为补充;
|
||||
配件仓位取库位主数据,未维护时显示「未维护」。
|
||||
</span>
|
||||
</div>
|
||||
@ -286,8 +286,8 @@ const MkhBomData = () => {
|
||||
{items.length === 0 ? (
|
||||
<div className="mkh-detail-tip">
|
||||
<Text type="secondary">
|
||||
该 MKH 商品在业务组合 BOM 和「产品资料系统数据库」中均暂无配件明细。
|
||||
上游维护好该商品的组合配件后,此处会自动显示配件明细、数量与仓位。
|
||||
该 MKH 商品在聚水潭商品 BOM、业务组合 BOM 和「产品资料系统数据库」中均暂无配件明细。
|
||||
上游维护好该商品的正式 BOM 后,此处会自动显示配件明细、数量与仓位。
|
||||
</Text>
|
||||
</div>
|
||||
) : null}
|
||||
@ -338,7 +338,7 @@ const MkhBomData = () => {
|
||||
</Text>
|
||||
<Text type="secondary" className="mkh-note">
|
||||
数据口径:MKH 商品编码仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”的成品(当前 4408 条);
|
||||
配件明细与数量优先取自业务中台组合商品 BOM,产品资料系统数据库(PostgreSQL / furniture3d_prod)中维护的配件明细作为补充;
|
||||
配件明细与数量优先取自聚水潭商品 BOM,业务中台组合商品 BOM 与产品资料系统数据库中维护的配件作为补充;
|
||||
无指定仓库商品档案的 BOM 组合编码不会进入列表;配件仓位未维护时显示「未维护」。
|
||||
</Text>
|
||||
</div>
|
||||
@ -367,14 +367,14 @@ const MkhBomData = () => {
|
||||
showIcon
|
||||
message={data.bomRowCount > 0
|
||||
? `已接入 ${data.bomRowCount} 个模块化商品的配件 BOM`
|
||||
: '业务组合 BOM 和产品资料系统数据库中尚无可用的模块化商品配件明细'}
|
||||
: '聚水潭商品 BOM 及补充来源中尚无可用的模块化商品配件明细'}
|
||||
description={(
|
||||
<span>
|
||||
MKH 商品编码({data.total || 0} 条)仅取自业务库 jst_sku 中“简尚-崔桥工厂品牌仓”;
|
||||
配件明细与数量取自业务组合 BOM({data.bomBusinessRowCount || 0} 个),
|
||||
「产品资料系统数据库」维护的 BOM({data.bomProductDbRowCount || 0} 个)作为补充。
|
||||
配件明细优先取自聚水潭商品 BOM({data.bomOfficialRowCount || 0} 个),
|
||||
业务组合 BOM({data.bomBusinessRowCount || 0} 个)与产品资料系统({data.bomProductDbRowCount || 0} 个)作为补充。
|
||||
BOM 中没有指定仓库商品档案的组合编码不会加入商品列表。
|
||||
在资料库里补充配件明细后,本页「BOM 状态 / 配件种类 / 配件总数量」与展开明细会自动更新。
|
||||
上游补充正式 BOM 后,本页「BOM 状态 / 配件种类 / 配件总数量」与展开明细会自动更新。
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user