This commit is contained in:
seb
2026-10-12 17:29:29 +02:00
parent 3d6bc97411
commit 2e961252f7
44 changed files with 1166 additions and 374 deletions

View File

@@ -217,13 +217,13 @@ Returns the change history of an article by its SKU.
**Returns**
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>` — The article history entries.
`Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>` — The article history entries.
**Example**
```js
const history = await store.article.getHistoryBySku(42, 'SKU-1');
// => [{ id, companyuserId, date, text }]
// => [{ id, companyuserId, date, text, meta }]
const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
// => up to 5 most recent entries

19
API.articlehistory.md Normal file
View File

@@ -0,0 +1,19 @@
# articlehistory
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
## `articlehistory.record`
*Source: `src/store/articlehistory/record.js`*
Formats a numeric currency amount in German language style: "€ 1.000,99"
**Parameters**
| Name | Type | Description |
| --- | --- | --- |
| `amount` | `number|string` | The currency amount. |
**Returns**
`string` — The formatted currency string.

6
API.md
View File

@@ -11,7 +11,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)`
- **articleattributevalue**: `id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)`
- **articleean**: `id PK, article_id->article, ean:s(50) UQ`
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt`
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt, meta?:txt`
- **articlelist**: `id PK`
- **articlelistarticle**: `id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)`
- **articlepic**: `id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)`
@@ -93,7 +93,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, desc: string|null, seo: string|null, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, parentArticleId: number|null, parentSku: string|null}}>`
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>`
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
- `listChildren(googleAuthId: number, parentSku: string, tx?: object): Promise<Array<{id: number, sku: string, parentsort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
@@ -285,6 +285,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `create(googleAuthId: number, input: {type: string, name: string}, tx?: object): Promise<{id: number, companyId: number, type: string, name: string}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, companyId: number, type: string, name: string}>>`
- [articlehistory](API.articlehistory.md)
- `record(amount: number|string): string`
- [articlelist](API.articlelist.md)
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/order/create.js`*
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.
**Parameters**

View File

@@ -6,7 +6,7 @@ This document is auto-generated from the JSDoc comments in `src/store` by runnin
*Source: `src/store/shipping/create.js`*
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.
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 for all items, and ensures the order was not already shipped.
**Parameters**

View File

@@ -322,7 +322,7 @@ const SCHEMA_TABLES = [
['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, track_stock:b=1, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_parent:b=0, parentarticle_id?->article, parentattributename_id?->attributename, parentsort:i=0, created_at:dt, updated_at:dt, UQ(parentarticle_id, parentsort)'],
['articleattributevalue', 'id PK, article_id->article, attributevalue_id->attributevalue, sort:i=0, UQ(article_id, attributevalue_id)'],
['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'],
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt, meta?:txt'],
['articlelist', 'id PK'],
['articlelistarticle', 'id PK, articlelist_id->articlelist, article_id->article, count:i=1, sort:i=0, UQ(articlelist_id, article_id), UQ(articlelist_id, sort)'],
['articlepic', 'id PK, article_id->article, pic_id->pic, sort:i=0, UQ(article_id, pic_id), UQ(article_id, sort)'],

File diff suppressed because one or more lines are too long

View File

@@ -1246,49 +1246,53 @@
<!-- articlehistory -->
<g id="node23" class="node">
<title>articlehistory</title>
<polygon fill="#33415c" stroke="none" points="1648.85,-1073.6 1648.85,-1098 1814.89,-1098 1814.89,-1073.6 1648.85,-1073.6"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1073.6 1648.85,-1098 1814.89,-1098 1814.89,-1073.6 1648.85,-1073.6"/>
<text xml:space="preserve" text-anchor="start" x="1694.19" y="-1083.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">articlehistory</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1046.8 1648.85,-1073.6 1814.89,-1073.6 1814.89,-1046.8 1648.85,-1046.8"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1046.8 1648.85,-1073.6 1814.89,-1073.6 1814.89,-1046.8 1648.85,-1046.8"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1057" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
<text xml:space="preserve" text-anchor="start" x="1780.39" y="-1058.5" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.39" y="-1058.5" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1020 1648.85,-1046.8 1814.89,-1046.8 1814.89,-1020 1648.85,-1020"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1020 1648.85,-1046.8 1814.89,-1046.8 1814.89,-1020 1648.85,-1020"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1030.2" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">article_id</text>
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1031.7" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1031.7" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-993.2 1648.85,-1020 1814.89,-1020 1814.89,-993.2 1648.85,-993.2"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-993.2 1648.85,-1020 1814.89,-1020 1814.89,-993.2 1648.85,-993.2"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1003.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">companyuser_id</text>
<text xml:space="preserve" text-anchor="start" x="1765.11" y="-1003.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1004.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1004.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#272e3d" stroke="none" points="1648.85,-966.4 1648.85,-993.2 1814.89,-993.2 1814.89,-966.4 1648.85,-966.4"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-966.4 1648.85,-993.2 1814.89,-993.2 1814.89,-966.4 1648.85,-966.4"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-974.2" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">date</text>
<text xml:space="preserve" text-anchor="start" x="1759.39" y="-976.2" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
<polygon fill="#2a3242" stroke="none" points="1648.85,-939.6 1648.85,-966.4 1814.89,-966.4 1814.89,-939.6 1648.85,-939.6"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-939.6 1648.85,-966.4 1814.89,-966.4 1814.89,-939.6 1648.85,-939.6"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-947.4" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">text</text>
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-949.4" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
<polygon fill="#33415c" stroke="none" points="1648.85,-1087 1648.85,-1111.4 1814.89,-1111.4 1814.89,-1087 1648.85,-1087"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1087 1648.85,-1111.4 1814.89,-1111.4 1814.89,-1087 1648.85,-1087"/>
<text xml:space="preserve" text-anchor="start" x="1694.19" y="-1096.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="12.00" fill="#e2e8f0">articlehistory</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1060.2 1648.85,-1087 1814.89,-1087 1814.89,-1060.2 1648.85,-1060.2"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1060.2 1648.85,-1087 1814.89,-1087 1814.89,-1060.2 1648.85,-1060.2"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1070.4" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">id</text>
<text xml:space="preserve" text-anchor="start" x="1780.39" y="-1071.9" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.39" y="-1071.9" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">PK</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1033.4 1648.85,-1060.2 1814.89,-1060.2 1814.89,-1033.4 1648.85,-1033.4"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1033.4 1648.85,-1060.2 1814.89,-1060.2 1814.89,-1033.4 1648.85,-1033.4"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1043.6" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">article_id</text>
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1045.1" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1045.1" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#2b3a52" stroke="none" points="1648.85,-1006.6 1648.85,-1033.4 1814.89,-1033.4 1814.89,-1006.6 1648.85,-1006.6"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-1006.6 1648.85,-1033.4 1814.89,-1033.4 1814.89,-1006.6 1648.85,-1006.6"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-1016.8" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="14.00" fill="#d8dee9">companyuser_id</text>
<text xml:space="preserve" text-anchor="start" x="1765.11" y="-1016.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">?</text>
<text xml:space="preserve" text-anchor="start" x="1780.9" y="-1018.3" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">INT </text>
<text xml:space="preserve" text-anchor="start" x="1797.9" y="-1018.3" font-family="Helvetica,sans-Serif" font-weight="bold" font-size="9.00" fill="#8b95a7">FK</text>
<polygon fill="#272e3d" stroke="none" points="1648.85,-979.8 1648.85,-1006.6 1814.89,-1006.6 1814.89,-979.8 1648.85,-979.8"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-979.8 1648.85,-1006.6 1814.89,-1006.6 1814.89,-979.8 1648.85,-979.8"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-987.6" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">date</text>
<text xml:space="preserve" text-anchor="start" x="1759.39" y="-989.6" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">DATETIME2</text>
<polygon fill="#2a3242" stroke="none" points="1648.85,-953 1648.85,-979.8 1814.89,-979.8 1814.89,-953 1648.85,-953"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-953 1648.85,-979.8 1814.89,-979.8 1814.89,-953 1648.85,-953"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-960.8" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">text</text>
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-962.8" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
<polygon fill="#272e3d" stroke="none" points="1648.85,-926.2 1648.85,-953 1814.89,-953 1814.89,-926.2 1648.85,-926.2"/>
<polygon fill="none" stroke="#3f4c5f" points="1648.85,-926.2 1648.85,-953 1814.89,-953 1814.89,-926.2 1648.85,-926.2"/>
<text xml:space="preserve" text-anchor="start" x="1653.85" y="-934" font-family="Helvetica,sans-Serif" font-size="14.00" fill="#d8dee9">meta?</text>
<text xml:space="preserve" text-anchor="start" x="1733.89" y="-936" font-family="Helvetica,sans-Serif" font-size="9.00" fill="#8b95a7">NVARCHAR(MAX)</text>
</g>
<!-- articlehistory&#45;&gt;companyuser -->
<g id="edge28" class="edge">
<title>articlehistory:e&#45;&gt;companyuser:n</title>
<g id="a_edge28"><a xlink:title="FK_articlehistory_companyuser">
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M1815.89,-1006.6C1875.29,-1006.6 1809.1,-914.69 1856.87,-879.4 2089.17,-707.8 6820.72,-979.05 7054.87,-810 7120.81,-762.4 7117.3,-718.3 7116.98,-639.55"/>
<path fill="none" stroke="#5aa7ff" stroke-width="1.4" d="M1815.89,-1020C1880.98,-1020 1805.07,-918.81 1856.87,-879.4 2086.74,-704.56 6820.72,-979.05 7054.87,-810 7120.81,-762.4 7117.3,-718.3 7116.98,-639.55"/>
<polygon fill="#5aa7ff" stroke="#5aa7ff" stroke-width="1.4" points="7119.43,-639.91 7116.96,-632.92 7114.53,-639.92 7119.43,-639.91"/>
</a>
</g>
</g>
<!-- articlehistory&#45;&gt;article -->
<g id="edge27" class="edge">
<title>articlehistory:sw&#45;&gt;article:n</title>
<title>articlehistory:se&#45;&gt;article:n</title>
<g id="a_edge27"><a xlink:title="FK_articlehistory_article">
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M1647.85,-1020C1621.33,-993.47 1627.31,-966.31 1648.85,-935.6 1694.51,-870.52 1974.73,-886.23 1995.47,-813.78"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="1997.86,-814.37 1996.39,-807.1 1993.01,-813.7 1997.86,-814.37"/>
<path fill="none" stroke="#6bdf9a" stroke-width="1.4" d="M1815.89,-1033.4C1865.98,-983.32 1812.84,-934.87 1856.87,-879.4 1898.67,-826.74 1988.16,-871.01 1996.11,-813.77"/>
<polygon fill="#6bdf9a" stroke="#6bdf9a" stroke-width="1.4" points="1998.54,-814.26 1996.55,-807.11 1993.65,-813.94 1998.54,-814.26"/>
</a>
</g>
</g>

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 321 KiB

View File

@@ -145,8 +145,13 @@ async function run() {
// 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 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 orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)');
assert(orderHistory, 'Article history should contain order creation entry with formatted gross price');
assert(orderHistory.meta, 'Article history entry should have meta');
const parsedMeta = JSON.parse(orderHistory.meta);
assert.equal(parsedMeta.type, 'order');
assert.equal(parsedMeta.orderId, order.id);
assert.equal(parsedMeta.gross, 59.5);
const shippingHistory = history.find((h) => h.text.includes('Versand Auftrag'));
assert(!shippingHistory, 'Article history should NOT report the shipment');
@@ -154,14 +159,29 @@ async function run() {
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',
untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2, € 23,80)'),
'Untracked article history should contain order creation entry with formatted gross price',
);
assert(
!untrackedHistory.some((h) => h.text.includes('Versand Auftrag')),
'Untracked article history should NOT report the shipment',
);
// Test order import with historical date preserves date in article history
const historicalDate = '2023-04-15T10:30:00.000Z';
await store.order.create(googleAuthId, {
number: 'O-HIST-001',
date: historicalDate,
createdAt: historicalDate,
lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }],
});
const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku);
const oldEntry = histWithOld.find((h) => h.text.startsWith('Auftrag O-HIST-001'));
assert(oldEntry, 'Historical order entry should exist in article history');
assert.equal(new Date(oldEntry.date).toISOString(), historicalDate, 'History entry should preserve historical order date');
const oldMeta = JSON.parse(oldEntry.meta);
assert.equal(oldMeta.orderNumber, 'O-HIST-001');
// 6. Test immutability: once shipped, cannot be deleted
console.log('6. Testing cannot delete shipped shipping...');
await assert.rejects(

View File

@@ -1,17 +1,17 @@
export const INSERT = `
INSERT INTO articlehistory (article_id, companyuser_id, [text])
VALUES (@articleId, @companyuserId, @text)
INSERT INTO articlehistory (article_id, companyuser_id, [date], [text], meta)
VALUES (@articleId, @companyuserId, COALESCE(@date, SYSUTCDATETIME()), @text, @meta)
`;
export const LIST_BY_ARTICLE_ID = `
SELECT id, companyuser_id AS companyuserId, [date], [text]
SELECT id, companyuser_id AS companyuserId, [date], [text], meta
FROM articlehistory
WHERE article_id = @articleId
ORDER BY [date] DESC, id DESC
`;
export const LIST_BY_ARTICLE_ID_LIMITED = `
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text]
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text], meta
FROM articlehistory
WHERE article_id = @articleId
ORDER BY [date] DESC, id DESC

