feat(bc): sync pricing units2

This commit is contained in:
Christian Vidal Wolf
2026-05-26 10:25:37 +02:00
parent c64ce84e44
commit 74d4d37769
6 changed files with 55 additions and 55 deletions
+2 -1
View File
@@ -15,7 +15,8 @@ BC_ITEMS_WRITE_METHOD="PATCH"
BC_ITEMS_WRITE_URL_TEMPLATE="{{itemsUrl}}('{{itemNo}}')"
BC_ITEMS_WRITE_BODY_TEMPLATE=''
BC_UOM_WRITE_METHOD="PATCH"
BC_UOM_WRITE_URL_TEMPLATE="{{itemUnitsOfMeasureUrl}}('{{itemNo}}')"
BC_UOM_CODE="OUTER"
BC_UOM_WRITE_URL_TEMPLATE="{{itemUnitsOfMeasureUrl}}(itemNo='{{itemNo}}',code='{{itemUnitsCode}}')"
BC_UOM_WRITE_BODY_TEMPLATE=''
# APP_URL: The URL where this applet is hosted.
+1 -1
View File
@@ -22,6 +22,6 @@ View your app in AI Studio: https://ai.studio/apps/bac9908e-4996-4e4c-8ec7-3add5
`BC_WRITE_METHOD`, `BC_WRITE_URL_TEMPLATE`, `BC_WRITE_BODY_TEMPLATE`
5. For the safer BC sync preview/apply flow, you can also set:
`BC_ITEMS_WRITE_METHOD`, `BC_ITEMS_WRITE_URL_TEMPLATE`, `BC_ITEMS_WRITE_BODY_TEMPLATE`,
`BC_UOM_WRITE_METHOD`, `BC_UOM_WRITE_URL_TEMPLATE`, `BC_UOM_WRITE_BODY_TEMPLATE`
`BC_UOM_CODE`, `BC_UOM_WRITE_METHOD`, `BC_UOM_WRITE_URL_TEMPLATE`, `BC_UOM_WRITE_BODY_TEMPLATE`
6. Run the app:
`npm run dev`
+6 -1
View File
@@ -16,8 +16,9 @@ export function getBcConfig(env = process.env) {
itemsWriteMethod: String(env.BC_ITEMS_WRITE_METHOD || legacyWriteMethod).toUpperCase(),
itemsWriteUrlTemplate: env.BC_ITEMS_WRITE_URL_TEMPLATE || legacyWriteUrlTemplate,
itemsWriteBodyTemplate: env.BC_ITEMS_WRITE_BODY_TEMPLATE || null,
itemUnitsCode: env.BC_UOM_CODE || 'OUTER',
itemUnitsWriteMethod: env.BC_UOM_WRITE_METHOD ? String(env.BC_UOM_WRITE_METHOD).toUpperCase() : 'PATCH',
itemUnitsWriteUrlTemplate: env.BC_UOM_WRITE_URL_TEMPLATE || `{{itemUnitsOfMeasureUrl}}('{{itemNo}}')`,
itemUnitsWriteUrlTemplate: env.BC_UOM_WRITE_URL_TEMPLATE || `{{itemUnitsOfMeasureUrl}}(itemNo='{{itemNo}}',code='{{itemUnitsCode}}')`,
itemUnitsWriteBodyTemplate: env.BC_UOM_WRITE_BODY_TEMPLATE || null,
};
}
@@ -30,6 +31,10 @@ export function getItemsUrl(config) {
return `https://api.businesscentral.dynamics.com/v2.0/${config.tenantId}/production/api/craze/integrations/v1.0/companies(${config.companyId})/items`;
}
export function getItemUnitsOfMeasureUrl(config) {
return `https://api.businesscentral.dynamics.com/v2.0/${config.tenantId}/production/api/craze/integrations/v1.0/companies(${config.companyId})/itemUnitsOfMeasure2`;
}
let tokenCache = { token: null, expiresAt: 0 };
export async function getBCToken(config) {
+36 -34
View File
@@ -1,9 +1,5 @@
import crypto from 'crypto';
import { getBcConfig, getBCToken, getItemsUrl } from './bc-runtime.js';
function getItemUnitsOfMeasureUrl(config) {
return `https://api.businesscentral.dynamics.com/v2.0/${config.tenantId}/production/api/craze/integrations/v1.0/companies(${config.companyId})/itemUnitsOfMeasure`;
}
import { getBcConfig, getBCToken, getItemsUrl, getItemUnitsOfMeasureUrl } from './bc-runtime.js';
function findHeaderIndex(headers, patterns) {
const normalized = headers.map(h => String(h || '').toLowerCase());
@@ -89,13 +85,11 @@ export function buildBusinessCentralMappingPreview(headers, row) {
const itemUnitsOfMeasurePayload = {
itemNo: getValue(row, articleNoIdx),
qtyPerUnitOfMeasure6: toBcDecimal(getValue(row, unitsOuterIdx)),
width4: toBcDecimal(getValue(row, innerWIdx)),
length4: toBcDecimal(getValue(row, innerLIdx)),
height4: toBcDecimal(getValue(row, innerHIdx)),
width6: toBcDecimal(getValue(row, outerWIdx)),
length6: toBcDecimal(getValue(row, outerLIdx)),
height6: toBcDecimal(getValue(row, outerHIdx)),
code: 'OUTER',
qtyPerUnitOfMeasure: toBcDecimal(getValue(row, unitsOuterIdx)),
width: toBcDecimal(getValue(row, outerWIdx)),
length: toBcDecimal(getValue(row, outerLIdx)),
height: toBcDecimal(getValue(row, outerHIdx)),
};
return {
@@ -115,13 +109,10 @@ export function buildBusinessCentralMappingPreview(headers, row) {
],
itemUnitsFields: [
makeFieldPreview('Article No.', articleNoIdx, 'itemNo', itemUnitsOfMeasurePayload.itemNo),
makeFieldPreview('Units Outer', unitsOuterIdx, 'qtyPerUnitOfMeasure6', itemUnitsOfMeasurePayload.qtyPerUnitOfMeasure6),
makeFieldPreview('CDU/Inner W (cm)', innerWIdx, 'width4', itemUnitsOfMeasurePayload.width4),
makeFieldPreview('CDU/Inner L (cm)', innerLIdx, 'length4', itemUnitsOfMeasurePayload.length4),
makeFieldPreview('CDU/Inner H (cm)', innerHIdx, 'height4', itemUnitsOfMeasurePayload.height4),
makeFieldPreview('Outer W (cm)', outerWIdx, 'width6', itemUnitsOfMeasurePayload.width6),
makeFieldPreview('Outer L (cm)', outerLIdx, 'length6', itemUnitsOfMeasurePayload.length6),
makeFieldPreview('Outer H (cm)', outerHIdx, 'height6', itemUnitsOfMeasurePayload.height6),
makeFieldPreview('Units Outer', unitsOuterIdx, 'qtyPerUnitOfMeasure', itemUnitsOfMeasurePayload.qtyPerUnitOfMeasure),
makeFieldPreview('Outer W (cm)', outerWIdx, 'width', itemUnitsOfMeasurePayload.width),
makeFieldPreview('Outer L (cm)', outerLIdx, 'length', itemUnitsOfMeasurePayload.length),
makeFieldPreview('Outer H (cm)', outerHIdx, 'height', itemUnitsOfMeasurePayload.height),
],
};
}
@@ -238,13 +229,21 @@ function getItemsSelect() {
function getItemUnitsSelect() {
return [
'itemNo',
'qtyPerUnitOfMeasure6',
'width4',
'length4',
'height4',
'width6',
'length6',
'height6',
'code',
'qtyPerUnitOfMeasure',
'qtyRoundingPrecision',
'length',
'width',
'height',
'cubage',
'weight',
'layerPerPalet2CRZ',
'outerPerLayer2CRZ',
'barCodeCRZ',
'layerPerPaletCRZ',
'outerPerLayerCRZ',
'netWeightCRZ',
'innerTypeBCT',
].join(',');
}
@@ -270,7 +269,7 @@ export async function fetchBusinessCentralSnapshot(config, token, articleNo) {
const { json: itemsJson, etag: itemsEtag } = await fetchJsonOrThrow(itemsUrl, token, 'BC GET items');
const item = Array.isArray(itemsJson.value) && itemsJson.value.length > 0 ? itemsJson.value[0] : null;
const uomFilter = encodeURIComponent(`itemNo eq '${articleNo}'`);
const uomFilter = encodeURIComponent(`itemNo eq '${articleNo}' and code eq '${config.itemUnitsCode || 'OUTER'}'`);
const uomUrl = `${getItemUnitsOfMeasureUrl(config)}?$filter=${uomFilter}&$select=${getItemUnitsSelect()}`;
const { json: uomJson, etag: uomEtag } = await fetchJsonOrThrow(uomUrl, token, 'BC GET itemUnitsOfMeasure');
const itemUnitsOfMeasure = Array.isArray(uomJson.value) && uomJson.value.length > 0 ? uomJson.value[0] : null;
@@ -388,13 +387,15 @@ function renderTemplate(template, context) {
const DECIMAL_FIELDS = new Set([
'minimumOrderQuantity',
'qtyPerUnitOfMeasure6',
'width4',
'length4',
'height4',
'width6',
'length6',
'height6',
'qtyPerUnitOfMeasure',
'length',
'width',
'height',
'cubage',
'weight',
'layerPerPalet2CRZ',
'outerPerLayer2CRZ',
'netWeightCRZ',
]);
const EMPTY_STRING_FIELDS = new Set([
@@ -518,6 +519,7 @@ export async function applyBusinessCentralSync(config, token, headers, row, prev
itemUnitsOfMeasureUrl: getItemUnitsOfMeasureUrl(config),
articleNo: mapping.articleNo,
itemNo: mapping.articleNo,
itemUnitsCode: config.itemUnitsCode || 'OUTER',
systemId: snapshot.item?.systemId || '',
cpnpNo: mapping.itemsPayload.cpnpNo,
itemsPayload: preview.items.desired,
+1 -1
View File
@@ -497,7 +497,7 @@ export function MatrixView({ data, headers, rowStatuses }: MatrixViewProps) {
<div className="rounded-lg border border-slate-700 bg-slate-950/60 p-4">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="text-sm font-semibold text-white">API `itemUnitsOfMeasure` preview</h3>
<h3 className="text-sm font-semibold text-white">API `itemUnitsOfMeasure2` preview</h3>
<p className="text-xs text-slate-400">SKU {bcMappingPreview.articleNo}</p>
</div>
</div>
+9 -17
View File
@@ -81,9 +81,6 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
const cpnpIdx = findHeaderIndex(headers, ['cpnp']);
const unitsOuterIdx = findHeaderIndex(headers, ['units', 'outer']);
const innerWIdx = findHeaderIndex(headers, ['inner', 'w']);
const innerLIdx = findHeaderIndex(headers, ['inner', 'l']);
const innerHIdx = findHeaderIndex(headers, ['inner', 'h']);
const outerWIdx = findHeaderIndex(headers, ['outer', 'w']);
const outerLIdx = findHeaderIndex(headers, ['outer', 'l']);
const outerHIdx = findHeaderIndex(headers, ['outer', 'h']);
@@ -104,13 +101,11 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
const itemUnitsOfMeasurePayload = {
itemNo: getValue(row, articleNoIdx),
qtyPerUnitOfMeasure6: toBcDecimal(getValue(row, unitsOuterIdx)),
width4: toBcDecimal(getValue(row, innerWIdx)),
length4: toBcDecimal(getValue(row, innerLIdx)),
height4: toBcDecimal(getValue(row, innerHIdx)),
width6: toBcDecimal(getValue(row, outerWIdx)),
length6: toBcDecimal(getValue(row, outerLIdx)),
height6: toBcDecimal(getValue(row, outerHIdx)),
code: 'OUTER',
qtyPerUnitOfMeasure: toBcDecimal(getValue(row, unitsOuterIdx)),
width: toBcDecimal(getValue(row, outerWIdx)),
length: toBcDecimal(getValue(row, outerLIdx)),
height: toBcDecimal(getValue(row, outerHIdx)),
};
return {
@@ -130,13 +125,10 @@ export function buildBusinessCentralMappingPreview(headers: string[], row: Excel
],
itemUnitsFields: [
makeFieldPreview('Article No.', articleNoIdx, 'itemNo', itemUnitsOfMeasurePayload.itemNo),
makeFieldPreview('Units Outer', unitsOuterIdx, 'qtyPerUnitOfMeasure6', itemUnitsOfMeasurePayload.qtyPerUnitOfMeasure6),
makeFieldPreview('CDU/Inner W (cm)', innerWIdx, 'width4', itemUnitsOfMeasurePayload.width4),
makeFieldPreview('CDU/Inner L (cm)', innerLIdx, 'length4', itemUnitsOfMeasurePayload.length4),
makeFieldPreview('CDU/Inner H (cm)', innerHIdx, 'height4', itemUnitsOfMeasurePayload.height4),
makeFieldPreview('Outer W (cm)', outerWIdx, 'width6', itemUnitsOfMeasurePayload.width6),
makeFieldPreview('Outer L (cm)', outerLIdx, 'length6', itemUnitsOfMeasurePayload.length6),
makeFieldPreview('Outer H (cm)', outerHIdx, 'height6', itemUnitsOfMeasurePayload.height6),
makeFieldPreview('Units Outer', unitsOuterIdx, 'qtyPerUnitOfMeasure', itemUnitsOfMeasurePayload.qtyPerUnitOfMeasure),
makeFieldPreview('Outer W (cm)', outerWIdx, 'width', itemUnitsOfMeasurePayload.width),
makeFieldPreview('Outer L (cm)', outerLIdx, 'length', itemUnitsOfMeasurePayload.length),
makeFieldPreview('Outer H (cm)', outerHIdx, 'height', itemUnitsOfMeasurePayload.height),
],
};
}