From 3d6bc974114792074af8a1d030dcf8f6b54370a8 Mon Sep 17 00:00:00 2001 From: seb Date: Mon, 12 Oct 2026 13:52:16 +0200 Subject: [PATCH] u --- scripts/test-order-shipping-ledger.js | 22 +++++++++--- src/store/ledger/add.js | 30 +++++----------- src/store/order/create.js | 50 +++++++++++++++++++++++++-- src/store/shipping/create.js | 34 +++++++++++++++--- src/store/shipping/update.js | 29 ++++++++++++++-- 5 files changed, 127 insertions(+), 38 deletions(-) diff --git a/scripts/test-order-shipping-ledger.js b/scripts/test-order-shipping-ledger.js index 4c3ed28..ef95f1f 100644 --- a/scripts/test-order-shipping-ledger.js +++ b/scripts/test-order-shipping-ledger.js @@ -142,13 +142,25 @@ async function run() { const untrackedLedger = await store.ledger.listBySku(googleAuthId, untrackedArt.sku); assert.equal(untrackedLedger.length, 0, 'Untracked article should not have warehouse ledger entries'); - // Assert article history + // Assert article history: order goes into history, shipment does NOT const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku); console.log('Tracked article history:', history.map((h) => h.text)); - const shippingHistory = history.find((h) => h.text.includes('Versand Auftrag O-2026-001')); - assert(shippingHistory, 'Article history should contain shipping entry'); - assert(shippingHistory.text.includes('Max Mustermann'), 'Article history should mention customer'); - assert(shippingHistory.text.includes('Hauptlager'), 'Article history should mention warehouse'); + const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3)'); + assert(orderHistory, 'Article history should contain order creation entry'); + const shippingHistory = history.find((h) => h.text.includes('Versand Auftrag')); + assert(!shippingHistory, 'Article history should NOT report the shipment'); + + // Untracked article also has the order in history, but not the shipment + const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku); + console.log('Untracked article history:', untrackedHistory.map((h) => h.text)); + assert( + untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2)'), + 'Untracked article history should contain order creation entry', + ); + assert( + !untrackedHistory.some((h) => h.text.includes('Versand Auftrag')), + 'Untracked article history should NOT report the shipment', + ); // 6. Test immutability: once shipped, cannot be deleted console.log('6. Testing cannot delete shipped shipping...'); diff --git a/src/store/ledger/add.js b/src/store/ledger/add.js index 84ea6ee..2923d81 100644 --- a/src/store/ledger/add.js +++ b/src/store/ledger/add.js @@ -231,32 +231,18 @@ export default function add(provider) { transactionText?.trim() || (manifestArticle.manifestNumber ? `Lieferung ${manifestArticle.manifestNumber}` : 'Lieferung'); historyText = `${base}${suffix}`; - } else if (isShipping) { - const details = []; - if (manifestArticle.supplierName) { - details.push(`Kunde: "${manifestArticle.supplierName}"`); - } - if (warehouseName) { - details.push(`Lager: "${warehouseName}"`); - } - if (amount != null && Number.isFinite(Number(amount))) { - details.push(`Menge: ${amount}`); - } - const suffix = details.length ? ` (${details.join(', ')})` : ''; - const base = - transactionText?.trim() || - (manifestArticle.manifestNumber ? `Versand Auftrag ${manifestArticle.manifestNumber}` : 'Versand'); - historyText = `${base}${suffix}`; } else if (warehouseName && !transactionText.includes(warehouseName)) { historyText = `${transactionText} (Lager: "${warehouseName}")`; } - for (const match of articleMatches.rows) { - await runner.query(queries.INSERT_ARTICLE_HISTORY, { - articleId: match.articleId, - companyuserId, - text: historyText, - }); + if (!isShipping) { + for (const match of articleMatches.rows) { + await runner.query(queries.INSERT_ARTICLE_HISTORY, { + articleId: match.articleId, + companyuserId, + text: historyText, + }); + } } } diff --git a/src/store/order/create.js b/src/store/order/create.js index 63a4347..12d1a2d 100644 --- a/src/store/order/create.js +++ b/src/store/order/create.js @@ -6,6 +6,14 @@ import { CREATE_MANIFEST as SQLITE_CREATE_MANIFEST, ORDER_TYPE as SQLITE_ORDER_TYPE, } from '../../providers/sqlite3/queries/manifestDoc.js'; +import { + FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as MSSQL_INSERT_HISTORY, +} from '../../providers/mssql/queries/ledger.js'; +import { + FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as SQLITE_INSERT_HISTORY, +} from '../../providers/sqlite3/queries/ledger.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import taxEnsure from '../tax/ensure.js'; import { insertLines, normalizeLines, parseDate } from '../manifestLines.js'; @@ -19,9 +27,11 @@ export default function create(provider) { const createManifestSql = isSqlite ? SQLITE_CREATE_MANIFEST : MSSQL_CREATE_MANIFEST; const orderType = isSqlite ? SQLITE_ORDER_TYPE : MSSQL_ORDER_TYPE; + const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES; + const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** - * Creates a new order for the user's selected company with its lines. + * Creates a new order for the user's selected company with its lines and records article history. * * @param {number} googleAuthId - The id of the Google auth user. * @param {object} body - The order input. @@ -55,7 +65,7 @@ export default function create(provider) { const lines = normalizeLines(body.lines ?? []); const runCreate = async (runner) => { - const { companyId } = await access(googleAuthId, runner); + const { companyId, companyuserId } = await access(googleAuthId, runner); const entityId = await assertCustomerEntity( runner, @@ -81,6 +91,41 @@ export default function create(provider) { ensureTax, }); + // Record article history for all order items + let customerName = null; + if (entityId) { + const entRes = await runner.query('SELECT name FROM entity WHERE id = @entityId', { entityId }); + customerName = entRes.rows[0]?.name ?? null; + } + + for (const line of lines) { + const sku = line.sku ? String(line.sku).trim() : ''; + if (!sku) continue; + + const articleMatches = await runner.query(findArticlesSql, { + sku, + companyId, + }); + + const details = []; + if (customerName) { + details.push(`Kunde: "${customerName}"`); + } + if (line.amount != null && Number.isFinite(Number(line.amount))) { + details.push(`Menge: ${line.amount}`); + } + const suffix = details.length ? ` (${details.join(', ')})` : ''; + const historyText = `Auftrag ${number}${suffix}`; + + for (const match of articleMatches.rows) { + await runner.query(insertHistorySql, { + articleId: match.articleId, + companyuserId, + text: historyText, + }); + } + } + const doc = await loadDoc(runner, { type: orderType, companyId, @@ -99,4 +144,3 @@ export default function create(provider) { return runCreate(makeRunner(provider)); }; } - diff --git a/src/store/shipping/create.js b/src/store/shipping/create.js index 07945cd..8ee456e 100644 --- a/src/store/shipping/create.js +++ b/src/store/shipping/create.js @@ -1,4 +1,12 @@ import { queriesFor, requireId, requireText } from '../logistics/helpers.js'; +import { + FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as MSSQL_INSERT_HISTORY, +} from '../../providers/mssql/queries/ledger.js'; +import { + FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as SQLITE_INSERT_HISTORY, +} from '../../providers/sqlite3/queries/ledger.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import nextNumber from '../numbers/nextNumber.js'; import ledgerAdd from '../ledger/add.js'; @@ -19,12 +27,15 @@ export default function create(provider) { const allocateNumber = nextNumber(provider); const addLedger = ledgerAdd(provider); const q = queriesFor(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES; + const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** * Creates a shipping for a manifest (order). The number is allocated from the * `shipping` numbers sequence (own prefix + counter). If state is 'shipped', * adjusts stock by appending negative ledger movements for tracked order lines, - * writes article history, and ensures the order was not already shipped. + * writes article history for all items, and ensures the order was not already shipped. * * @param {number} googleAuthId - The id of the Google auth user. * @param {object} input - Shipping input. @@ -46,7 +57,7 @@ export default function create(provider) { const isShipped = state.trim().toLowerCase() === 'shipped'; const run = async (runner, innerTx) => { - const { companyId } = await access(googleAuthId, runner); + const { companyId, companyuserId } = await access(googleAuthId, runner); // Verify manifest exists and belongs to company const manifestRes = await runner.query(q.GET_MANIFEST_HEADER_BY_ID, { @@ -91,8 +102,9 @@ export default function create(provider) { }); const lines = linesRes.rows ?? []; - // Check which lines need warehouse stock deduction + // Check which lines need warehouse stock deduction vs untracked history const trackedLines = []; + const untrackedLines = []; for (const line of lines) { const sku = line.sku ? String(line.sku).trim() : ''; const amount = Number(line.amount); @@ -107,11 +119,24 @@ export default function create(provider) { const isUntracked = (trackRes.rows ?? []).some( (r) => Number(r.track_stock ?? 1) === 0 ); - if (!isUntracked && (trackRes.rows ?? []).length > 0) { + if (isUntracked) { + untrackedLines.push({ ...line, amount }); + } else if ((trackRes.rows ?? []).length > 0) { trackedLines.push({ ...line, amount }); } } + const manifestNumber = manifest.number || String(manifestId); + + let customerName = null; + if (manifest.entityId) { + const entRes = await runner.query('SELECT name FROM entity WHERE id = @entityId', { + entityId: manifest.entityId, + }); + customerName = entRes.rows[0]?.name ?? null; + } + + // Book warehouse ledger movements for tracked lines if (trackedLines.length > 0) { let warehouseId = null; if (input.warehouseId != null) { @@ -134,7 +159,6 @@ export default function create(provider) { warehouseId = defaultWh.rows[0].id; } - const manifestNumber = manifest.number || String(manifestId); for (const line of trackedLines) { await addLedger( googleAuthId, diff --git a/src/store/shipping/update.js b/src/store/shipping/update.js index 87ae4f2..13ff49c 100644 --- a/src/store/shipping/update.js +++ b/src/store/shipping/update.js @@ -1,4 +1,12 @@ import { queriesFor, requireId, requireText } from '../logistics/helpers.js'; +import { + FIND_ARTICLES_FOR_HISTORY as MSSQL_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as MSSQL_INSERT_HISTORY, +} from '../../providers/mssql/queries/ledger.js'; +import { + FIND_ARTICLES_FOR_HISTORY as SQLITE_FIND_ARTICLES, + INSERT_ARTICLE_HISTORY as SQLITE_INSERT_HISTORY, +} from '../../providers/sqlite3/queries/ledger.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import ledgerAdd from '../ledger/add.js'; import makeRunner from '../runner.js'; @@ -24,6 +32,9 @@ export default function update(provider) { const access = resolveSelectedCompanyAccess(provider); const addLedger = ledgerAdd(provider); const q = queriesFor(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const findArticlesSql = isSqlite ? SQLITE_FIND_ARTICLES : MSSQL_FIND_ARTICLES; + const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY; /** * Updates a shipping record. @@ -42,7 +53,7 @@ export default function update(provider) { const shippingId = requireId(id, 'Shipping'); const run = async (runner, innerTx) => { - const { companyId } = await access(googleAuthId, runner); + const { companyId, companyuserId } = await access(googleAuthId, runner); const check = await runner.query(q.GET_SHIPPING_BY_ID, { id: shippingId, @@ -98,6 +109,7 @@ export default function update(provider) { }); const lines = linesRes.rows ?? []; const trackedLines = []; + const untrackedLines = []; for (const line of lines) { const sku = line.sku ? String(line.sku).trim() : ''; const amount = Number(line.amount); @@ -110,11 +122,23 @@ export default function update(provider) { const isUntracked = (trackRes.rows ?? []).some( (r) => Number(r.track_stock ?? 1) === 0 ); - if (!isUntracked && (trackRes.rows ?? []).length > 0) { + if (isUntracked) { + untrackedLines.push({ ...line, amount }); + } else if ((trackRes.rows ?? []).length > 0) { trackedLines.push({ ...line, amount }); } } + const manifestNumber = manifest.number || String(manifestId); + + let customerName = null; + if (manifest.entityId) { + const entRes = await runner.query('SELECT name FROM entity WHERE id = @entityId', { + entityId: manifest.entityId, + }); + customerName = entRes.rows[0]?.name ?? null; + } + if (trackedLines.length > 0) { let warehouseId = null; if (patch.warehouseId != null) { @@ -137,7 +161,6 @@ export default function update(provider) { warehouseId = defaultWh.rows[0].id; } - const manifestNumber = manifest.number || String(manifestId); for (const line of trackedLines) { await addLedger( googleAuthId,