View File

@@ -38,8 +38,8 @@ WHERE sku = @sku
`;
export const INSERT_ARTICLE_HISTORY = `
INSERT INTO articlehistory (article_id, companyuser_id, [text])
VALUES (@articleId, @companyuserId, @text);
INSERT INTO articlehistory (article_id, companyuser_id, [date], [text], meta)
VALUES (@articleId, @companyuserId, COALESCE(@date, SYSUTCDATETIME()), @text, @meta);
`;
export const GET_LATEST_ROW = `

View File

@@ -372,6 +372,7 @@ BEGIN
companyuser_id INT NULL,
[date] DATETIME2 NOT NULL CONSTRAINT DF_articlehistory_date DEFAULT SYSUTCDATETIME(),
[text] NVARCHAR(MAX) NOT NULL,
meta NVARCHAR(MAX) NULL,
CONSTRAINT FK_articlehistory_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
CONSTRAINT FK_articlehistory_companyuser FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
);

View File

@@ -5,7 +5,7 @@ const queries = translateQueries(source, 'articlehistory');
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
export const LIST_BY_ARTICLE_ID_LIMITED = `
SELECT id, companyuser_id AS companyuserId, date, text
SELECT id, companyuser_id AS companyuserId, date, text, meta
FROM articlehistory
WHERE article_id = @articleId
ORDER BY date DESC, id DESC

View File

@@ -38,8 +38,8 @@ WHERE sku = @sku
`;
export const INSERT_ARTICLE_HISTORY = `
INSERT INTO articlehistory (article_id, companyuser_id, text)
VALUES (@articleId, @companyuserId, @text);
INSERT INTO articlehistory (article_id, companyuser_id, date, text, meta)
VALUES (@articleId, @companyuserId, COALESCE(@date, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), @text, @meta);
`;
export const GET_LATEST_ROW = `

View File

@@ -551,6 +551,7 @@ CREATE TABLE IF NOT EXISTS articlehistory (
companyuser_id INTEGER,
"date" TEXT NOT NULL DEFAULT (${UTC_NOW}),
"text" TEXT NOT NULL,
meta TEXT,
FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
FOREIGN KEY (companyuser_id) REFERENCES companyuser(id) ON DELETE SET NULL
);

View File

@@ -10,12 +10,14 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
import {
recordVariantAddedOnParentHistory,
recordChildAssignedToParentHistory,
} from '../articlehistory/record.js';
import attachChild from './attachChild.js';
export default function addChild(provider) {
@@ -31,7 +33,6 @@ export default function addChild(provider) {
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Adds a child article to a parent article (creates and attaches with auto-generated SKU).
@@ -242,17 +243,21 @@ export default function addChild(provider) {
});
// History on child
await runner.query(insertHistorySql, {
articleId: childId,
await recordChildAssignedToParentHistory(runner, {
childId,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" angelegt`,
childName,
childSku,
parentSku: parent.sku,
created: true,
});
// History on parent
await runner.query(insertHistorySql, {
articleId: parentId,
await recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
childName,
childSku,
});
return load(googleAuthId, childSku, runner);

