diff --git a/scripts/test-delivery-link-check.js b/scripts/test-delivery-link-check.js new file mode 100644 index 0000000..cb80679 --- /dev/null +++ b/scripts/test-delivery-link-check.js @@ -0,0 +1,145 @@ +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import LibDashStore from '../src/index.js'; +import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; + +async function run() { + console.log('--- Testing Delivery Link Check & Sync in dashdbschema ---'); + + const tempDir = mkdtempSync(join(tmpdir(), 'test-delivery-link-check-')); + const dbFile = join(tempDir, 'test.db'); + const storageDir = join(tempDir, 'storage'); + + const provider = new BetterSqlite3Provider({ + filename: dbFile, + storageDir, + }); + + await provider.initialize(); + const store = await new LibDashStore(provider); + + try { + const googleAuthId = await store.google_auth.add('link-check-test@example.com'); + const company = await store.company.create({ + name: 'Link Check Corp', + iso639: 'de', + taxrate: 19, + reducedtaxrate: 7, + googleAuthId, + }); + await store.google_auth.setSelectedCompany(googleAuthId, company.id); + + await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); + + const supplier = await store.supplier.create(googleAuthId, { + name: 'eks', + }); + + const art = await store.article.createUnderParent({ + googleAuthId, + parentId: company.rootCategoryId, + name: 'Club Mate 0,5l', + track_stock: 1, + }); + + // Assign supplier with default pu=1, without EK + await store.articlesupplier.create(googleAuthId, art.sku, { + entityId: supplier.id, + sku: 'mate', + pu: 1, + }); + + let asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku); + assert.equal(asup.length, 1); + assert.equal(asup[0].pu, 1); + assert.equal(asup[0].price, null); + + // Create delivery with manifest line: sku: 'mate', pu: 20, net: 12 + const delivery = await store.delivery.create(googleAuthId, { + date: '2026-10-25T10:00:00Z', + externalNumber: 'LS-000001', + supplierId: supplier.id, + lines: [ + { sku: 'mate', name: 'Club Mate 0,5l', pu: 20, amount: 2, net: 12, taxrate: 19 }, + ], + }); + + assert(delivery.id > 0); + assert.equal(delivery.lines[0].pu, 20); + + // Verify articlesupplier link was updated! + asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku); + assert.equal(asup.length, 1); + assert.equal(asup[0].pu, 20, 'VPE in articlesupplier must be synced to 20'); + assert.equal(asup[0].price.net, 12, 'Net price in articlesupplier must be synced to 12'); + assert.equal(asup[0].price.taxRate, 19, 'Tax rate in articlesupplier must be synced to 19'); + + // Verify article history + const history = await store.article.getHistoryBySku(googleAuthId, art.sku); + const historyTexts = (Array.isArray(history) ? history : history.history).map((h) => h.text); + console.log('Article history entries:', historyTexts); + + assert( + historyTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 20, EK: € 12,00, MwSt.: 19%)')), + 'Must record history entry for supplier update with VPE, EK, and MwSt' + ); + + // Now update delivery with new line pu: 24, net: 11.50, and taxrate: 7 + await store.delivery.update(googleAuthId, delivery.id, { + lines: [ + { sku: 'mate', name: 'Club Mate 0,5l', pu: 24, amount: 3, net: 11.5, taxrate: 7 }, + ], + }); + + asup = await store.articlesupplier.listByArticleSku(googleAuthId, art.sku); + assert.equal(asup[0].pu, 24, 'VPE in articlesupplier must be synced to 24 on update'); + assert.equal(asup[0].price.net, 11.5, 'Net price in articlesupplier must be synced to 11.5 on update'); + assert.equal(asup[0].price.taxRate, 7, 'Tax rate in articlesupplier must be synced to 7 on update'); + + const historyAfterUpdate = await store.article.getHistoryBySku(googleAuthId, art.sku); + const historyAfterTexts = (Array.isArray(historyAfterUpdate) ? historyAfterUpdate : historyAfterUpdate.history).map((h) => h.text); + console.log('Article history entries after delivery update:', historyAfterTexts); + assert( + historyAfterTexts.some((t) => t.includes('Lieferant "eks" aktualisiert (VPE: 24, EK: € 11,50, MwSt.: 7%)')), + 'Must record history entry for supplier update on delivery update' + ); + + // Now test booking the delivery with amount: 3, pu: 24 + // Should add 3 * 24 = 72 to stock and record clear history! + await store.delivery.book(googleAuthId, delivery.id); + + const artDetail = await store.article.getBySku(googleAuthId, art.sku); + const totalStock = (artDetail.article.warehouses || []).reduce((acc, w) => acc + (w.stock || 0), 0); + console.log('Article stock after booking delivery with amount 3, pu 24:', totalStock); + assert.equal(totalStock, 72, 'Physical warehouse stock must be amount * pu (3 * 24 = 72)'); + + const historyAfterBooking = await store.article.getHistoryBySku(googleAuthId, art.sku); + const bookingTexts = (Array.isArray(historyAfterBooking) ? historyAfterBooking : historyAfterBooking.history).map((h) => h.text); + console.log('Article history entries after delivery booking:', bookingTexts); + + assert( + bookingTexts.some((t) => t.includes('Lager: "Hauptlager"') && t.includes('Menge: 72 (3 x VPE 24)')), + 'Warehouse history must clearly show Menge: 72 (3 x VPE 24)' + ); + assert( + bookingTexts.some((t) => t.includes('Lieferant: "eks"') && t.includes('Menge: 3, VPE: 24')), + 'Acceptance history must clearly show Menge: 3, VPE: 24' + ); + + console.log('✓ Delivery link check and sync passed successfully!'); + } finally { + try { + provider.close(); + rmSync(tempDir, { recursive: true, force: true }); + } catch { + // ignore cleanup errors + } + } +} + +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/providers/mssql/index.js b/src/providers/mssql/index.js index 245bdfc..ec54c60 100644 --- a/src/providers/mssql/index.js +++ b/src/providers/mssql/index.js @@ -93,6 +93,43 @@ export class MssqlProvider { #config; #connecting; + async #tryCreateDatabaseIfMissing() { + if (!this.#config?.database) return false; + const targetDb = String(this.#config.database); + if (targetDb.toLowerCase() === 'master') return false; + + const masterConfig = { + ...this.#config, + database: 'master', + pool: { min: 0, max: 1, idleTimeoutMillis: 5000 }, + }; + + let masterPool; + try { + masterPool = await new sql.ConnectionPool(masterConfig).connect(); + } catch { + return false; + } + + try { + const checkReq = masterPool.request(); + checkReq.input('dbname', sql.NVarChar, targetDb); + const res = await checkReq.query('SELECT DB_ID(@dbname) AS dbid'); + if (res.recordset[0]?.dbid != null) { + return false; + } + + const createReq = masterPool.request(); + createReq.input('dbname', sql.NVarChar, targetDb); + await createReq.query( + "DECLARE @sql NVARCHAR(MAX) = N'CREATE DATABASE ' + QUOTENAME(@dbname); EXEC sp_executesql @sql;" + ); + return true; + } finally { + await masterPool.close(); + } + } + /** * Returns the underlying pool, creating and connecting it on first use * when the provider was constructed from a config object. @@ -101,15 +138,26 @@ export class MssqlProvider { ensurePool() { if (this.pool.connected) return Promise.resolve(this.pool); if (!this.#connecting) { - this.#connecting = this.pool - .connect() - .then((pool) => { - return pool; - }) - .catch((err) => { - this.#connecting = undefined; + this.#connecting = (async () => { + try { + return await this.pool.connect(); + } catch (err) { + if (err.code === 'ELOGIN') { + try { + const created = await this.#tryCreateDatabaseIfMissing(); + if (created) { + return await this.pool.connect(); + } + } catch { + // ignore creation error and fall through to rethrow original error + } + } throw err; - }); + } + })().catch((err) => { + this.#connecting = undefined; + throw err; + }); } return this.#connecting; } diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 9b539b1..e6514d4 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -462,12 +462,12 @@ export const GET_ARTICLE_BY_SKU_FULL = ` co.default_language_id AS languageId, co.poscustomergroup_id AS posCustomerGroupId, cu.id AS companyuserId - FROM article a - INNER JOIN company co ON co.id = a.company_id - INNER JOIN google_auth ga ON ga.id = @googleAuthId - INNER JOIN companyuser cu + FROM article a WITH (NOLOCK) + INNER JOIN company co WITH (NOLOCK) ON co.id = a.company_id + INNER JOIN google_auth ga WITH (NOLOCK) ON ga.id = @googleAuthId + INNER JOIN companyuser cu WITH (NOLOCK) ON cu.company_id = a.company_id AND cu.google_auth_id = @googleAuthId - LEFT JOIN categoryarticle ca ON ca.article_id = a.id + LEFT JOIN categoryarticle ca WITH (NOLOCK) ON ca.article_id = a.id WHERE a.sku = @sku AND (ga.selected_company IS NULL OR a.company_id = ga.selected_company) ) @@ -503,7 +503,7 @@ SELECT bp.thumbhash AS brandThumbHash, ( SELECT COUNT(*) - FROM optionslistarticle ola + FROM optionslistarticle ola WITH (NOLOCK) WHERE ola.optionslist_id = a.optionslist_id ) AS optionsCount, a.optionslist_id AS optionslistId, @@ -516,16 +516,16 @@ SELECT ola.count, ola.sort, ola.mandated - FROM optionslistarticle ola - INNER JOIN article a_opt ON a_opt.id = ola.article_id - LEFT JOIN articletext at_opt ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId + FROM optionslistarticle ola WITH (NOLOCK) + INNER JOIN article a_opt WITH (NOLOCK) ON a_opt.id = ola.article_id + LEFT JOIN articletext at_opt WITH (NOLOCK) ON at_opt.article_id = a_opt.id AND at_opt.language_id = auth.languageId WHERE ola.optionslist_id = a.optionslist_id ORDER BY ola.sort ASC, ola.id ASC FOR JSON PATH ) AS optionslistItemsJson, ( SELECT COUNT(*) - FROM articlelistarticle ala + FROM articlelistarticle ala WITH (NOLOCK) WHERE ala.articlelist_id = a.articlelist_id ) AS articleListCount, a.articlelist_id AS articlelistId, @@ -537,16 +537,16 @@ SELECT at_item.name, ala.count, ala.sort - FROM articlelistarticle ala - INNER JOIN article a_item ON a_item.id = ala.article_id - LEFT JOIN articletext at_item ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId + FROM articlelistarticle ala WITH (NOLOCK) + INNER JOIN article a_item WITH (NOLOCK) ON a_item.id = ala.article_id + LEFT JOIN articletext at_item WITH (NOLOCK) ON at_item.article_id = a_item.id AND at_item.language_id = auth.languageId WHERE ala.articlelist_id = a.articlelist_id ORDER BY ala.sort ASC, ala.id ASC FOR JSON PATH ) AS articlelistItemsJson, ( SELECT COUNT(*) - FROM article child + FROM article child WITH (NOLOCK) WHERE child.parentarticle_id = a.id ) AS childCount, a.is_parent AS isParent, @@ -563,11 +563,11 @@ SELECT at_child.name, av.id AS attributeValueId, av.value AS attributeValue - FROM article child - LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = auth.languageId + FROM article child WITH (NOLOCK) + LEFT JOIN articletext at_child WITH (NOLOCK) ON at_child.article_id = child.id AND at_child.language_id = auth.languageId LEFT JOIN ( - articleattributevalue aav - INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id + articleattributevalue aav WITH (NOLOCK) + INNER JOIN attributevalue av WITH (NOLOCK) ON av.id = aav.attributevalue_id AND av.attribute_id = a.parentattributename_id ) ON aav.article_id = child.id WHERE child.parentarticle_id = a.id ORDER BY child.parentsort ASC, child.id ASC @@ -575,7 +575,7 @@ SELECT ) AS childrenJson, ( SELECT ca2.category_id - FROM categoryarticle ca2 + FROM categoryarticle ca2 WITH (NOLOCK) WHERE ca2.article_id = a.id ORDER BY ca2.category_id FOR JSON PATH @@ -590,15 +590,15 @@ SELECT p.thumbw AS thumbW, p.thumbh AS thumbH, p.thumbhash AS thumbHash - FROM articlepic ap - INNER JOIN pic p ON p.id = ap.pic_id + FROM articlepic ap WITH (NOLOCK) + INNER JOIN pic p WITH (NOLOCK) ON p.id = ap.pic_id WHERE ap.article_id = a.id ORDER BY ap.sort ASC, ap.id ASC FOR JSON PATH ) AS photosJson, ( SELECT ae.ean - FROM articleean ae + FROM articleean ae WITH (NOLOCK) WHERE ae.article_id = a.id ORDER BY ae.ean FOR JSON PATH @@ -609,8 +609,8 @@ SELECT pr.net, t.rate AS taxRate, pr.customergroup_id AS customergroupId - FROM price pr - INNER JOIN tax t ON t.id = pr.tax_id + FROM price pr WITH (NOLOCK) + INNER JOIN tax t WITH (NOLOCK) ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.customergroup_id, pr.minmultiplier FOR JSON PATH @@ -632,11 +632,11 @@ SELECT SELECT m.[date] AS [date], SUM(ma.amount - COALESCE(ma_ledger.booked_amount, 0)) AS amount - FROM manifest m - INNER JOIN manifestarticle ma ON ma.manifest_id = m.id + FROM manifest m WITH (NOLOCK) + INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.manifest_id = m.id LEFT JOIN ( SELECT manifestarticle_id, SUM(amount) AS booked_amount - FROM ledger + FROM ledger WITH (NOLOCK) WHERE warehouse_id IS NULL GROUP BY manifestarticle_id ) ma_ledger ON ma_ledger.manifestarticle_id = ma.id @@ -651,9 +651,9 @@ SELECT ORDER BY m.[date] ASC, m.id ASC FOR JSON PATH ) AS incomingJson - FROM articlesupplier asup - INNER JOIN entity e ON e.id = asup.entity_id - LEFT JOIN tax t ON t.id = asup.tax_id + FROM articlesupplier asup WITH (NOLOCK) + INNER JOIN entity e WITH (NOLOCK) ON e.id = asup.entity_id + LEFT JOIN tax t WITH (NOLOCK) ON t.id = asup.tax_id WHERE asup.article_id = a.id ORDER BY asup.sort ASC, asup.id ASC FOR JSON PATH @@ -665,21 +665,21 @@ SELECT w.sort, CASE WHEN a.articlelist_id IS NOT NULL AND EXISTS ( - SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id + SELECT 1 FROM articlelistarticle ala WITH (NOLOCK) WHERE ala.articlelist_id = a.articlelist_id ) THEN COALESCE(( SELECT CASE WHEN MIN(COALESCE(comp_stock.stock, 0) / ala.count) < 0 THEN 0 ELSE FLOOR(MIN(COALESCE(comp_stock.stock, 0) / ala.count)) END - FROM articlelistarticle ala + FROM articlelistarticle ala WITH (NOLOCK) LEFT JOIN ( SELECT ca.id AS comp_id, SUM(l.amount) AS stock - FROM ledger l - INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id - INNER JOIN manifest m ON m.id = ma.manifest_id - INNER JOIN article ca ON ( + FROM ledger l WITH (NOLOCK) + INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.id = l.manifestarticle_id + INNER JOIN manifest m WITH (NOLOCK) ON m.id = ma.manifest_id + INNER JOIN article ca WITH (NOLOCK) ON ( ma.sku = ca.sku - OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id) + OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = ca.id) ) WHERE m.company_id = a.company_id AND l.warehouse_id = w.id @@ -690,16 +690,16 @@ SELECT WHEN a.articlelist_id IS NOT NULL THEN 0 ELSE COALESCE(stock_calc.stock, 0) END AS stock - FROM warehouse w + FROM warehouse w WITH (NOLOCK) LEFT JOIN ( SELECT l.warehouse_id, SUM(l.amount) AS stock - FROM ledger l - INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id - INNER JOIN manifest m ON m.id = ma.manifest_id + FROM ledger l WITH (NOLOCK) + INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.id = l.manifestarticle_id + INNER JOIN manifest m WITH (NOLOCK) ON m.id = ma.manifest_id WHERE m.company_id = a.company_id AND ( ma.sku = a.sku - OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) + OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = a.id) ) GROUP BY l.warehouse_id ) stock_calc ON stock_calc.warehouse_id = w.id @@ -715,20 +715,20 @@ SELECT COALESCE(os.customer_name, LTRIM(RTRIM(ISNULL(e.first_name + ' ', '') + ISNULL(e.name, '')))) AS customerName, ROUND(SUM(ma.amount) - COALESCE(( SELECT SUM(ma_s.amount) - FROM manifest m_s - INNER JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id - LEFT JOIN shipmentsummary ss ON ss.manifest_id = m_s.id + FROM manifest m_s WITH (NOLOCK) + INNER JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = m_s.id + LEFT JOIN shipmentsummary ss WITH (NOLOCK) ON ss.manifest_id = m_s.id WHERE m_s.company_id = m.company_id AND m_s.[type] = 'shipment' AND ISNULL(ss.state, 'new') != 'cancelled' AND m_s.[number] NOT LIKE '%#cancelled#%' AND ma_s.sku = a.sku - AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' + AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WITH (NOLOCK) WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' ), 0), 3) AS amount - FROM manifest m - INNER JOIN manifestarticle ma ON ma.manifest_id = m.id - LEFT JOIN ordersummary os ON os.manifest_id = m.id - LEFT JOIN entity e ON e.id = m.entity_id + FROM manifest m WITH (NOLOCK) + INNER JOIN manifestarticle ma WITH (NOLOCK) ON ma.manifest_id = m.id + LEFT JOIN ordersummary os WITH (NOLOCK) ON os.manifest_id = m.id + LEFT JOIN entity e WITH (NOLOCK) ON e.id = m.entity_id WHERE m.company_id = a.company_id AND m.[type] = 'order' AND ma.sku = a.sku @@ -736,35 +736,35 @@ SELECT GROUP BY m.id, m.[number], m.[date], m.company_id, os.customer_name, e.first_name, e.name HAVING (SUM(ma.amount) - COALESCE(( SELECT SUM(ma_s.amount) - FROM manifest m_s - INNER JOIN manifestarticle ma_s ON ma_s.manifest_id = m_s.id - LEFT JOIN shipmentsummary ss ON ss.manifest_id = m_s.id + FROM manifest m_s WITH (NOLOCK) + INNER JOIN manifestarticle ma_s WITH (NOLOCK) ON ma_s.manifest_id = m_s.id + LEFT JOIN shipmentsummary ss WITH (NOLOCK) ON ss.manifest_id = m_s.id WHERE m_s.company_id = m.company_id AND m_s.[type] = 'shipment' AND ISNULL(ss.state, 'new') != 'cancelled' AND m_s.[number] NOT LIKE '%#cancelled#%' AND ma_s.sku = a.sku - AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' + AND m_s.[number] LIKE m.[number] + COALESCE((SELECT prefix FROM numbers WITH (NOLOCK) WHERE company_id = m.company_id AND [type] = 'shipment'), '-L-') + '%' ), 0)) > 0 ORDER BY m.[date] ASC, m.id ASC FOR JSON PATH ) AS undeliveredOrdersJson - FROM article a - LEFT JOIN articletext at + FROM article a WITH (NOLOCK) + LEFT JOIN articletext at WITH (NOLOCK) ON at.article_id = a.id AND at.language_id = auth.languageId - LEFT JOIN unit u ON u.id = a.unit_id - LEFT JOIN unittext ut + LEFT JOIN unit u WITH (NOLOCK) ON u.id = a.unit_id + LEFT JOIN unittext ut WITH (NOLOCK) ON ut.unit_id = u.id AND ut.language_id = auth.languageId - LEFT JOIN brand b ON b.id = a.brand_id + LEFT JOIN brand b WITH (NOLOCK) ON b.id = a.brand_id OUTER APPLY ( SELECT TOP 1 bp2.pic_id - FROM brandpic bp2 + FROM brandpic bp2 WITH (NOLOCK) WHERE bp2.brand_id = b.id ORDER BY bp2.sort ASC, bp2.id ASC ) first_brand_pic - LEFT JOIN pic bp ON bp.id = first_brand_pic.pic_id - LEFT JOIN article fa ON fa.id = a.parentarticle_id - LEFT JOIN attributename fan ON fan.id = a.parentattributename_id + LEFT JOIN pic bp WITH (NOLOCK) ON bp.id = first_brand_pic.pic_id + LEFT JOIN article fa WITH (NOLOCK) ON fa.id = a.parentarticle_id + LEFT JOIN attributename fan WITH (NOLOCK) ON fan.id = a.parentattributename_id WHERE a.id = auth.articleId FOR JSON PATH, WITHOUT_ARRAY_WRAPPER ) AS articleJson diff --git a/src/providers/mssql/queries/ledger.js b/src/providers/mssql/queries/ledger.js index 7085f53..66cd203 100644 --- a/src/providers/mssql/queries/ledger.js +++ b/src/providers/mssql/queries/ledger.js @@ -3,6 +3,7 @@ SELECT ma.id, ma.sku, ma.amount AS lineAmount, + ma.pu AS linePu, ma.net, ma.rebate, ma.taxrate, @@ -67,7 +68,7 @@ SELECT TOP 1 hash_version, prev_hash, created_at -FROM ledger WITH (UPDLOCK, HOLDLOCK) +FROM ledger WITH (UPDLOCK, ROWLOCK) ORDER BY id DESC; `; diff --git a/src/providers/mssql/queries/manifestDoc.js b/src/providers/mssql/queries/manifestDoc.js index 18a62d6..92fae88 100644 --- a/src/providers/mssql/queries/manifestDoc.js +++ b/src/providers/mssql/queries/manifestDoc.js @@ -158,22 +158,61 @@ SELECT a.id, a.is_parent AS isParent, COALESCE( - (SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id), - (SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id), + (SELECT at.name FROM articletext at WITH (NOLOCK) WHERE at.article_id = a.id AND at.language_id = co.default_language_id), + (SELECT MIN(at2.name) FROM articletext at2 WITH (NOLOCK) WHERE at2.article_id = a.id), a.sku ) AS articleName, COALESCE( - (SELECT MIN(asup.pu) FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku), - (SELECT MIN(asup2.pu) FROM articlesupplier asup2 WHERE asup2.article_id = a.id), + (SELECT MIN(asup.pu) FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = a.id AND asup.sku = @sku), + (SELECT MIN(asup2.pu) FROM articlesupplier asup2 WITH (NOLOCK) WHERE asup2.article_id = a.id), a.unitamount, 1 ) AS pu -FROM article a -INNER JOIN manifest m ON m.company_id = a.company_id -INNER JOIN company co ON co.id = m.company_id +FROM article a WITH (NOLOCK) +INNER JOIN manifest m WITH (NOLOCK) ON m.company_id = a.company_id +INNER JOIN company co WITH (NOLOCK) ON co.id = m.company_id WHERE m.id = @manifestId AND (a.sku = @sku OR EXISTS ( - SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku + SELECT 1 FROM articlesupplier asup WITH (NOLOCK) WHERE asup.article_id = a.id AND asup.sku = @sku )) `; +export const GET_MANIFEST_CONTEXT = ` +SELECT + m.id, + m.company_id AS companyId, + m.[type] AS [type], + m.entity_id AS entityId, + e.name AS entityName +FROM manifest m +LEFT JOIN entity e ON e.id = m.entity_id +WHERE m.id = @manifestId +`; + +export const GET_ARTICLE_SUPPLIER_LINK = ` +SELECT + asup.id, + asup.article_id AS articleId, + asup.entity_id AS entityId, + asup.sku, + asup.pu, + asup.net, + asup.tax_id AS taxId, + t.rate AS taxRate, + e.name AS supplierName +FROM articlesupplier asup +LEFT JOIN entity e ON e.id = asup.entity_id +LEFT JOIN tax t ON t.id = asup.tax_id +WHERE asup.article_id = @articleId + AND asup.entity_id = @entityId +`; + +export const UPDATE_ARTICLE_SUPPLIER_LINK = ` +UPDATE articlesupplier +SET + pu = @pu, + net = @net, + tax_id = @taxId +WHERE id = @id +`; + diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 4924006..ddd236d 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -360,6 +360,16 @@ BEGIN ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 0; END END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_article_sku' AND object_id = OBJECT_ID('article')) +BEGIN + CREATE NONCLUSTERED INDEX IX_article_sku ON article (sku); +END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_article_parentarticle_id' AND object_id = OBJECT_ID('article')) +BEGIN + CREATE NONCLUSTERED INDEX IX_article_parentarticle_id ON article (parentarticle_id); +END `; export const INIT_ARTICLEHISTORY = ` @@ -624,6 +634,18 @@ BEGIN CREATE INDEX IX_articlesupplier_article_entity ON articlesupplier (article_id, entity_id); END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_articlesupplier_sku' AND object_id = OBJECT_ID('articlesupplier')) +BEGIN + CREATE NONCLUSTERED INDEX IX_articlesupplier_sku + ON articlesupplier (sku, article_id); +END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_articlesupplier_entity' AND object_id = OBJECT_ID('articlesupplier')) +BEGIN + CREATE NONCLUSTERED INDEX IX_articlesupplier_entity + ON articlesupplier (entity_id); +END `; export const INIT_MANIFEST = ` @@ -648,6 +670,12 @@ BEGIN CREATE UNIQUE NONCLUSTERED INDEX UQ_manifest_company_type_number ON manifest (company_id, [type], [number]); END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifest_entity_id' AND object_id = OBJECT_ID('manifest')) +BEGIN + CREATE NONCLUSTERED INDEX IX_manifest_entity_id + ON manifest (entity_id); +END `; export const INIT_MANIFESTARTICLE = ` @@ -668,6 +696,18 @@ BEGIN CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE ); END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifestarticle_manifest_id' AND object_id = OBJECT_ID('manifestarticle')) +BEGIN + CREATE NONCLUSTERED INDEX IX_manifestarticle_manifest_id + ON manifestarticle (manifest_id); +END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_manifestarticle_sku' AND object_id = OBJECT_ID('manifestarticle')) +BEGIN + CREATE NONCLUSTERED INDEX IX_manifestarticle_sku + ON manifestarticle (sku); +END `; @@ -706,6 +746,18 @@ BEGIN CONSTRAINT FK_ledger_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION ) WITH (LEDGER = ON (APPEND_ONLY = ON)); END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ledger_manifestarticle_warehouse' AND object_id = OBJECT_ID('ledger')) +BEGIN + CREATE NONCLUSTERED INDEX IX_ledger_manifestarticle_warehouse + ON ledger (manifestarticle_id, warehouse_id); +END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_ledger_warehouse_id' AND object_id = OBJECT_ID('ledger')) +BEGIN + CREATE NONCLUSTERED INDEX IX_ledger_warehouse_id + ON ledger (warehouse_id); +END `; export const INIT_NUMBERS = ` @@ -1146,6 +1198,12 @@ BEGIN CONSTRAINT FK_payment_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE ); END + +IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_payment_manifest_id' AND object_id = OBJECT_ID('payment')) +BEGIN + CREATE NONCLUSTERED INDEX IX_payment_manifest_id + ON payment (manifest_id); +END `; export const INIT_CARRIER = ` diff --git a/src/providers/sqlite3/queries/ledger.js b/src/providers/sqlite3/queries/ledger.js index 601daa6..5920b83 100644 --- a/src/providers/sqlite3/queries/ledger.js +++ b/src/providers/sqlite3/queries/ledger.js @@ -3,6 +3,7 @@ SELECT ma.id, ma.sku, ma.amount AS lineAmount, + ma.pu AS linePu, ma.net, ma.rebate, ma.taxrate, diff --git a/src/providers/sqlite3/queries/manifestDoc.js b/src/providers/sqlite3/queries/manifestDoc.js index 9c5c241..d71d239 100644 --- a/src/providers/sqlite3/queries/manifestDoc.js +++ b/src/providers/sqlite3/queries/manifestDoc.js @@ -4,8 +4,8 @@ import { translateQueries } from './translate.js'; const queries = translateQueries(source, 'manifestDoc'); export const { CHECK_ARTICLE_IS_PARENT, COUNT_MANIFESTS, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST, - DELETE_MANIFEST, GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS, - ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST, + DELETE_MANIFEST, GET_ARTICLE_SUPPLIER_LINK, GET_MANIFEST_CONTEXT, GET_MANIFEST_HEADER, + GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE, QUOTE_TYPE, UPDATE_ARTICLE_SUPPLIER_LINK, UPDATE_MANIFEST, } = queries; export const LIST_MANIFESTS_PAGED = ` diff --git a/src/providers/sqlite3/queries/translate.js b/src/providers/sqlite3/queries/translate.js index d0eee34..8df566c 100644 --- a/src/providers/sqlite3/queries/translate.js +++ b/src/providers/sqlite3/queries/translate.js @@ -31,6 +31,7 @@ export function sqliteQuery(sql) { if (typeof sql !== 'string') return sql; let result = quoteIdentifiers(sql) + .replace(/\bWITH\s*\(\s*(?:NOLOCK|UPDLOCK|ROWLOCK|HOLDLOCK|READPAST)(?:\s*,\s*(?:NOLOCK|UPDLOCK|ROWLOCK|HOLDLOCK|READPAST))*\s*\)/gi, '') .replace(/\bISNULL\s*\(/gi, 'IFNULL(') .replace(/\bSYSUTCDATETIME\s*\(\s*\)/gi, "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')") .replace(/\bDATEDIFF_BIG\s*\(\s*second\s*,\s*'1970-01-01'\s*,\s*([^)]+?)\s*\)/gi, "CAST(strftime('%s', $1) AS INTEGER)") diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 583105a..2ea2c8e 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -243,6 +243,8 @@ CREATE TABLE IF NOT EXISTS article ( CREATE UNIQUE INDEX IF NOT EXISTS UQ_article_parentarticle_parentsort ON article(parentarticle_id, parentsort) WHERE parentarticle_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS IX_article_sku ON article (sku); +CREATE INDEX IF NOT EXISTS IX_article_parentarticle_id ON article (parentarticle_id); `; export const INIT_ARTICLEEAN = ` @@ -417,6 +419,10 @@ CREATE TABLE IF NOT EXISTS articlesupplier ( ); CREATE INDEX IF NOT EXISTS IX_articlesupplier_article_entity ON articlesupplier(article_id, entity_id); +CREATE INDEX IF NOT EXISTS IX_articlesupplier_sku + ON articlesupplier(sku, article_id); +CREATE INDEX IF NOT EXISTS IX_articlesupplier_entity + ON articlesupplier(entity_id); `; export const INIT_MANIFEST = ` @@ -434,6 +440,8 @@ CREATE TABLE IF NOT EXISTS manifest ( ); CREATE UNIQUE INDEX IF NOT EXISTS UQ_manifest_company_type_number ON manifest (company_id, "type", "number"); +CREATE INDEX IF NOT EXISTS IX_manifest_entity_id + ON manifest (entity_id); `; export const INIT_MANIFESTARTICLE = ` @@ -451,6 +459,10 @@ CREATE TABLE IF NOT EXISTS manifestarticle ( shippable INTEGER NOT NULL DEFAULT 1, FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE ); +CREATE INDEX IF NOT EXISTS IX_manifestarticle_manifest_id + ON manifestarticle (manifest_id); +CREATE INDEX IF NOT EXISTS IX_manifestarticle_sku + ON manifestarticle (sku); `; export const INIT_WAREHOUSE = ` @@ -479,6 +491,10 @@ CREATE TABLE IF NOT EXISTS ledger ( FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION, FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION ); +CREATE INDEX IF NOT EXISTS IX_ledger_manifestarticle_warehouse + ON ledger (manifestarticle_id, warehouse_id); +CREATE INDEX IF NOT EXISTS IX_ledger_warehouse_id + ON ledger (warehouse_id); `; export const INIT_NUMBERS = ` @@ -817,6 +833,8 @@ CREATE TABLE IF NOT EXISTS payment ( FOREIGN KEY (account_id) REFERENCES account(id) ON DELETE CASCADE, FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE ); +CREATE INDEX IF NOT EXISTS IX_payment_manifest_id + ON payment (manifest_id); `; export const INIT_CARRIER = ` diff --git a/src/store/articlehistory/record.js b/src/store/articlehistory/record.js index a7c1fc5..270be51 100644 --- a/src/store/articlehistory/record.js +++ b/src/store/articlehistory/record.js @@ -149,7 +149,7 @@ export async function recordOrderHistory(runner, { articleId, companyuserId, dat } /** Records incoming delivery acceptance: "Lieferung {manifestNumber} (Lieferant: "{supplierName}", EK brutto: {ekBrutto}, Menge: {amount})" */ -export async function recordAcceptanceHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, net, rebate, taxrate, amount, transactionText }) { +export async function recordAcceptanceHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, net, rebate, taxrate, amount, pu, transactionText }) { const details = []; if (supplierName) details.push(`Lieferant: "${supplierName}"`); @@ -159,11 +159,15 @@ export async function recordAcceptanceHistory(runner, { articleId, companyuserId details.push(`EK brutto: ${ekBrutto}`); } if (isFiniteNum(amount)) details.push(`Menge: ${amount}`); + const numPu = pu != null ? Number(pu) : null; + if (numPu != null && numPu > 1) { + details.push(`VPE: ${numPu}`); + } const base = manifestNumber ? `Lieferung ${manifestNumber}` : (transactionText?.trim() || 'Lieferung'); const meta = { type: 'acceptance', manifestNumber: manifestNumber ?? null, deliveryNumber: manifestNumber ?? null, - supplierName: supplierName ?? null, amount: toNumOrNull(amount), net: toNumOrNull(net), + supplierName: supplierName ?? null, amount: toNumOrNull(amount), pu: numPu ?? 1, net: toNumOrNull(net), rebate: rebate != null ? Number(rebate) : 0, taxrate: taxrate != null ? Number(taxrate) : 0, ekBrutto: toNumOrNull(ekBrutto), transactionText: transactionText ?? null, }; @@ -185,28 +189,40 @@ export async function recordShipmentHistory(runner, { articleId, companyuserId, } /** Records warehouse inventory ledger entry (Inbound "Lieferung" / Outbound "Lieferschein"). */ -export async function recordWarehouseHistory(runner, { articleId, companyuserId, date, manifestNumber, manifestType, warehouseName, amount, transactionText }) { +export async function recordWarehouseHistory(runner, { articleId, companyuserId, date, manifestNumber, manifestType, warehouseName, amount, pu, manifestAmount, transactionText }) { const isShipment = manifestType === 'shipment' || (amount != null && Number(amount) < 0); const prefix = isShipment ? 'Lieferschein' : 'Lieferung'; const displayAmount = amount != null ? Math.abs(Number(amount)) : null; + const numPu = pu != null ? Number(pu) : 1; + const numManifestAmount = manifestAmount != null ? Math.abs(Number(manifestAmount)) : null; + const details = []; if (warehouseName) details.push(`Lager: "${warehouseName}"`); - if (displayAmount != null && Number.isFinite(displayAmount)) details.push(`Menge: ${displayAmount}`); + if (displayAmount != null && Number.isFinite(displayAmount)) { + if (numPu > 1 && numManifestAmount != null) { + details.push(`Menge: ${displayAmount} (${numManifestAmount} x VPE ${numPu})`); + } else if (numPu > 1) { + details.push(`Menge: ${displayAmount} (VPE: ${numPu})`); + } else { + details.push(`Menge: ${displayAmount}`); + } + } const base = manifestNumber ? `${prefix} ${manifestNumber}` : (transactionText?.trim() || prefix); const meta = { type: isShipment ? 'warehouse_out' : 'warehouse_in', manifestNumber: manifestNumber ?? null, deliveryNumber: !isShipment ? (manifestNumber ?? null) : null, shipmentNumber: isShipment ? (manifestNumber ?? null) : null, - warehouseName: warehouseName ?? null, amount: toNumOrNull(amount), transactionText: transactionText ?? null, + warehouseName: warehouseName ?? null, amount: toNumOrNull(amount), pu: numPu, + manifestAmount: numManifestAmount, transactionText: transactionText ?? null, }; return recordArticleHistory(runner, { articleId, companyuserId, date, text: `${base}${fmtSuffix(details)}`, meta }); } /** Backward-compatible helper that records delivery / warehouse ledger entry in article history. */ -export async function recordDeliveryHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, warehouseName, net, rebate, taxrate, amount, transactionText, isDelivery = true }) { +export async function recordDeliveryHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, warehouseName, net, rebate, taxrate, amount, pu, manifestAmount, transactionText, isDelivery = true }) { if (isDelivery && !warehouseName) { - return recordAcceptanceHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, net, rebate, taxrate, amount, transactionText }); + return recordAcceptanceHistory(runner, { articleId, companyuserId, date, manifestNumber, supplierName, net, rebate, taxrate, amount, pu, transactionText }); } let historyText = transactionText; @@ -220,7 +236,17 @@ export async function recordDeliveryHistory(runner, { articleId, companyuserId, ekBrutto = round2(calculateGross(net, rebate, taxrate)).toFixed(2); details.push(`EK brutto: ${ekBrutto}`); } - if (isFiniteNum(amount)) details.push(`Menge: ${amount}`); + const numPu = pu != null ? Number(pu) : 1; + const numManifestAmount = manifestAmount != null ? Math.abs(Number(manifestAmount)) : null; + if (isFiniteNum(amount)) { + if (numPu > 1 && numManifestAmount != null) { + details.push(`Menge: ${amount} (${numManifestAmount} x VPE ${numPu})`); + } else if (numPu > 1) { + details.push(`Menge: ${amount} (VPE: ${numPu})`); + } else { + details.push(`Menge: ${amount}`); + } + } const base = manifestNumber ? `Lieferung ${manifestNumber}` : (transactionText?.trim() || 'Lieferung'); historyText = `${base}${fmtSuffix(details)}`; } else if (warehouseName && transactionText && !transactionText.includes(warehouseName)) { @@ -230,6 +256,7 @@ export async function recordDeliveryHistory(runner, { articleId, companyuserId, const meta = { type: isDelivery ? 'delivery' : 'movement', manifestNumber: manifestNumber ?? null, deliveryNumber: manifestNumber ?? null, supplierName: supplierName ?? null, warehouseName: warehouseName ?? null, amount: toNumOrNull(amount), + pu: pu != null ? Number(pu) : 1, manifestAmount: manifestAmount != null ? Number(manifestAmount) : null, net: toNumOrNull(net), rebate: rebate != null ? Number(rebate) : 0, taxrate: taxrate != null ? Number(taxrate) : 0, ekBrutto: toNumOrNull(ekBrutto), transactionText: transactionText ?? null, }; @@ -393,13 +420,27 @@ export async function recordChildDeletedFromParentHistory(runner, { parentId, co // MARK: - Suppliers /** Records adding a supplier to an article. */ -export async function recordSupplierAddHistory(runner, { articleId, companyuserId, supplierName, supplierSku, netPrice }) { +export async function recordSupplierAddHistory(runner, { articleId, companyuserId, supplierName, supplierSku, netPrice, pu }) { const details = []; if (supplierSku) details.push(`Artikelnr: "${supplierSku}"`); - if (netPrice != null) details.push(`EK: ${netPrice}`); + const resolvedPu = pu != null && Number.isFinite(Number(pu)) ? Number(pu) : 1; + details.push(`VPE: ${resolvedPu}`); + + if (isFiniteNum(netPrice)) { + const formattedPrice = formatCurrencyDe(netPrice); + details.push(`EK: ${formattedPrice}`); + } else { + details.push('ohne EK'); + } const text = `Lieferant "${supplierName}" hinzugefügt${fmtSuffix(details)}`; - const meta = { type: 'supplier_add', supplierName, supplierSku: supplierSku ?? null, netPrice: netPrice ?? null }; + const meta = { + type: 'supplier_add', + supplierName, + supplierSku: supplierSku ?? null, + netPrice: toNumOrNull(netPrice), + pu: resolvedPu, + }; return recordArticleHistory(runner, { articleId, companyuserId, text, meta }); } diff --git a/src/store/articlesupplier/create.js b/src/store/articlesupplier/create.js index 468bfa8..f41e929 100644 --- a/src/store/articlesupplier/create.js +++ b/src/store/articlesupplier/create.js @@ -150,6 +150,7 @@ export default function create(provider) { const actualSku = supplierRow?.sku ?? supplierSku; const actualPrice = supplierRow?.priceNet ?? netPrice; + const actualPu = supplierRow?.pu ?? pu; await recordSupplierAddHistory(runner, { articleId, @@ -157,6 +158,7 @@ export default function create(provider) { supplierName, supplierSku: actualSku, netPrice: actualPrice, + pu: actualPu, }); return shapeArticleSupplier(supplierRow); diff --git a/src/store/delivery/create.js b/src/store/delivery/create.js index 5a07bd6..7d81088 100644 --- a/src/store/delivery/create.js +++ b/src/store/delivery/create.js @@ -33,7 +33,7 @@ export default function create(provider) { const lines = normalizeLines(body.lines ?? []); - const { companyId } = await access(googleAuthId); + const { companyId, companyuserId } = await access(googleAuthId); const run = async (runner, tx) => { const targetSupplierId = body.supplierId !== undefined ? body.supplierId : body.entityId; @@ -73,6 +73,7 @@ export default function create(provider) { manifestId, lines, ensureTax, + companyuserId, }); await insertAttachments( diff --git a/src/store/delivery/syncSummary.js b/src/store/delivery/syncSummary.js index 624fbb1..0ace0d2 100644 --- a/src/store/delivery/syncSummary.js +++ b/src/store/delivery/syncSummary.js @@ -70,10 +70,10 @@ const SQL_DELIVERY_TOTALS_SQLITE = ` COALESCE(SUM(line_net), 0) AS net_total, COALESCE(SUM(amount), 0) AS total_quantity, COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions, - COALESCE(SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS stock_booked_positions, + COALESCE(SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END), 0) AS stock_booked_positions, COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity, COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS stock_booked_quantity, - COALESCE(SUM(CASE WHEN (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS all_booked_positions, + COALESCE(SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END), 0) AS all_booked_positions, COALESCE(SUM(booked_amount), 0) AS all_booked_quantity FROM line_data `; @@ -107,10 +107,10 @@ const SQL_DELIVERY_TOTALS_MSSQL = ` COALESCE(SUM(line_net), 0) AS net_total, COALESCE(SUM(amount), 0) AS total_quantity, COALESCE(SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END), 0) AS stock_positions, - COALESCE(SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS stock_booked_positions, + COALESCE(SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END), 0) AS stock_booked_positions, COALESCE(SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END), 0) AS stock_quantity, COALESCE(SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END), 0) AS stock_booked_quantity, - COALESCE(SUM(CASE WHEN (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END), 0) AS all_booked_positions, + COALESCE(SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END), 0) AS all_booked_positions, COALESCE(SUM(booked_amount), 0) AS all_booked_quantity FROM line_data `; @@ -325,10 +325,10 @@ export async function rebuildDeliverySummaries(runner, companyId) { SUM(line_net) AS net_total, SUM(amount) AS total_quantity, SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END) AS stock_positions, - SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS stock_booked_positions, + SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END) AS stock_booked_positions, SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END) AS stock_quantity, SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END) AS stock_booked_quantity, - SUM(CASE WHEN (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS all_booked_positions, + SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END) AS all_booked_positions, SUM(booked_amount) AS all_booked_quantity FROM line_data GROUP BY manifest_id @@ -435,10 +435,10 @@ export async function rebuildDeliverySummaries(runner, companyId) { SUM(line_net) AS net_total, SUM(amount) AS total_quantity, SUM(CASE WHEN track_stock = 1 THEN 1 ELSE 0 END) AS stock_positions, - SUM(CASE WHEN track_stock = 1 AND (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS stock_booked_positions, + SUM(CASE WHEN track_stock = 1 AND ((amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0)) THEN 1 ELSE 0 END) AS stock_booked_positions, SUM(CASE WHEN track_stock = 1 THEN amount ELSE 0 END) AS stock_quantity, SUM(CASE WHEN track_stock = 1 THEN booked_amount ELSE 0 END) AS stock_booked_quantity, - SUM(CASE WHEN (CASE WHEN amount > 0 THEN booked_amount >= amount ELSE booked_amount > 0 END) THEN 1 ELSE 0 END) AS all_booked_positions, + SUM(CASE WHEN (amount > 0 AND booked_amount >= amount) OR (amount <= 0 AND booked_amount > 0) THEN 1 ELSE 0 END) AS all_booked_positions, SUM(booked_amount) AS all_booked_quantity FROM line_data GROUP BY manifest_id diff --git a/src/store/delivery/update.js b/src/store/delivery/update.js index d1b6aad..7cabd6c 100644 --- a/src/store/delivery/update.js +++ b/src/store/delivery/update.js @@ -46,7 +46,7 @@ export default function update(provider) { normalizedLines = normalizeLines(patch.lines); } - const { companyId } = await access(googleAuthId); + const { companyId, companyuserId } = await access(googleAuthId); const run = async (runner, tx) => { const existing = await runner(queries.GET_DELIVERY_HEADER, { @@ -141,6 +141,7 @@ export default function update(provider) { manifestId: id, lines: normalizedLines, ensureTax, + companyuserId, }); } diff --git a/src/store/ledger/add.js b/src/store/ledger/add.js index 92fb17e..34330a3 100644 --- a/src/store/ledger/add.js +++ b/src/store/ledger/add.js @@ -208,6 +208,10 @@ export default function add(provider) { memo: text, }); + const linePu = manifestArticle.linePu != null && Number(manifestArticle.linePu) > 0 + ? Number(manifestArticle.linePu) + : 1; + // Record history for acceptance for (const match of articleMatches) { const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH; @@ -223,6 +227,7 @@ export default function add(provider) { rebate: manifestArticle.rebate, taxrate: manifestArticle.taxrate, amount: Math.abs(amount), + pu: linePu, transactionText: text, }); } @@ -245,10 +250,12 @@ export default function add(provider) { warehouseName = defWh.rows[0].name; } + const stockRowAmount = Math.abs(amount) * linePu; + await insertLedgerRow({ whId: effectiveWhId, type: 'WAREHOUSE', - rowAmount: Math.abs(amount), + rowAmount: stockRowAmount, memo: text, }); @@ -265,7 +272,9 @@ export default function add(provider) { manifestNumber: deliveryNumber, manifestType: 'delivery', warehouseName, - amount: Math.abs(amount), + amount: stockRowAmount, + pu: linePu, + manifestAmount: Math.abs(amount), transactionText: text, }); } diff --git a/src/store/manifestLines.js b/src/store/manifestLines.js index 8117058..2171d8c 100644 --- a/src/store/manifestLines.js +++ b/src/store/manifestLines.js @@ -1,11 +1,18 @@ import { CHECK_ARTICLE_IS_PARENT as MSSQL_CHECK_ARTICLE_IS_PARENT, CREATE_LINE as MSSQL_CREATE_LINE, + GET_MANIFEST_CONTEXT as MSSQL_GET_MANIFEST_CONTEXT, + GET_ARTICLE_SUPPLIER_LINK as MSSQL_GET_ARTICLE_SUPPLIER_LINK, + UPDATE_ARTICLE_SUPPLIER_LINK as MSSQL_UPDATE_ARTICLE_SUPPLIER_LINK, } from '../providers/mssql/queries/manifestDoc.js'; import { CHECK_ARTICLE_IS_PARENT as SQLITE_CHECK_ARTICLE_IS_PARENT, CREATE_LINE as SQLITE_CREATE_LINE, + GET_MANIFEST_CONTEXT as SQLITE_GET_MANIFEST_CONTEXT, + GET_ARTICLE_SUPPLIER_LINK as SQLITE_GET_ARTICLE_SUPPLIER_LINK, + UPDATE_ARTICLE_SUPPLIER_LINK as SQLITE_UPDATE_ARTICLE_SUPPLIER_LINK, } from '../providers/sqlite3/queries/manifestDoc.js'; +import { formatCurrencyDe, recordArticleHistory } from './articlehistory/record.js'; import makeRunner from './runner.js'; /** @@ -123,14 +130,23 @@ export async function insertLines( params = txOrParams; } - const { manifestId, lines, ensureTax } = params; + const { manifestId, lines, ensureTax, companyuserId } = params; const isSqlite = runner?.dialect === 'sqlite3'; const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE; const checkParentSql = isSqlite ? SQLITE_CHECK_ARTICLE_IS_PARENT : MSSQL_CHECK_ARTICLE_IS_PARENT; + const getManifestContextSql = isSqlite ? SQLITE_GET_MANIFEST_CONTEXT : MSSQL_GET_MANIFEST_CONTEXT; + const getArticleSupplierLinkSql = isSqlite ? SQLITE_GET_ARTICLE_SUPPLIER_LINK : MSSQL_GET_ARTICLE_SUPPLIER_LINK; + const updateArticleSupplierLinkSql = isSqlite ? SQLITE_UPDATE_ARTICLE_SUPPLIER_LINK : MSSQL_UPDATE_ARTICLE_SUPPLIER_LINK; + + const manifestCtxRes = await runner.query(getManifestContextSql, { manifestId }); + const manifestCtx = manifestCtxRes.rows[0]; + const isDelivery = String(manifestCtx?.type || '').toLowerCase() === 'delivery'; + const supplierEntityId = manifestCtx?.entityId != null ? Number(manifestCtx.entityId) : null; for (const line of lines) { let resolvedName = line.name; let resolvedPu = line.pu; + let connectedArticleId = null; if (line.sku) { const parentCheck = await runner.query(checkParentSql, { @@ -147,11 +163,103 @@ export async function insertLines( if (resolvedPu == null && art?.pu != null) { resolvedPu = art.pu; } + if (art?.id != null) { + connectedArticleId = art.id; + } } resolvedName = resolvedName || line.sku || null; resolvedPu = resolvedPu ?? 1; + // Link table check: if delivery manifest has supplier entity and line connects to an article + if (isDelivery && supplierEntityId && connectedArticleId) { + const linkRes = await runner.query(getArticleSupplierLinkSql, { + articleId: connectedArticleId, + entityId: supplierEntityId, + }); + const supplierLink = linkRes.rows[0]; + if (supplierLink) { + const oldPu = supplierLink.pu != null ? Number(supplierLink.pu) : 1; + const newPu = Number(resolvedPu); + const puChanged = Number.isFinite(newPu) && newPu > 0 && newPu !== oldPu; + + const oldNet = supplierLink.net != null ? Number(supplierLink.net) : null; + const lineNet = line.net != null && Number.isFinite(Number(line.net)) ? Number(line.net) : null; + const netChanged = lineNet !== null && lineNet > 0 && (oldNet === null || lineNet !== oldNet); + + const oldTaxRate = supplierLink.taxRate != null ? Number(supplierLink.taxRate) : null; + const oldTaxId = supplierLink.taxId ?? null; + const lineTaxRate = line.taxrate != null && Number.isFinite(Number(line.taxrate)) && Number(line.taxrate) >= 0 + ? Number(line.taxrate) + : null; + + let targetTaxId = oldTaxId; + let taxChanged = false; + + if (lineTaxRate !== null && (oldTaxId === null || oldTaxRate === null || lineTaxRate !== oldTaxRate)) { + if (ensureTax) { + targetTaxId = await ensureTax(lineTaxRate, runner); + taxChanged = targetTaxId !== null && (targetTaxId !== oldTaxId || lineTaxRate !== oldTaxRate); + } + } + + if (puChanged || netChanged || taxChanged) { + const nextPu = puChanged ? newPu : oldPu; + const nextNet = netChanged ? lineNet : oldNet; + const nextTaxId = taxChanged ? targetTaxId : oldTaxId; + + await runner.query(updateArticleSupplierLinkSql, { + id: supplierLink.id, + pu: nextPu, + net: nextNet, + taxId: nextTaxId, + }); + + // Update in-memory row in case duplicate line items appear in same manifest + supplierLink.pu = nextPu; + supplierLink.net = nextNet; + supplierLink.taxId = nextTaxId; + if (lineTaxRate !== null) { + supplierLink.taxRate = lineTaxRate; + } + + const supplierName = supplierLink.supplierName || manifestCtx?.entityName || `Lieferant #${supplierEntityId}`; + const details = []; + if (puChanged) { + details.push(`VPE: ${nextPu}`); + } + if (netChanged) { + details.push(`EK: ${formatCurrencyDe(nextNet)}`); + } + if (taxChanged && lineTaxRate !== null) { + details.push(`MwSt.: ${lineTaxRate}%`); + } + + const text = `Lieferant "${supplierName}" aktualisiert (${details.join(', ')})`; + const meta = { + type: 'supplier_update', + supplierName, + supplierSku: supplierLink.sku ?? null, + pu: nextPu, + netPrice: nextNet != null ? Number(nextNet) : null, + taxRate: lineTaxRate ?? oldTaxRate, + taxId: nextTaxId, + oldPu, + oldNetPrice: oldNet, + oldTaxRate, + oldTaxId, + }; + + await recordArticleHistory(runner, { + articleId: connectedArticleId, + companyuserId, + text, + meta, + }); + } + } + } + if (ensureTax) { await ensureTax(line.taxrate, runner); } diff --git a/src/store/stock/accept.js b/src/store/stock/accept.js index b7857e8..0934ec8 100644 --- a/src/store/stock/accept.js +++ b/src/store/stock/accept.js @@ -119,6 +119,10 @@ export default function accept(provider) { } const lineAmount = Number(manifestArticle.lineAmount || 0); + const linePu = manifestArticle.linePu != null && Number(manifestArticle.linePu) > 0 + ? Number(manifestArticle.linePu) + : 1; + const stockAmount = lineAmount * linePu; const deliveryNumber = manifestArticle.manifestNumber; const isReturn = lineAmount < 0; @@ -179,6 +183,7 @@ export default function accept(provider) { rebate: manifestArticle.rebate, taxrate: manifestArticle.taxrate, amount: docAmount, + pu: linePu, transactionText: docText, }); } @@ -191,7 +196,9 @@ export default function accept(provider) { { manifestarticleId, warehouseId: item.warehouseId, - amount: docAmount, + amount: stockAmount, + pu: linePu, + manifestAmount: lineAmount, text: docText, }, activeTx, diff --git a/src/store/stock/adjust.js b/src/store/stock/adjust.js index 3546bd1..887890f 100644 --- a/src/store/stock/adjust.js +++ b/src/store/stock/adjust.js @@ -71,7 +71,7 @@ export default function adjust(provider) { const { companyId, companyuserId } = await access(googleAuthId, runner); const checkRes = await runner.query( - `SELECT ma.id, ma.manifest_id, ma.sku, ma.name, m.company_id, m.type AS manifest_type, m.number AS manifest_number + `SELECT ma.id, ma.manifest_id, ma.sku, ma.name, ma.amount, ma.pu, m.company_id, m.type AS manifest_type, m.number AS manifest_number FROM manifestarticle ma INNER JOIN manifest m ON m.id = ma.manifest_id WHERE ma.id = @manifestarticleId AND m.company_id = @companyId`, @@ -173,6 +173,13 @@ export default function adjust(provider) { await runner.query(touchSql, { articleId: art.id }); await runner.query(touchStuecklisteSql, { articleId: art.id }); + const effectivePu = input.pu != null + ? Number(input.pu) + : (manifestArticle.pu != null && Number(manifestArticle.pu) > 0 ? Number(manifestArticle.pu) : 1); + const effectiveManifestAmount = input.manifestAmount != null + ? Number(input.manifestAmount) + : (manifestArticle.amount != null ? Number(manifestArticle.amount) : null); + // Write warehouse history for tracked article if (manifestArticle.manifest_type === 'delivery') { await recordWarehouseHistory(runner, { @@ -182,6 +189,8 @@ export default function adjust(provider) { manifestType: 'delivery', warehouseName, amount, + pu: effectivePu, + manifestAmount: effectiveManifestAmount, transactionText: text, }); } else if (manifestArticle.manifest_type === 'shipment') {