View File

@@ -6,11 +6,13 @@ import * as mssqlArticleAttrVal from '../../providers/mssql/queries/articleattri
import * as sqliteArticleAttrVal from '../../providers/sqlite3/queries/articleattributevalue.js';
import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/categoryarticle.js';
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import listChildren from './listChildren.js';
import {
recordChildAssignedToParentHistory,
recordVariantAddedOnParentHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function attachChild(provider) {
@@ -23,7 +25,6 @@ export default function attachChild(provider) {
const attrValQueries = isSqlite ? sqliteAttrVal : mssqlAttrVal;
const articleAttrValQueries = isSqlite ? sqliteArticleAttrVal : mssqlArticleAttrVal;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Attaches an existing article as a child to a parent article.
@@ -288,16 +289,19 @@ export default function attachChild(provider) {
// History only if not already child
if (!isAlreadyChildOfThisParent) {
await runner.query(insertHistorySql, {
articleId: child.id,
await recordChildAssignedToParentHistory(runner, {
childId: child.id,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${parent.sku}" zugewiesen`,
childName,
childSku,
parentSku: parent.sku,
});
await runner.query(insertHistorySql, {
articleId: parentId,
await recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
childName,
childSku,
});
}

View File

@@ -4,14 +4,13 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import nextSku from '../numbers/nextSku.js';
import makeRunner from '../runner.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
import { recordArticleCreatedHistory } from '../articlehistory/record.js';
export default function createUnderParent(provider) {
const access = resolveCategoryAccess(provider);
@@ -23,7 +22,6 @@ export default function createUnderParent(provider) {
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Creates a new article under a given parent category, allocating a SKU and
@@ -95,10 +93,10 @@ export default function createUnderParent(provider) {
seo: finalSeo,
});
await runner.query(insertHistorySql, {
await recordArticleCreatedHistory(runner, {
articleId: id,
companyuserId,
text: `Artikel "${name}" angelegt`,
name,
});
return {

View File

@@ -17,11 +17,11 @@ export default function getHistoryBySku(provider) {
* @param {object} [opts] - Optional options.
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string, meta: string|null}>>} The article history entries.
*
* @example
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
* // => [{ id, companyuserId, date, text }]
* // => [{ id, companyuserId, date, text, meta }]
*
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
* // => up to 5 most recent entries
@@ -51,6 +51,7 @@ export default function getHistoryBySku(provider) {
companyuserId: row.companyuserId ?? null,
date: row.date,
text: row.text,
meta: row.meta ?? null,
}));
};
}

View File

@@ -4,10 +4,14 @@ import { ADD as MSSQL_ADD_CATEGORYARTICLE } from '../../providers/mssql/queries/
import { ADD as SQLITE_ADD_CATEGORYARTICLE } from '../../providers/sqlite3/queries/categoryarticle.js';
import { CREATE as MSSQL_CREATE_ARTICLETEXT } from '../../providers/mssql/queries/articletext.js';
import { CREATE as SQLITE_CREATE_ARTICLETEXT } from '../../providers/sqlite3/queries/articletext.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import {
recordParentConvertedHistory,
recordVariantAddedOnParentHistory,
recordChildAssignedToParentHistory,
recordArticleImportedHistory,
} from '../articlehistory/record.js';
import {
CREATE as MSSQL_CREATE_BRAND,
GET_BY_COMPANY_AND_NAME as MSSQL_GET_BRAND_BY_NAME,
@@ -55,7 +59,6 @@ export default function importRows(provider) {
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
const addCategoryArticleSql = isSqlite ? SQLITE_ADD_CATEGORYARTICLE : MSSQL_ADD_CATEGORYARTICLE;
const createArticleTextSql = isSqlite ? SQLITE_CREATE_ARTICLETEXT : MSSQL_CREATE_ARTICLETEXT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const getBrandByNameSql = isSqlite ? SQLITE_GET_BRAND_BY_NAME : MSSQL_GET_BRAND_BY_NAME;
const createBrandSql = isSqlite ? SQLITE_CREATE_BRAND : MSSQL_CREATE_BRAND;
@@ -210,10 +213,10 @@ export default function importRows(provider) {
seo: null,
});
await runner.query(insertHistorySql, {
await recordArticleImportedHistory(runner, {
articleId,
companyuserId,
text: `Artikel "${name}" importiert`,
name,
});
// Brand (upsert by name).
@@ -318,22 +321,24 @@ export default function importRows(provider) {
});
if (!wasParent) {
await runner.query(insertHistorySql, {
await recordParentConvertedHistory(runner, {
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
const childName = String(row?.name ?? sku).trim();
await runner.query(insertHistorySql, {
articleId: parentId,
await recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
childName,
childSku: sku,
});
await runner.query(insertHistorySql, {
articleId: childId,
await recordChildAssignedToParentHistory(runner, {
childId,
companyuserId,
text: `Artikel "${childName}" (${sku}) als Variante von "${parentSku}" zugewiesen`,
childName,
childSku: sku,
parentSku,
});
}
}

View File

@@ -1,9 +1,11 @@
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import articleDelete from './delete.js';
import {
recordChildUnlinkedFromParentHistory,
recordChildDeletedFromParentHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function removeChild(provider) {
@@ -12,7 +14,6 @@ export default function removeChild(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Removes a child article from a parent article, either by unlinking or deleting it.
@@ -78,10 +79,11 @@ export default function removeChild(provider) {
await recompactParentSort();
// History on parent (child article is gone so we can only log on parent)
await runner.query(insertHistorySql, {
articleId: parentId,
await recordChildDeletedFromParentHistory(runner, {
parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) gelöscht`,
childName,
childSku,
});
return {
@@ -119,18 +121,14 @@ export default function removeChild(provider) {
await recompactParentSort();
// Add history on child
await runner.query(insertHistorySql, {
articleId: childId,
// Add history on child and parent
await recordChildUnlinkedFromParentHistory(runner, {
childId,
parentId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
});
// Add history on parent
await runner.query(insertHistorySql, {
articleId: parentId,
companyuserId,
text: `Variante "${childName}" (${childSku}) getrennt`,
childSku,
childName,
parentSku,
});
return {

View File

@@ -6,10 +6,9 @@ import {
GET_BRAND_BY_COMPANY_AND_ID as SQLITE_GET_BRAND,
UPDATE_BRAND as SQLITE_UPDATE_BRAND,
} from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordBrandChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setBrandIdBySku(provider) {
@@ -19,7 +18,6 @@ export default function setBrandIdBySku(provider) {
const getBrandSql = isSqlite ? SQLITE_GET_BRAND : MSSQL_GET_BRAND;
const updateBrandSql = isSqlite ? SQLITE_UPDATE_BRAND : MSSQL_UPDATE_BRAND;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Sets (or clears) the brand of an article by its SKU.
@@ -82,13 +80,12 @@ export default function setBrandIdBySku(provider) {
}
// null = no brand; treat as distinct sentinel so first assignment is logged
if (oldBrandName !== newBrandName) {
await runner.query(insertHistorySql, {
await recordBrandChangeHistory(runner, {
articleId,
companyuserId,
text: `Marke ge\u00e4ndert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`,
oldBrandName,
newBrandName,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -6,11 +6,10 @@ import {
ADD_CATEGORY as SQLITE_ADD_CATEGORY,
DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES,
} from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordCategoryChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setCategoryIdsBySku(provider) {
@@ -21,7 +20,6 @@ export default function setCategoryIdsBySku(provider) {
const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY;
const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Replaces the category membership of an article by its SKU.
@@ -86,15 +84,12 @@ export default function setCategoryIdsBySku(provider) {
}
// Write history if categories changed
const oldStr = oldCategoryIds.join(', ');
const newStr = [...normalized].sort((a, b) => a - b).join(', ');
if (oldStr !== newStr) {
await runner.query(insertHistorySql, {
await recordCategoryChangeHistory(runner, {
articleId,
companyuserId,
text: `Kategorien geändert von [${oldStr}] auf [${newStr}]`,
oldCategoryIds,
newCategoryIds: normalized,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -8,11 +8,10 @@ import {
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT,
} from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import { isUniqueConstraintError } from '../../providers/provider.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordEanChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setEansBySku(provider) {
@@ -22,7 +21,6 @@ export default function setEansBySku(provider) {
const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN;
const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT;
/**
@@ -93,15 +91,12 @@ export default function setEansBySku(provider) {
}
// Write history if EANs changed
const oldStr = oldEans.join(', ');
const newStr = [...normalized].sort().join(', ');
if (oldStr !== newStr) {
await runner.query(insertHistorySql, {
await recordEanChangeHistory(runner, {
articleId,
companyuserId,
text: `EANs geändert von "${oldStr}" auf "${newStr}"`,
oldEans,
newEans: normalized,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -2,10 +2,13 @@ import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import {
recordParentConvertedHistory,
recordVariantAddedOnParentHistory,
recordChildAssignedToParentHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setParentBySku(provider) {
@@ -15,7 +18,6 @@ export default function setParentBySku(provider) {
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Links an article to a parent article (V/K compound) by their SKUs.
@@ -118,23 +120,25 @@ export default function setParentBySku(provider) {
});
if (!wasParent) {
await runner.query(insertHistorySql, {
await recordParentConvertedHistory(runner, {
articleId: parentId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
await runner.query(insertHistorySql, {
articleId: parentId,
await recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
childName,
childSku: sku,
});
await runner.query(insertHistorySql, {
articleId,
await recordChildAssignedToParentHistory(runner, {
childId: articleId,
companyuserId,
text: `Artikel "${sku}" als Variante von "${parentSku}" zugewiesen`,
childName: null,
childSku: sku,
parentSku,
});
const article = await load(articleId, languageId, runner);

View File

@@ -6,11 +6,10 @@ import {
CREATE as SQLITE_CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
} from '../../providers/sqlite3/queries/price.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import taxEnsure from '../tax/ensure.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordPriceChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
function normalizePrices(prices) {
@@ -66,7 +65,6 @@ export default function setPricesBySku(provider) {
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Replaces the price list of an article by its SKU.
@@ -136,16 +134,12 @@ export default function setPricesBySku(provider) {
}
// Write history if prices changed
const fmtPrice = (p) => `${p.net}@${p.taxRate ?? 0}%`;
const oldStr = oldPrices.map(fmtPrice).join(', ');
const newStr = normalized.map((e) => `${e.price}@${e.tax}%`).join(', ');
if (oldStr !== newStr) {
await runner.query(insertHistorySql, {
await recordPriceChangeHistory(runner, {
articleId,
companyuserId,
text: `Preise geändert von [${oldStr}] auf [${newStr}]`,
oldPrices,
newPrices: normalized,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -1,9 +1,8 @@
import { UPDATE_TRACK_STOCK as MSSQL_UPDATE_TRACK_STOCK } from '../../providers/mssql/queries/article.js';
import { UPDATE_TRACK_STOCK as SQLITE_UPDATE_TRACK_STOCK } from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordTrackStockChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setTrackStockBySku(provider) {
@@ -13,7 +12,6 @@ export default function setTrackStockBySku(provider) {
provider?.dialect === 'sqlite3'
? SQLITE_UPDATE_TRACK_STOCK
: MSSQL_UPDATE_TRACK_STOCK;
const insertHistorySql = provider?.dialect === 'sqlite3' ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Sets the track_stock flag of an article by its SKU.
@@ -55,13 +53,12 @@ export default function setTrackStockBySku(provider) {
}
// Write history if track_stock changed
if (oldTrack !== newTrack) {
await runner.query(insertHistorySql, {
await recordTrackStockChangeHistory(runner, {
articleId,
companyuserId,
text: `Bestandsführung geändert von "${oldTrack ? 'Ja' : 'Nein'}" auf "${newTrack ? 'Ja' : 'Nein'}"`,
oldTrack,
newTrack,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -1,16 +1,14 @@
import { UPDATE_WEIGHT as MSSQL_UPDATE_WEIGHT } from '../../providers/mssql/queries/article.js';
import { UPDATE_WEIGHT as SQLITE_UPDATE_WEIGHT } from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import { recordWeightChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setWeightBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_UPDATE_WEIGHT : MSSQL_UPDATE_WEIGHT;
const insertHistorySql = provider?.dialect === 'sqlite3' ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Sets the weight of an article by its SKU.
@@ -51,13 +49,12 @@ export default function setWeightBySku(provider) {
}
// Write history if weight changed
if (Number(oldWeight) !== weight) {
await runner.query(insertHistorySql, {
await recordWeightChangeHistory(runner, {
articleId,
companyuserId,
text: `Gewicht geändert von "${oldWeight}" auf "${weight}"`,
oldWeight,
newWeight: weight,
});
}
const article = await load(articleId, languageId, runner);
if (!article) {

View File

@@ -4,10 +4,12 @@ import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import * as mssqlAttr from '../../providers/mssql/queries/attributename.js';
import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import {
recordParentConvertedHistory,
recordParentDeactivatedHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function toggleParent(provider) {
@@ -18,7 +20,6 @@ export default function toggleParent(provider) {
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const attrQueries = isSqlite ? sqliteAttr : mssqlAttr;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Toggles an article into or out of parent mode.
@@ -142,32 +143,11 @@ export default function toggleParent(provider) {
});
// Add history entry recording parent conversion and deleted prices
const historyLines = [
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
];
if (existingPrices.length > 0) {
const priceParts = existingPrices.map((p) => {
let str = `${p.net} €`;
if (p.taxRate != null) {
str += ` (${p.taxRate}% MwSt.)`;
}
if (p.minmultiplier > 1) {
str += ` ab ${p.minmultiplier} Stk.`;
}
if (p.customerGroupName) {
str += ` [${p.customerGroupName}]`;
}
return str;
});
const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht';
historyLines.push(`${label}: ${priceParts.join(', ')}`);
}
await runner.query(insertHistorySql, {
await recordParentConvertedHistory(runner, {
articleId,
companyuserId,
text: historyLines.join('\n'),
attributeName,
existingPrices,
});
} else {
// Disabling parent: verify zero children
@@ -183,10 +163,9 @@ export default function toggleParent(provider) {
parentAttributeNameId: null,
});
await runner.query(insertHistorySql, {
await recordParentDeactivatedHistory(runner, {
articleId,
companyuserId,
text: 'Vaterartikel-Modus deaktiviert',
});
}

View File

@@ -1,7 +1,5 @@
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as MSSQL_UPDATE_TEXT } from '../../providers/mssql/queries/articletext.js';
import { UPDATE_BY_ARTICLE_AND_LANGUAGE as SQLITE_UPDATE_TEXT } from '../../providers/sqlite3/queries/articletext.js';
import { GET_DETAIL_BY_ARTICLE_ID as MSSQL_GET_DETAIL } from '../../providers/mssql/queries/articleDetail.js';
@@ -12,6 +10,7 @@ import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
import { recordArticleUpdateHistory } from '../articlehistory/record.js';
function formatUnitLabel(amount, symbol) {
const sym = symbol ?? '';
@@ -28,7 +27,6 @@ export default function updateBySku(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
const updateTextSql = isSqlite ? SQLITE_UPDATE_TEXT : MSSQL_UPDATE_TEXT;
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
const getUnitSql = isSqlite ? SQLITE_GET_UNIT : MSSQL_GET_UNIT;
@@ -221,10 +219,10 @@ export default function updateBySku(provider) {
}
if (historyLines.length) {
await runner.query(insertHistorySql, {
await recordArticleUpdateHistory(runner, {
articleId,
companyuserId,
text: historyLines.join('\n'),
historyLines,
});
}

View File

@@ -8,9 +8,8 @@ import {
INSERT_ROW as SQLITE_INSERT,
GET_VALUE_DETAIL as SQLITE_GET_DETAIL,
} from '../../providers/sqlite3/queries/articleattributevalue.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import { recordAttributeChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setByArticleSku(provider) {
@@ -19,7 +18,6 @@ export default function setByArticleSku(provider) {
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Replaces all attribute values for an article by its SKU.
@@ -182,16 +180,12 @@ export default function setByArticleSku(provider) {
}
// Write history if attribute values changed
const fmtAttr = (a) => `${a.attributeName}: ${a.value}`;
const oldStr = oldAttrs.map(fmtAttr).join(', ');
const newStr = validated.map(fmtAttr).join(', ');
if (oldStr !== newStr) {
await runner.query(insertHistorySql, {
await recordAttributeChangeHistory(runner, {
articleId,
companyuserId,
text: `Attributwerte geändert von [${oldStr}] auf [${newStr}]`,
oldAttrs,
newAttrs: validated,
});
}
return inserted;
};

View File

@@ -0,0 +1,855 @@
import { INSERT as MSSQL_INSERT } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT } from '../../providers/sqlite3/queries/articlehistory.js';
/**
* Formats a numeric currency amount in German language style: "€ 1.000,99"
*
* @param {number|string} amount - The currency amount.
* @returns {string} The formatted currency string.
*/
export function formatCurrencyDe(amount) {
if (amount == null || !Number.isFinite(Number(amount))) return '';
const num = Number(amount);
const formatted = new Intl.NumberFormat('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(num);
return `€ ${formatted}`;
}
/**
* Calculates unit gross price given net, rebate, and taxrate.
*
* @param {number|string} net - Unit net price.
* @param {number|string} [rebate=0] - Rebate percentage.
* @param {number|string} [taxrate=0] - Tax rate percentage.
* @returns {number} The calculated gross price.
*/
export function calculateGross(net, rebate = 0, taxrate = 0) {
const n = Number(net || 0);
const r = Number(rebate || 0);
const t = Number(taxrate || 0);
return n * (1 - r / 100) * (1 + t / 100);
}
/**
* Core function for recording an article history entry.
* Serializes meta into a JSON string if provided as an object.
*
* @param {object} runner - The database runner.
* @param {object} params - Insertion parameters.
* @param {number} params.articleId - The ID of the article.
* @param {number|null} [params.companyuserId] - The ID of the acting company user.
* @param {string} params.text - The human-readable history text.
* @param {object|string|null} [params.meta] - Structured meta object or JSON string.
* @param {string} [params.type] - Optional fallback type if meta is not an object.
* @returns {Promise<object>} Query result.
*/
export async function recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
type,
}) {
if (!articleId) {
throw new Error('articleId required for article history');
}
const cleanText = typeof text === 'string' ? text.trim() : '';
if (!cleanText) {
throw new Error('text required for article history');
}
const isSqlite = runner?.dialect === 'sqlite3';
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
let metaObj = meta;
if (metaObj === undefined || metaObj === null) {
metaObj = type ? { type } : null;
} else if (typeof metaObj === 'object' && type && !metaObj.type) {
metaObj = { type, ...metaObj };
}
let serializedMeta = null;
if (metaObj !== undefined && metaObj !== null) {
serializedMeta = typeof metaObj === 'string' ? metaObj : JSON.stringify(metaObj);
}
let parsedDate = null;
if (date != null) {
parsedDate = date instanceof Date ? date : new Date(date);
if (Number.isNaN(parsedDate.getTime())) {
parsedDate = null;
}
}
return runner.query(insertSql, {
articleId,
companyuserId: companyuserId ?? null,
date: parsedDate,
text: cleanText,
meta: serializedMeta,
});
}
/**
* Records an order entry in article history, including German currency formatted gross price.
* Format: "Auftrag {number} (Kunde: "{customerName}", Menge: {amount}, € 1.000,99)"
*/
export async function recordOrderHistory(runner, {
articleId,
companyuserId,
date,
orderId,
number,
customerName,
amount,
net,
rebate,
taxrate,
}) {
const details = [];
if (customerName) {
details.push(`Kunde: "${customerName}"`);
}
if (amount != null && Number.isFinite(Number(amount))) {
details.push(`Menge: ${amount}`);
}
let gross = null;
if (net != null && Number.isFinite(Number(net))) {
gross = calculateGross(net, rebate, taxrate);
const formattedGross = formatCurrencyDe(gross);
if (formattedGross) {
details.push(formattedGross);
}
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
const text = `Auftrag ${number}${suffix}`;
const meta = {
type: 'order',
...(orderId != null ? { orderId } : {}),
orderNumber: number,
customerName: customerName ?? null,
amount: amount != null ? Number(amount) : null,
net: net != null ? Number(net) : null,
rebate: rebate != null ? Number(rebate) : 0,
taxrate: taxrate != null ? Number(taxrate) : 0,
gross: gross != null ? Math.round(gross * 100) / 100 : null,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records a 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,
}) {
let historyText = transactionText;
let ekBrutto = null;
if (isDelivery) {
const details = [];
if (supplierName) {
details.push(`Lieferant: "${supplierName}"`);
}
if (warehouseName) {
details.push(`Lager: "${warehouseName}"`);
}
if (net != null && Number.isFinite(Number(net))) {
const netNum = Number(net);
const rebateNum = Number(rebate ?? 0);
const taxrateNum = Number(taxrate ?? 0);
const netAfterRebate = netNum * (1 - rebateNum / 100);
const brutto = netAfterRebate * (1 + taxrateNum / 100);
ekBrutto = (Math.round(brutto * 100) / 100).toFixed(2);
details.push(`EK brutto: ${ekBrutto}`);
}
if (amount != null && Number.isFinite(Number(amount))) {
details.push(`Menge: ${amount}`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
const base =
transactionText?.trim() ||
(manifestNumber ? `Lieferung ${manifestNumber}` : 'Lieferung');
historyText = `${base}${suffix}`;
} else if (warehouseName && transactionText && !transactionText.includes(warehouseName)) {
historyText = `${transactionText} (Lager: "${warehouseName}")`;
}
const meta = {
type: isDelivery ? 'delivery' : 'movement',
manifestNumber: manifestNumber ?? null,
supplierName: supplierName ?? null,
warehouseName: warehouseName ?? null,
amount: amount != null ? Number(amount) : null,
net: net != null ? Number(net) : null,
rebate: rebate != null ? Number(rebate) : 0,
taxrate: taxrate != null ? Number(taxrate) : 0,
ekBrutto: ekBrutto != null ? Number(ekBrutto) : null,
transactionText: transactionText ?? null,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text: historyText,
meta,
});
}
/**
* Records a stock adjustment in article history.
*/
export async function recordStockAdjustmentHistory(runner, {
articleId,
companyuserId,
date,
amount,
reason,
}) {
const sign = amount > 0 ? '+' : '';
const text = `Bestandskorrektur ${sign}${amount} (${reason})`;
const meta = {
type: 'stock_adjustment',
amount,
reason,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records price changes in article history.
*/
export async function recordPriceChangeHistory(runner, {
articleId,
companyuserId,
date,
oldPrices,
newPrices,
}) {
const fmtPrice = (p) => `${p.net ?? p.price}@${p.taxRate ?? p.tax ?? 0}%`;
const oldStr = (oldPrices || []).map(fmtPrice).join(', ');
const newStr = (newPrices || []).map(fmtPrice).join(', ');
if (oldStr === newStr) return;
const text = `Preise geändert von [${oldStr}] auf [${newStr}]`;
const meta = {
type: 'price_change',
oldPrices,
newPrices,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records category assignment changes in article history.
*/
export async function recordCategoryChangeHistory(runner, {
articleId,
companyuserId,
date,
oldCategoryIds,
newCategoryIds,
}) {
const oldStr = [...(oldCategoryIds || [])].sort((a, b) => a - b).join(', ');
const newStr = [...(newCategoryIds || [])].sort((a, b) => a - b).join(', ');
if (oldStr === newStr) return;
const text = `Kategorien geändert von [${oldStr}] auf [${newStr}]`;
const meta = {
type: 'category_change',
oldCategoryIds,
newCategoryIds,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records brand changes in article history.
*/
export async function recordBrandChangeHistory(runner, {
articleId,
companyuserId,
date,
oldBrandName,
newBrandName,
}) {
if (oldBrandName === newBrandName) return;
const text = `Marke geändert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`;
const meta = {
type: 'brand_change',
oldBrandName: oldBrandName ?? null,
newBrandName: newBrandName ?? null,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records EAN changes in article history.
*/
export async function recordEanChangeHistory(runner, {
articleId,
companyuserId,
date,
oldEans,
newEans,
}) {
const oldStr = (oldEans || []).join(', ');
const newStr = [...(newEans || [])].sort().join(', ');
if (oldStr === newStr) return;
const text = `EANs geändert von "${oldStr}" auf "${newStr}"`;
const meta = {
type: 'ean_change',
oldEans,
newEans,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records weight changes in article history.
*/
export async function recordWeightChangeHistory(runner, {
articleId,
companyuserId,
date,
oldWeight,
newWeight,
}) {
if (Number(oldWeight) === Number(newWeight)) return;
const text = `Gewicht geändert von "${oldWeight}" auf "${newWeight}"`;
const meta = {
type: 'weight_change',
oldWeight: Number(oldWeight),
newWeight: Number(newWeight),
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records track_stock changes in article history.
*/
export async function recordTrackStockChangeHistory(runner, {
articleId,
companyuserId,
date,
oldTrack,
newTrack,
}) {
if (Boolean(oldTrack) === Boolean(newTrack)) return;
const text = `Bestandsführung geändert von "${oldTrack ? 'Ja' : 'Nein'}" auf "${newTrack ? 'Ja' : 'Nein'}"`;
const meta = {
type: 'track_stock_change',
oldTrackStock: Boolean(oldTrack),
newTrackStock: Boolean(newTrack),
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records general article field updates (name, unit, description, track_stock).
*/
export async function recordArticleUpdateHistory(runner, {
articleId,
companyuserId,
date,
historyLines,
changes,
}) {
if (!historyLines || !historyLines.length) return;
const text = historyLines.join('\n');
const meta = {
type: 'article_update',
changes: changes ?? historyLines,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records initial article creation.
*/
export async function recordArticleCreatedHistory(runner, {
articleId,
companyuserId,
date,
name,
}) {
const text = `Artikel "${name}" angelegt`;
const meta = {
type: 'article_created',
name,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records article import.
*/
export async function recordArticleImportedHistory(runner, {
articleId,
companyuserId,
date,
name,
}) {
const text = `Artikel "${name}" importiert`;
const meta = {
type: 'article_imported',
name,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
date,
text,
meta,
});
}
/**
* Records conversion of an article to parent article.
*/
export async function recordParentConvertedHistory(runner, {
articleId,
companyuserId,
attributeName,
existingPrices,
}) {
const historyLines = [
attributeName
? `Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`
: 'Artikel zu Vaterartikel konvertiert',
];
if (existingPrices && existingPrices.length > 0) {
const priceParts = existingPrices.map((p) => {
let str = `${p.net} €`;
if (p.taxRate != null) str += ` (${p.taxRate}% MwSt.)`;
if (p.minmultiplier > 1) str += ` ab ${p.minmultiplier} Stk.`;
if (p.customerGroupName) str += ` [${p.customerGroupName}]`;
return str;
});
const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht';
historyLines.push(`${label}: ${priceParts.join(', ')}`);
}
const text = historyLines.join('\n');
const meta = {
type: 'parent_converted',
attributeName: attributeName ?? null,
deletedPricesCount: existingPrices ? existingPrices.length : 0,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records deactivation of parent article mode.
*/
export async function recordParentDeactivatedHistory(runner, {
articleId,
companyuserId,
}) {
const text = 'Vaterartikel-Modus deaktiviert';
const meta = {
type: 'parent_deactivated',
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records adding a variant onto a parent article.
*/
export async function recordVariantAddedOnParentHistory(runner, {
parentId,
companyuserId,
childName,
childSku,
}) {
const text = `Variante "${childName}" (${childSku}) hinzugefügt`;
const meta = {
type: 'variant_added_to_parent',
childName,
childSku,
};
return recordArticleHistory(runner, {
articleId: parentId,
companyuserId,
text,
meta,
});
}
/**
* Records assigning a child article to a parent article.
*/
export async function recordChildAssignedToParentHistory(runner, {
childId,
companyuserId,
childName,
childSku,
parentSku,
created = false,
}) {
const text = created
? `Artikel "${childName}" (${childSku}) als Variante von "${parentSku}" angelegt`
: (childName
? `Artikel "${childName}" (${childSku}) als Variante von "${parentSku}" zugewiesen`
: `Artikel "${childSku}" als Variante von "${parentSku}" zugewiesen`);
const meta = {
type: created ? 'child_created_under_parent' : 'child_assigned_to_parent',
childName: childName ?? null,
childSku,
parentSku,
};
return recordArticleHistory(runner, {
articleId: childId,
companyuserId,
text,
meta,
});
}
/**
* Records unlinking a child article from a parent article on both child and parent.
*/
export async function recordChildUnlinkedFromParentHistory(runner, {
childId,
parentId,
companyuserId,
childSku,
childName,
parentSku,
}) {
await recordArticleHistory(runner, {
articleId: childId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${parentSku}" getrennt`,
meta: {
type: 'child_unlinked',
childSku,
parentSku,
},
});
await recordArticleHistory(runner, {
articleId: parentId,
companyuserId,
text: `Variante "${childName || childSku}" (${childSku}) getrennt`,
meta: {
type: 'variant_separated',
childSku,
childName: childName ?? null,
},
});
}
/**
* Records deleting a variant child article from a parent.
*/
export async function recordChildDeletedFromParentHistory(runner, {
parentId,
companyuserId,
childName,
childSku,
}) {
const text = `Variante "${childName || childSku}" (${childSku}) gelöscht`;
const meta = {
type: 'variant_deleted',
childName: childName ?? null,
childSku,
};
return recordArticleHistory(runner, {
articleId: parentId,
companyuserId,
text,
meta,
});
}
/**
* Records adding a supplier to an article.
*/
export async function recordSupplierAddHistory(runner, {
articleId,
companyuserId,
supplierName,
supplierSku,
netPrice,
}) {
const details = [];
if (supplierSku) details.push(`Artikelnr: "${supplierSku}"`);
if (netPrice !== null && netPrice !== undefined) details.push(`EK: ${netPrice}`);
const suffix = details.length ? ` (${details.join(', ')})` : '';
const text = `Lieferant "${supplierName}" hinzugefügt${suffix}`;
const meta = {
type: 'supplier_add',
supplierName,
supplierSku: supplierSku ?? null,
netPrice: netPrice ?? null,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records updating supplier details on an article.
*/
export async function recordSupplierUpdateHistory(runner, {
articleId,
companyuserId,
historyLines,
supplierName,
}) {
if (!historyLines || !historyLines.length) return;
const text = historyLines.join('\n');
const meta = {
type: 'supplier_update',
supplierName,
changes: historyLines,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records deleting a supplier from an article.
*/
export async function recordSupplierDeleteHistory(runner, {
articleId,
companyuserId,
supplierName,
supplierSku,
}) {
const suffix = supplierSku ? ` (Artikelnr: "${supplierSku}")` : '';
const text = `Lieferant "${supplierName}" entfernt${suffix}`;
const meta = {
type: 'supplier_delete',
supplierName,
supplierSku: supplierSku ?? null,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records property changes on an article.
*/
export async function recordPropertyChangeHistory(runner, {
articleId,
companyuserId,
oldProps,
newProps,
}) {
const fmtProp = (p) => `${p.name}=${p.value}`;
const oldStr = (oldProps || []).map(fmtProp).join(', ');
const newStr = (newProps || []).map(fmtProp).join(', ');
if (oldStr === newStr) return;
const text = `Eigenschaften geändert von [${oldStr}] auf [${newStr}]`;
const meta = {
type: 'property_change',
oldProps,
newProps,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records property deletion from an article.
*/
export async function recordPropertyDeleteHistory(runner, {
articleId,
companyuserId,
name,
value,
}) {
const text = `Eigenschaft "${name}" (${value}) gelöscht`;
const meta = {
type: 'property_delete',
name,
value,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records attribute value changes on an article.
*/
export async function recordAttributeChangeHistory(runner, {
articleId,
companyuserId,
oldAttrs,
newAttrs,
}) {
const fmtAttr = (a) => `${a.attributeName}: ${a.value}`;
const oldStr = (oldAttrs || []).map(fmtAttr).join(', ');
const newStr = (newAttrs || []).map(fmtAttr).join(', ');
if (oldStr === newStr) return;
const text = `Attributwerte geändert von [${oldStr}] auf [${newStr}]`;
const meta = {
type: 'attribute_change',
oldAttrs,
newAttrs,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records Stückliste (component items) assignment on an article.
*/
export async function recordArticleListChangeHistory(runner, {
articleId,
companyuserId,
items,
}) {
const newNames = (items || []).map((r) => `${r.count}x ${r.name || r.sku}`);
const text = `Stückliste gesetzt: [${newNames.join(', ')}]`;
const meta = {
type: 'articlelist_change',
items,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}
/**
* Records Optionsliste assignment on an article.
*/
export async function recordOptionsListChangeHistory(runner, {
articleId,
companyuserId,
options,
}) {
const newNames = (options || []).map((r) => r.name || r.sku);
const text = `Optionsliste gesetzt: [${newNames.join(', ')}]`;
const meta = {
type: 'optionslist_change',
options,
};
return recordArticleHistory(runner, {
articleId,
companyuserId,
text,
meta,
});
}

View File

@@ -1,9 +1,11 @@
import * as mssqlQueries from '../../providers/mssql/queries/articlelist.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articlelist.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import getItemsByArticleSku from './getItemsByArticleSku.js';
import {
recordArticleListChangeHistory,
recordArticleHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
async function hasArticlelistCycle(runner, rootArticleId, candidateComponentId) {
@@ -44,7 +46,6 @@ export default function setItemsByArticleSku(provider) {
const getItems = getItemsByArticleSku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Sets or clears the Stückliste (bundle items) for an article.
@@ -100,10 +101,11 @@ export default function setItemsByArticleSku(provider) {
});
if (oldItemNames.length) {
await runner.query(insertHistorySql, {
await recordArticleHistory(runner, {
articleId,
companyuserId,
text: `Stückliste geleert (war: [${oldItemNames.join(', ')}])`,
type: 'articlelist_cleared',
});
}
}
@@ -221,11 +223,10 @@ export default function setItemsByArticleSku(provider) {
const result = await getItems(googleAuthId, sku, runner);
// Write history for Stückliste change
const newNames = result.map((r) => `${r.count}x ${r.name || r.sku}`);
await runner.query(insertHistorySql, {
await recordArticleListChangeHistory(runner, {
articleId,
companyuserId,
text: `Stückliste gesetzt: [${newNames.join(', ')}]`,
items: result,
});
return result;

View File

@@ -4,16 +4,14 @@ import {
import {
DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE,
} from '../../providers/sqlite3/queries/articleproperty.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import { recordPropertyDeleteHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function deleteArticleProperty(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Deletes a property from an article by SKU and propertyId.
@@ -41,10 +39,11 @@ export default function deleteArticleProperty(provider) {
await runner.query(deleteSql, { articleId, propertyId });
if (prop) {
await runner.query(insertHistorySql, {
await recordPropertyDeleteHistory(runner, {
articleId,
companyuserId,
text: `Eigenschaft "${prop.name}" (${prop.value}) gelöscht`,
name: prop.name,
value: prop.value,
});
}
};

View File

@@ -12,9 +12,8 @@ import {
import {
GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP,
} from '../../providers/sqlite3/queries/propertyname.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import { recordPropertyChangeHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function setByArticleSku(provider) {
@@ -23,7 +22,6 @@ export default function setByArticleSku(provider) {
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
const getPropSql = isSqlite ? SQLITE_GET_PROP : MSSQL_GET_PROP;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Replaces all properties for an article by its SKU.
@@ -119,16 +117,12 @@ export default function setByArticleSku(provider) {
}
// Write history if properties changed
const fmtProp = (p) => `${p.name}=${p.value}`;
const oldStr = oldProps.map(fmtProp).join(', ');
const newStr = validated.map(fmtProp).join(', ');
if (oldStr !== newStr) {
await runner.query(insertHistorySql, {
await recordPropertyChangeHistory(runner, {
articleId,
companyuserId,
text: `Eigenschaften geändert von [${oldStr}] auf [${newStr}]`,
oldProps,
newProps: validated,
});
}
return inserted;
};

View File

@@ -1,7 +1,5 @@
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import taxEnsure from '../tax/ensure.js';
import {
@@ -10,6 +8,7 @@ import {
resolveCompanyDefaultTax,
shapeArticleSupplier,
} from './helpers.js';
import { recordSupplierAddHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function create(provider) {
@@ -18,7 +17,6 @@ export default function create(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Assigns a supplier to an article, optionally creating a purchase price.
@@ -150,21 +148,15 @@ export default function create(provider) {
const supplierName =
supplierRow?.supplierName ?? (entityId ? `Lieferant #${entityId}` : 'Lieferant');
const details = [];
const actualSku = supplierRow?.sku ?? supplierSku;
if (actualSku) {
details.push(`Artikelnr: "${actualSku}"`);
}
const actualPrice = supplierRow?.priceNet ?? netPrice;
if (actualPrice !== null && actualPrice !== undefined) {
details.push(`EK: ${actualPrice}`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
await runner.query(insertHistorySql, {
await recordSupplierAddHistory(runner, {
articleId,
companyuserId,
text: `Lieferant "${supplierName}" hinzugefügt${suffix}`,
supplierName,
supplierSku: actualSku,
netPrice: actualPrice,
});
return shapeArticleSupplier(supplierRow);

View File

@@ -1,15 +1,13 @@
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import { recordSupplierDeleteHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function del(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Deletes a supplier assignment and cleans up its associated purchase price.
@@ -55,16 +53,11 @@ export default function del(provider) {
const supplierName =
existing.supplierName ?? (existing.entityId ? `Lieferant #${existing.entityId}` : 'Lieferant');
const details = [];
if (existing.sku) {
details.push(`Artikelnr: "${existing.sku}"`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
await runner.query(insertHistorySql, {
await recordSupplierDeleteHistory(runner, {
articleId,
companyuserId,
text: `Lieferant "${supplierName}" entfernt${suffix}`,
supplierName,
supplierSku: existing.sku,
});
};

View File

@@ -1,7 +1,5 @@
import * as mssqlQueries from '../../providers/mssql/queries/articlesupplier.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articlesupplier.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import taxEnsure from '../tax/ensure.js';
import {
@@ -9,6 +7,7 @@ import {
resolveCompanyDefaultTax,
shapeArticleSupplier,
} from './helpers.js';
import { recordSupplierUpdateHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function update(provider) {
@@ -17,7 +16,6 @@ export default function update(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Updates an existing supplier assignment and its associated purchase price.
@@ -213,13 +211,12 @@ export default function update(provider) {
}
}
if (historyLines.length) {
await runner.query(insertHistorySql, {
await recordSupplierUpdateHistory(runner, {
articleId,
companyuserId,
text: historyLines.join('\n'),
historyLines,
supplierName,
});
}
const loadRes = await runner.query(queries.GET_BY_ID_AND_ARTICLE_ID, {
id,

View File

@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
import * as mssqlQueries from '../../providers/mssql/queries/ledger.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/ledger.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import { recordDeliveryHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
@@ -205,42 +206,20 @@ export default function add(provider) {
!upperType.includes('LOSS') &&
!upperType.includes('WRITE_OFF'));
let historyText = transactionText;
if (isDelivery) {
const details = [];
if (manifestArticle.supplierName) {
details.push(`Lieferant: "${manifestArticle.supplierName}"`);
}
if (warehouseName) {
details.push(`Lager: "${warehouseName}"`);
}
if (manifestArticle.net != null && Number.isFinite(Number(manifestArticle.net))) {
const net = Number(manifestArticle.net);
const rebate = Number(manifestArticle.rebate ?? 0);
const taxrate = Number(manifestArticle.taxrate ?? 0);
const netAfterRebate = net * (1 - rebate / 100);
const brutto = netAfterRebate * (1 + taxrate / 100);
const ekBrutto = (Math.round(brutto * 100) / 100).toFixed(2);
details.push(`EK brutto: ${ekBrutto}`);
}
if (amount != null && Number.isFinite(Number(amount))) {
details.push(`Menge: ${amount}`);
}
const suffix = details.length ? ` (${details.join(', ')})` : '';
const base =
transactionText?.trim() ||
(manifestArticle.manifestNumber ? `Lieferung ${manifestArticle.manifestNumber}` : 'Lieferung');
historyText = `${base}${suffix}`;
} else if (warehouseName && !transactionText.includes(warehouseName)) {
historyText = `${transactionText} (Lager: "${warehouseName}")`;
}
if (!isShipping) {
for (const match of articleMatches.rows) {
await runner.query(queries.INSERT_ARTICLE_HISTORY, {
await recordDeliveryHistory(runner, {
articleId: match.articleId,
companyuserId,
text: historyText,
manifestNumber: manifestArticle.manifestNumber,
supplierName: manifestArticle.supplierName,
warehouseName,
net: manifestArticle.net,
rebate: manifestArticle.rebate,
taxrate: manifestArticle.taxrate,
amount,
transactionText,
isDelivery,
});
}
}

View File

@@ -1,9 +1,11 @@
import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import getOptionsByArticleSku from './getOptionsByArticleSku.js';
import {
recordOptionsListChangeHistory,
recordArticleHistory,
} from '../articlehistory/record.js';
import makeRunner from '../runner.js';
async function hasOptionslistCycle(runner, rootArticleId, candidateOptionId) {
@@ -44,7 +46,6 @@ export default function setOptionsByArticleSku(provider) {
const getOptions = getOptionsByArticleSku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Sets or clears the options list for an article.
@@ -100,10 +101,11 @@ export default function setOptionsByArticleSku(provider) {
});
if (oldOptNames.length) {
await runner.query(insertHistorySql, {
await recordArticleHistory(runner, {
articleId,
companyuserId,
text: `Optionsliste geleert (war: [${oldOptNames.join(', ')}])`,
type: 'optionslist_cleared',
});
}
}
@@ -228,11 +230,10 @@ export default function setOptionsByArticleSku(provider) {
const result = await getOptions(googleAuthId, sku, runner);
// Write history for options list change
const newNames = result.map((r) => r.name || r.sku);
await runner.query(insertHistorySql, {
await recordOptionsListChangeHistory(runner, {
articleId,
companyuserId,
text: `Optionsliste gesetzt: [${newNames.join(', ')}]`,
options: result,
});
return result;

View File

@@ -8,16 +8,15 @@ import {
} 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';
import { assertCustomerEntity, loadDoc } from '../manifestDoc/helpers.js';
import { recordOrderHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js';
export default function create(provider) {
@@ -28,7 +27,6 @@ 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 and records article history.
@@ -98,6 +96,8 @@ export default function create(provider) {
customerName = entRes.rows[0]?.name ?? null;
}
const historyDate = body.createdAt ? createdAt : date;
for (const line of lines) {
const sku = line.sku ? String(line.sku).trim() : '';
if (!sku) continue;
@@ -107,21 +107,18 @@ export default function create(provider) {
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, {
await recordOrderHistory(runner, {
articleId: match.articleId,
companyuserId,
text: historyText,
date: historyDate,
orderId: manifestId,
number,
customerName,
amount: line.amount,
net: line.net,
rebate: line.rebate,
taxrate: line.taxrate,
});
}
}

View File

@@ -1,12 +1,4 @@
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';
@@ -27,9 +19,6 @@ 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

View File

@@ -1,12 +1,4 @@
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';
@@ -32,9 +24,6 @@ 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.

View File

@@ -1,9 +1,8 @@
import * as mssqlQueries from '../../providers/mssql/queries/manifestDoc.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/manifestDoc.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import { recordStockAdjustmentHistory } from '../articlehistory/record.js';
import nextNumber from '../numbers/nextNumber.js';
import ledgerAdd from '../ledger/add.js';
import makeRunner from '../runner.js';
@@ -25,7 +24,6 @@ export default function adjust(provider) {
const addLedger = ledgerAdd(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Adjusts article stock manually by creating an implicit manifest of type "adjustment",
@@ -200,11 +198,11 @@ export default function adjust(provider) {
// Write article history for the adjusted article
if (item.articleId) {
const sign = item.amount > 0 ? '+' : '';
await runner.query(insertHistorySql, {
await recordStockAdjustmentHistory(runner, {
articleId: item.articleId,
companyuserId,
text: `Bestandskorrektur ${sign}${item.amount} (${item.text})`,
amount: item.amount,
reason: item.text,
});
}
}