From 0cafa11a32a554f756bfd73d0ec3f73330b7883b Mon Sep 17 00:00:00 2001 From: seb Date: Sun, 27 Sep 2026 21:01:30 +0200 Subject: [PATCH] u --- API.article.md | 41 +- API.brand.md | 19 + API.companysetting.md | 2 +- API.md | 21 +- API.warehouse.md | 51 +- README.md | 13 +- generate-api-doc.js | 4 +- schema-graph.dot | 32 +- schema-graph.svg | 2846 +++++++++--------- scripts/test-api.js | 26 + scripts/test-ledger.js | 39 +- scripts/test-sqlite-storage.js | 31 + src/index.js | 4 + src/providers/mssql/queries/article.js | 16 +- src/providers/mssql/queries/articleDetail.js | 1 + src/providers/mssql/queries/warehouse.js | 37 +- src/providers/mssql/schema.js | 10 + src/providers/sqlite3/index.js | 14 + src/providers/sqlite3/queries/article.js | 9 +- src/providers/sqlite3/queries/warehouse.js | 37 +- src/providers/sqlite3/schema.js | 2 + src/store/article/createUnderParent.js | 12 +- src/store/article/getBySku.js | 4 +- src/store/article/importRows.js | 5 + src/store/article/listExport.js | 1 + src/store/article/queryBySku.js | 1 + src/store/article/setTrackStockBySku.js | 53 + src/store/article/updateBySku.js | 31 +- src/store/category/queryCategoryArticles.js | 1 + src/store/warehouse/create.js | 42 +- src/store/warehouse/getById.js | 6 +- src/store/warehouse/list.js | 6 +- src/store/warehouse/order.js | 76 + src/store/warehouse/update.js | 51 +- 34 files changed, 2010 insertions(+), 1534 deletions(-) create mode 100644 src/store/article/setTrackStockBySku.js create mode 100644 src/store/warehouse/order.js diff --git a/API.article.md b/API.article.md index d54e12c..031b5c9 100644 --- a/API.article.md +++ b/API.article.md @@ -42,17 +42,18 @@ Creates a new article under a given parent category, allocating a SKU and a defa | `input.googleAuthId` | `number` | The id of the Google auth user. | | `input.parentId` | `number` | The id of the parent category. | | `input.name` | `string` | The article name. | +| `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{id: number, parentId: number, name: string, sku: string}>` — The created article. +`Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` — The created article. **Example** ```js const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); -// => { id, parentId, name, sku } +// => { id, parentId, name, sku, track_stock } ``` ## `article.delete` @@ -128,7 +129,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor **Returns** -`Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` — The full article detail. +`Promise<{article: {id: number, sku: string, name: string, slug: string, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` — The full article detail. **Throws** @@ -188,6 +189,7 @@ Imports article rows, creating articles by their SKU and returning per-row impor | `[input.rows[].net]` | `number|string` | The POS net price (accepts German decimals). | | `[input.rows[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). | | `[input.rows[].eans]` | `Array|string` | The EANs to set. | +| `[input.rows[].track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. | | `[input.rows[].fatherSku]` | `string` | The father article SKU for a V/K compound link. | | `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. | | `[tx]` | `object` | Optional transaction. | @@ -443,6 +445,36 @@ const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, t // => { id, sku, name, ... } ``` +## `article.setTrackStockBySku` + +*Source: `src/store/article/setTrackStockBySku.js`* + +Sets the track_stock flag of an article by its SKU. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `sku` | `string` | The article SKU. | +| `trackStock` | `boolean|number` | Whether stock tracking is enabled. | +| `[tx]` | `object` | Optional transaction. | + +**Returns** + +`Promise` — The updated article. + +**Throws** + +`Error` — If trackStock is invalid or the article is not found. + +**Example** + +```js +const article = await store.article.setTrackStockBySku(42, 'SKU-1', true); +// => { id, sku, name, track_stock, ... } +``` + ## `article.setWeightBySku` *Source: `src/store/article/setWeightBySku.js`* @@ -489,6 +521,7 @@ Updates an article's name and/or unit (SI and amount), recording history. | `[patch.name]` | `string` | The new article name. | | `[patch.unitSi]` | `string` | The new unit SI code. | | `[patch.unitamount]` | `number` | The new unit amount (must be > 0). | +| `[patch.track_stock]` | `boolean|number` | Whether inventory stock is tracked. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** @@ -502,6 +535,6 @@ Updates an article's name and/or unit (SI and amount), recording history. **Example** ```js -const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); +const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1, track_stock: true }); // => { id, sku, name, ... } ``` diff --git a/API.brand.md b/API.brand.md index 019125f..50d1dce 100644 --- a/API.brand.md +++ b/API.brand.md @@ -2,6 +2,25 @@ This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. +## `brand.addImage` + +*Source: `src/store/brand/addImage.js`* + +Adds an image to a brand, processing it into AVIF full and thumb variants. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `brandId` | `number` | The id of the brand. | +| `image` | `Buffer` | The source image buffer. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>` + ## `brand.delete` *Source: `src/store/brand/delete.js`* diff --git a/API.companysetting.md b/API.companysetting.md index 371ec29..fa61dea 100644 --- a/API.companysetting.md +++ b/API.companysetting.md @@ -34,7 +34,7 @@ const setting = await store.companysetting.get(42, 'ui.theme'); *Source: `src/store/companysetting/list.js`* -Lists the settings of the user's selected company, optionally only frontend-visible ones. Write-only settings (such as `keys.tavily`, `keys.openrouter`, `keys.serper`) have their value masked with `'***'` by default for usergui presentation. +Lists the settings of the user's selected company, optionally only frontend-visible ones. Write-only settings (such as `keys.tavily`, `keys.openrouter`) have their value masked with `'***'` by default for usergui presentation. **Parameters** diff --git a/API.md b/API.md index 1cd7674..01e3295 100644 --- a/API.md +++ b/API.md @@ -8,7 +8,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met > **Convention**: `PK` primary key · `?` nullable · `->tbl` foreign key · `UQ` unique · `s(N)` string · `txt` text · `i` int · `bi` bigint · `f` float · `d` decimal · `b` bit · `dt` datetime · `bin` binary · `=val` default -- **article**: `id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)` +- **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_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)` - **articleean**: `id PK, article_id->article, ean:s(50) UQ` - **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt` - **articlelist**: `id PK` @@ -49,7 +49,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - **tax**: `id PK, rate:d(18,4) UQ` - **unit**: `id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)` - **unittext**: `id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)` -- **warehouse**: `id PK, company_id->company, name:s(255), UQ(company_id, name)` +- **warehouse**: `id PK, company_id->company, name:s(255), sort:i=0, UQ(company_id, name)` ## Table of Contents @@ -77,10 +77,10 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>` - [article](API.article.md) - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` - - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string}>` + - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `delete(googleAuthId: number, sku: string, tx?: object): Promise` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise` - - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` + - `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>` - `getHistoryBySku(googleAuthId: number, sku: string, tx?: object): Promise>` - `importRows(input: {googleAuthId: number, rows: Array}, tx?: object): Promise<{imported: Array, skipped: Array, errors: Array}>` - `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise>` @@ -91,8 +91,9 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `setEansBySku(googleAuthId: number, sku: string, eans: Array, tx?: object): Promise` - `setFatherBySku(googleAuthId: number, sku: string, fatherSku: string|null, tx?: object): Promise` - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise` + - `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise` - `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise` - - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number}, tx?: object): Promise` + - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise` - [lang](API.lang.md) - `create(iso639: string, tx?: object): Promise` - `delete(id: number): Promise` @@ -113,6 +114,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise>` - `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>` - [brand](API.brand.md) + - `addImage(googleAuthId: number, brandId: number, image: Buffer, tx?: object): Promise<{id: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string, pics: Array}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `list(googleAuthId: number, tx?: object): Promise}>>` - `upsert(googleAuthId: number, input: {name: string, gsprpinfo?: string}, tx?: object): Promise<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>` @@ -176,11 +178,12 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise>` - `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise>` - [warehouse](API.warehouse.md) - - `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string}>` + - `create(googleAuthId: number, input: {name: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` - - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string}|undefined>` - - `list(googleAuthId: number, tx?: object): Promise>` - - `update(googleAuthId: number, id: number, input: {name: string}, tx?: object): Promise<{id: number, name: string}>` + - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string, sort: number}|undefined>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `order(googleAuthId: number, input: {warehouseIds: Array}, tx?: object): Promise<{warehouseIds: Array}>` + - `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>` - [stock](API.stock.md) - `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array, ledger: Array}>` - [posterminal](API.posterminal.md) diff --git a/API.warehouse.md b/API.warehouse.md index 3cc7cc3..1b1d9a9 100644 --- a/API.warehouse.md +++ b/API.warehouse.md @@ -15,21 +15,22 @@ Creates a new warehouse for the user's selected company. | `googleAuthId` | `number` | The id of the Google auth user. | | `input` | `object` | Warehouse input. | | `input.name` | `string` | The warehouse name. | +| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{id: number, name: string}>` — The created warehouse. +`Promise<{id: number, name: string, sort: number}>` — The created warehouse. **Throws** -`Error` — If name is missing or already exists. +`Error` — If name is missing, sort is invalid, or name already exists. **Example** ```js const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); -// => { id: 1, name: 'Hauptlager' } +// => { id: 1, name: 'Hauptlager', sort: 0 } ``` ## `warehouse.delete` @@ -76,13 +77,13 @@ Retrieves a warehouse by its ID for the user's selected company. **Returns** -`Promise<{id: number, name: string}|undefined>` — The warehouse or undefined. +`Promise<{id: number, name: string, sort: number}|undefined>` — The warehouse or undefined. **Example** ```js const wh = await store.warehouse.getById(googleAuthId, 1); -// => { id: 1, name: 'Hauptlager' } +// => { id: 1, name: 'Hauptlager', sort: 0 } ``` ## `warehouse.list` @@ -100,20 +101,43 @@ Lists all warehouses for the user's selected company. **Returns** -`Promise>` — List of warehouses. +`Promise>` — List of warehouses. **Example** ```js const warehouses = await store.warehouse.list(googleAuthId); -// => [{ id: 1, name: 'Hauptlager' }] +// => [{ id: 1, name: 'Hauptlager', sort: 0 }] ``` +## `warehouse.order` + +*Source: `src/store/warehouse/order.js`* + +Reorders the warehouses of the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | The reorder input. | +| `input.warehouseIds` | `Array` | The warehouse ids in their new order. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{warehouseIds: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + ## `warehouse.update` *Source: `src/store/warehouse/update.js`* -Updates a warehouse name for the user's selected company. +Updates a warehouse name and/or sort order for the user's selected company. **Parameters** @@ -122,20 +146,21 @@ Updates a warehouse name for the user's selected company. | `googleAuthId` | `number` | The id of the Google auth user. | | `id` | `number` | The warehouse ID to update. | | `input` | `object` | Update payload. | -| `input.name` | `string` | The new warehouse name. | +| `[input.name]` | `string` | The new warehouse name. | +| `[input.sort]` | `number` | The new sort order. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{id: number, name: string}>` — The updated warehouse. +`Promise<{id: number, name: string, sort: number}>` — The updated warehouse. **Throws** -`Error` — If warehouse not found, name is missing, or name already exists. +`Error` — If warehouse not found, name is empty, sort is invalid, or name already exists. **Example** ```js -const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); -// => { id: 1, name: 'Zentrallager' } +const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager', sort: 1 }); +// => { id: 1, name: 'Zentrallager', sort: 1 } ``` diff --git a/README.md b/README.md index 78eb39a..3dd9501 100644 --- a/README.md +++ b/README.md @@ -543,14 +543,15 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c | --- | --- | --- | | `listSearchIndex` | `(googleAuthId: string, companyId: number)` | `SearchIndexEntry[]` (see below) | | `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (see below) | -| `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string })` | `{ id: number, parentId: number, name: string, sku: string }` | +| `createUnderParent` | `({ googleAuthId: string, parentId: number, name: string, track_stock?: boolean })` | `{ id: number, parentId: number, name: string, sku: string, track_stock: boolean }` | | `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) | | `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, companyuserId: number \| null, date: Date, text: string }[]` | -| `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string })` | `ArticleDetail` | +| `updateBySku` | `(googleAuthId: string, sku: string, { name?: string, unitamount?: number, unitSi?: string, track_stock?: boolean })` | `ArticleDetail` | | `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` | | `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` | | `setBrandIdBySku` | `(googleAuthId: string, sku: string, number \| null)` | `ArticleDetail` | | `setWeightBySku` | `(googleAuthId: string, sku: string, number)` | `ArticleDetail` | +| `setTrackStockBySku` | `(googleAuthId: string, sku: string, boolean)` | `ArticleDetail` | | `setCategoryIdsBySku` | `(googleAuthId: string, sku: string, number[])` | `ArticleDetail` | | `addImage` | `(googleAuthId: string, sku: string, Buffer)` | `ImageResult` (see below) | | `deleteImage` | `(googleAuthId: string, sku: string, fullHash: string)` | `void` | @@ -605,7 +606,7 @@ type ArticleExportEntry = { } ``` -`getBySku` / `updateBySku` / `setPricesBySku` / `setEansBySku` / `setBrandIdBySku` / `setWeightBySku` / `setCategoryIdsBySku` all return `ArticleDetail`: +`getBySku` / `updateBySku` / `setPricesBySku` / `setEansBySku` / `setBrandIdBySku` / `setWeightBySku` / `setTrackStockBySku` / `setCategoryIdsBySku` all return `ArticleDetail`: ```ts type ArticleDetail = { @@ -615,6 +616,7 @@ type ArticleDetail = { slug: string | null, unitamount: number, weight: number, + track_stock: boolean, unit: { si: string, // e.g. "pc" basesi: string, // e.g. "pc" @@ -671,6 +673,8 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err `setWeightBySku` sets the article’s `weight`. Must be finite and `>= 0` (else `Invalid weight`). Errors: `Article not found`, `Not authorized`, `Invalid weight`. +`setTrackStockBySku` sets the article’s `track_stock` flag. Must be boolean or 0/1 (else `Invalid track_stock`). Errors: `Article not found`, `Not authorized`, `Invalid track_stock`. + `setCategoryIdsBySku` replaces the article’s category list (`categoryarticle`) in one transaction. Each category must belong to the article’s company and ids must be unique (else `Invalid categoryIds`); empty array is rejected — an article must have at least one category (else `Article must have at least one category`). Returns `ArticleDetail` (whose `categoryIds` reflects the new list). Errors: `Article not found`, `Not authorized`, `Invalid categoryIds`, `Article must have at least one category`. `deleteImage` removes `articlepic` and the `pic` row for this article (`fullHash`). Errors: `Article not found`, `Image not found`, `Not authorized`. @@ -801,11 +805,12 @@ Company-scoped number sequences (`numbers` table). Auth via selected company (`g | `GET /api/article/:sku/history` | `article.getHistoryBySku(googleAuthId, sku)` | | `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId)` | | `GET /api/article/export.zip` | `article.listExport(googleAuthId, categoryId?)` | -| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi? })` | +| `PATCH /api/article/:sku` | `article.updateBySku(googleAuthId, sku, { name?, unitamount?, unitSi?, track_stock? })` | | `PUT /api/article/:sku/prices` | `article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])` | | `PUT /api/article/:sku/eans` | `article.setEansBySku(googleAuthId, sku, string[])` | | `PUT /api/article/:sku/brand` | `article.setBrandIdBySku(googleAuthId, sku, brandId \| null)` | | `PUT /api/article/:sku/weight` | `article.setWeightBySku(googleAuthId, sku, weight)` | +| `PUT /api/article/:sku/track_stock` | `article.setTrackStockBySku(googleAuthId, sku, track_stock)` | | `PUT /api/article/:sku/categories` | `article.setCategoryIdsBySku(googleAuthId, sku, number[])` | | `DELETE /api/article/:sku` | `article.delete(googleAuthId, sku)` | | `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` | diff --git a/generate-api-doc.js b/generate-api-doc.js index 9bc25bc..9fc94ea 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -313,7 +313,7 @@ function render() { } const SCHEMA_TABLES = [ - ['article', 'id PK, sku?:s(100) UQ, unit_id->unit, unitamount:f, weight:f, brand_id?->brand, articlelist_id?->articlelist, optionslist_id?->optionslist, is_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)'], + ['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_father:b=0, fatherarticle_id?->article, fathersort:i=0, created_at:dt, updated_at:dt, UQ(fatherarticle_id, fathersort)'], ['articleean', 'id PK, article_id->article, ean:s(50) UQ'], ['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'], ['articlelist', 'id PK'], @@ -354,7 +354,7 @@ const SCHEMA_TABLES = [ ['tax', 'id PK, rate:d(18,4) UQ'], ['unit', 'id PK, company_id->company, si:s(50), basesi:s(50), basesifactor:f, UQ(company_id, si)'], ['unittext', 'id PK, unit_id->unit, language_id->language, symbol:s(50), name:s(255), UQ(unit_id, language_id)'], - ['warehouse', 'id PK, company_id->company, name:s(255), UQ(company_id, name)'], + ['warehouse', 'id PK, company_id->company, name:s(255), sort:i=0, UQ(company_id, name)'], ]; // Write the index API.md linking to each section file. diff --git a/schema-graph.dot b/schema-graph.dot index cf04835..62f85b1 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -42,7 +42,7 @@ digraph schema { optionslistarticle [label=<
optionslistarticle
idINT PK
optionslist_idINT FK
article_idINT FK
countINT
sortINT
mandatedBIT
>]; articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - article [label=<
article
idINT PK
company_idINT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
fatherarticle_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + article [label=<
article
idINT PK
company_idINT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
fatherarticle_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
track_stockBIT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
article_idINT FK
price_id?INT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; categoryarticle [label=<
categoryarticle
idINT PK
article_idINT FK
category_idINT FK
>]; @@ -65,7 +65,7 @@ digraph schema { manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateFLOAT
>]; manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; manifestattachment [label=<
manifestattachment
idINT PK
manifest_idINT FK
file_idINT FK
sortINT
>]; - warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
>]; + warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
sortINT
>]; } subgraph cluster_commerce { label="Pricing"; labeljust="l"; fontsize=15; @@ -94,7 +94,7 @@ digraph schema { google_auth:selected_company:w -> company:in9:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; category:parent_id:e -> category:id:e [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_category_parent", constraint=false]; company:root_category_id:e -> category:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; - company:default_language_id:w -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; + company:default_language_id:w -> language:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; company:taxrate:w -> tax:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; company:reducedtaxrate:w -> tax:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; company:taxfreerate:w -> tax:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; @@ -106,8 +106,8 @@ digraph schema { entityhistory:entity_id:sw -> entity:in3:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; entityhistory:companyuser_id:se -> companyuser:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; unit:company_id:w -> company:in12:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; - unittext:unit_id:w -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; - unittext:language_id:e -> language:in3:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; + unittext:unit_id:e -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; + unittext:language_id:w -> language:in3:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; brand:company_id:sw -> company:in10:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; article:company_id:w -> company:in11:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; article:unit_id:e -> unit:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; @@ -115,29 +115,29 @@ digraph schema { article:articlelist_id:w -> articlelist:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_articlelist", constraint=false, weight=1]; article:optionslist_id:w -> optionslist:in1:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; article:fatherarticle_id:e -> article:id:e [color="#c79bff", fontcolor="#c79bff", tooltip="FK_article_fatherarticle", constraint=false]; - articlehistory:article_id:se -> article:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlehistory_article", weight=3]; - articlehistory:companyuser_id:w -> companyuser:in1:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; + articlehistory:article_id:se -> article:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlehistory_article", weight=3]; + articlehistory:companyuser_id:e -> companyuser:in1:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; articleean:article_id:se -> article:in3:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articleean_article", weight=3]; articlepic:article_id:sw -> article:in7:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articlepic_article", weight=3]; articlepic:pic_id:e -> pic:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlepic_pic", constraint=false, weight=1]; brandpic:brand_id:e -> brand:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_brandpic_brand", weight=3]; brandpic:pic_id:w -> pic:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_brandpic_pic", constraint=false, weight=1]; - articlelistarticle:articlelist_id:sw -> articlelist:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlelistarticle_articlelist", weight=3]; - articlelistarticle:article_id:w -> article:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + articlelistarticle:articlelist_id:ne -> articlelist:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlelistarticle_articlelist", weight=3]; + articlelistarticle:article_id:w -> article:in1:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; optionslistarticle:optionslist_id:se -> optionslist:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_optionslistarticle_optionslist", weight=3]; - optionslistarticle:article_id:w -> article:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + optionslistarticle:article_id:w -> article:in2:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; categoryarticle:category_id:nw -> category:in3:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_categoryarticle_category", weight=3]; categoryarticle:article_id:sw -> article:in6:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; categorypic:category_id:sw -> category:in4:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categorypic_category", weight=3]; categorypic:pic_id:w -> pic:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categorypic_pic", constraint=false, weight=1]; - categorytext:category_id:e -> category:in5:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_category", weight=3]; - categorytext:language_id:w -> language:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorytext_language", constraint=false, weight=1]; + categorytext:category_id:w -> category:in5:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_category", weight=3]; + categorytext:language_id:e -> language:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorytext_language", constraint=false, weight=1]; articletext:article_id:w -> article:in9:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_articletext_article", weight=3]; - articletext:language_id:e -> language:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_articletext_language", constraint=false, weight=1]; + articletext:language_id:w -> language:in0:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_articletext_language", constraint=false, weight=1]; price:tax_id:w -> tax:in3:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_tax", constraint=false, weight=1]; price:article_id:e -> article:in8:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_article", weight=3]; price:customergroup_id:w -> customergroup:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:ne -> article:in5:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_article", weight=3]; + articlesupplier:article_id:nw -> article:in5:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_article", weight=3]; articlesupplier:entity_id:w -> entity:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; articlesupplier:price_id:e -> price:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; manifest:company_id:e -> company:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_company", weight=3]; @@ -148,11 +148,11 @@ digraph schema { ledger:warehouse_id:e -> warehouse:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_ledger_warehouse", constraint=false, weight=1]; numbers:company_id:se -> company:in5:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_numbers_company", weight=3]; companyuser:google_auth_id:e -> google_auth:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; - companyuser:company_id:se -> company:in6:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_companyuser_company", weight=3]; + companyuser:company_id:ne -> company:in6:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_companyuser_company", weight=3]; syncpos:company_id:nw -> company:in7:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_syncpos_company", weight=3]; companysetting:company_id:sw -> company:in8:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_companysetting_company", weight=3]; posterminal:company_id:e -> company:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_posterminal_company", weight=3]; - posterminalcategory:posterminal_id:ne -> posterminal:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_posterminalcategory_posterminal", weight=3]; + posterminalcategory:posterminal_id:sw -> posterminal:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_posterminalcategory_posterminal", weight=3]; posterminalcategory:category_id:e -> category:in1:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; printer:company_id:se -> company:in3:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_printer_company", weight=3]; manifestattachment:manifest_id:sw -> manifest:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_manifestattachment_manifest", weight=3]; diff --git a/schema-graph.svg b/schema-graph.svg index af841d5..581f796 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,311 +4,311 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_manifests - -Documents + +Documents cluster_commerce - -Pricing + +Pricing cluster_units - -Units + +Units cluster_pos - -POS + +POS meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) - - -sort -INT + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) + + +sort +INT company - - - - - - - - - - - - - - - -company - - -id -INT -PK - - -poscustomergroup_id -INT -FK - - -root_category_id -? -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -default_language_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + + + + + +company + + +id +INT +PK + + +poscustomergroup_id +INT +FK + + +root_category_id +? +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +default_language_id +? +INT +FK + + +name +NVARCHAR(255) customergroup:w->company:n - - + + numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT numbers:se->company:n - - + + entityaddress - - -entityaddress - - -id -INT -PK - - -entity_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name? -NVARCHAR(255) - - -street? -NVARCHAR(255) - - -streetnumber? -NVARCHAR(50) - - -zip? -NVARCHAR(50) - - -city? -NVARCHAR(255) - - -county? -NVARCHAR(255) - - -addresstype -NVARCHAR(50) - - -sort -INT + + +entityaddress + + +id +INT +PK + + +entity_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name? +NVARCHAR(255) + + +street? +NVARCHAR(255) + + +streetnumber? +NVARCHAR(50) + + +zip? +NVARCHAR(50) + + +city? +NVARCHAR(255) + + +county? +NVARCHAR(255) + + +addresstype +NVARCHAR(50) + + +sort +INT entity - - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -number? -NVARCHAR(100) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +number? +NVARCHAR(100) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 entityaddress:ne->entity:n - - + + @@ -316,127 +316,127 @@ entity:sw->customergroup:n - - + + companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK -companyuser:se->company:n +companyuser:ne->company:n - - + + google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 companyuser:e->google_auth:n - - + + entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - -sort -INT + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + +sort +INT entitycontact:sw->entity:n - - + + @@ -444,103 +444,103 @@ company:w->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) company:w->language:n - - + + category - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT company:e->category:n - - + + tax - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) company:w->tax:n - - + + @@ -548,8 +548,8 @@ company:w->tax:n - - + + @@ -557,82 +557,82 @@ company:w->tax:n - - + + syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) syncpos:nw->company:n - - + + entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) entityhistory:sw->entity:n - - + + @@ -640,50 +640,50 @@ entityhistory:se->companyuser:n - - + + companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - -writeonly -BIT + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + +writeonly +BIT companysetting:sw->company:n - - + + @@ -691,372 +691,376 @@ google_auth:w->company:n - - + + articletext - - -articletext - - -id -INT -PK - - -article_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +articletext + + +id +INT +PK + + +article_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) -articletext:e->language:n +articletext:w->language:n - - + + article - - - - - - - - - - - - -article - - -id -INT -PK - - -company_id -INT -FK - - -brand_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -unit_id -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + +article + + +id +INT +PK + + +company_id +INT +FK + + +brand_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +unit_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +track_stock +BIT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 articletext:w->article:n - - + + categorytext - - -categorytext - - -id -INT -PK - - -category_id -INT -FK - - -language_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +categorytext + + +id +INT +PK + + +category_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) -categorytext:w->language:n +categorytext:e->language:n - - + + -categorytext:e->category:n +categorytext:w->category:n - - + + unittext - - -unittext - - -id -INT -PK - - -unit_id -INT -FK - - -language_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +unit_id +INT +FK + + +language_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) -unittext:e->language:n +unittext:w->language:n - - + + unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT -unittext:w->unit:n +unittext:e->unit:n - - + + brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) brand:sw->company:n - - + + articlelist - - - - -articlelist - - -id -INT -PK + + + + +articlelist + + +id +INT +PK articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -articlelist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT -articlelistarticle:sw->articlelist:n +articlelistarticle:ne->articlelist:n - - + + @@ -1064,65 +1068,65 @@ articlelistarticle:w->article:n - - + + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -optionslist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT + + +optionslistarticle + + +id +INT +PK + + +optionslist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT optionslistarticle:se->optionslist:n - - + + @@ -1130,48 +1134,48 @@ optionslistarticle:w->article:n - - + + articlehistory - - -articlehistory - - -id -INT -PK - - -companyuser_id -? -INT -FK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +companyuser_id +? +INT +FK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) -articlehistory:w->companyuser:n +articlehistory:e->companyuser:n - - + + @@ -1179,38 +1183,38 @@ articlehistory:se->article:n - - + + articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) articleean:se->article:n - - + + @@ -1218,8 +1222,8 @@ article:w->company:n - - + + @@ -1227,8 +1231,8 @@ article:w->brand:n - - + + @@ -1236,8 +1240,8 @@ article:w->articlelist:n - - + + @@ -1245,8 +1249,8 @@ article:w->optionslist:n - - + + @@ -1254,8 +1258,8 @@ article:e->article:e - - + + @@ -1263,115 +1267,115 @@ article:e->unit:n - - + + articlesupplier - - -articlesupplier - - -id -INT -PK - - -entity_id -INT -FK - - -article_id -INT -FK - - -price_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -stock? -FLOAT - - -pu -FLOAT - - -sort -INT + + +articlesupplier + + +id +INT +PK + + +entity_id +INT +FK + + +article_id +INT +FK + + +price_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + +sort +INT articlesupplier:w->entity:n - - + + -articlesupplier:ne->article:n +articlesupplier:nw->article:n - - + + price - - - -price - - -id -INT -PK - - -customergroup_id -INT -FK - - -article_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + + +price + + +id +INT +PK + + +customergroup_id +INT +FK + + +article_id +INT +FK + + +tax_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT articlesupplier:e->price:n - - + + @@ -1379,39 +1383,39 @@ category:e->category:e - - + + categoryarticle - - -categoryarticle - - -id -INT -PK - - -article_id -INT -FK - - -category_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK categoryarticle:sw->article:n - - + + @@ -1419,98 +1423,98 @@ categoryarticle:nw->category:n - - + + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) brandpic - - -brandpic - - -id -INT -PK - - -brand_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT brandpic:e->brand:n - - + + @@ -1518,43 +1522,43 @@ brandpic:w->pic:n - - + + articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT articlepic:sw->article:n - - + + @@ -1562,43 +1566,43 @@ articlepic:e->pic:n - - + + categorypic - - -categorypic - - -id -INT -PK - - -pic_id -INT -FK - - -category_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT categorypic:sw->category:n - - + + @@ -1606,256 +1610,260 @@ categorypic:w->pic:n - - + + file - - - -file - - -id -INT -PK - - -hash -NVARCHAR(64) - - -mimetype -NVARCHAR(255) - - -name -NVARCHAR(255) - - -size -BIGINT - - -created -DATETIME2 - - -modified -DATETIME2 - - -content -VARBINARY(MAX) - - -thumbavif? -VARBINARY(MAX) - - -thumbx? -INT - - -thumby? -INT - - -meta? -NVARCHAR(MAX) + + + +file + + +id +INT +PK + + +hash +NVARCHAR(64) + + +mimetype +NVARCHAR(255) + + +name +NVARCHAR(255) + + +size +BIGINT + + +created +DATETIME2 + + +modified +DATETIME2 + + +content +VARBINARY(MAX) + + +thumbavif? +VARBINARY(MAX) + + +thumbx? +INT + + +thumby? +INT + + +meta? +NVARCHAR(MAX) ledger - - -ledger + + +ledger + + +id +INT +PK -id -INT -PK +manifestarticle_id +INT +FK -manifestarticle_id +warehouse_id +? INT FK - + -warehouse_id -? -INT -FK - +transaction_type +NVARCHAR(100) + -transaction_type -NVARCHAR(100) - +amount +FLOAT + -amount -FLOAT - +transaction_text +NVARCHAR(MAX) + -transaction_text -NVARCHAR(MAX) - +hash_version +INT + -hash_version -INT - +prev_hash? +NVARCHAR(128) + -prev_hash? -NVARCHAR(128) - - -created_at -DATETIME2 +created_at +DATETIME2 manifestarticle - - - -manifestarticle + + + +manifestarticle + + +id +INT +PK -id -INT -PK - +manifest_id +INT +FK + -manifest_id -INT -FK - +sku? +NVARCHAR(100) + -sku? -NVARCHAR(100) - +name? +NVARCHAR(255) + -name? -NVARCHAR(255) - +pu +FLOAT + -pu +amount FLOAT - + -amount +net FLOAT - + -net -FLOAT - +comment? +NVARCHAR(MAX) + -comment? -NVARCHAR(MAX) - +rebate +FLOAT + -rebate +taxrate FLOAT - - -taxrate -FLOAT ledger:sw->manifestarticle:n - - + + warehouse - - - -warehouse - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) + + + +warehouse + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +sort +INT ledger:e->warehouse:n - - + + manifest - - - - -manifest - - -id -INT -PK - - -entity_id -? -INT -FK - - -company_id -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -external_number? -NVARCHAR(100) - - -date -DATETIME2 - - -created_at -DATETIME2 + + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +company_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +date +DATETIME2 + + +created_at +DATETIME2 manifestarticle:se->manifest:n - - + + @@ -1863,8 +1871,8 @@ manifest:e->entity:n - - + + @@ -1872,43 +1880,43 @@ manifest:e->company:n - - + + manifestattachment - - -manifestattachment + + +manifestattachment + + +id +INT +PK -id -INT -PK +manifest_id +INT +FK -manifest_id +file_id INT FK - + -file_id -INT -FK - - -sort -INT +sort +INT manifestattachment:e->file:n - - + + @@ -1916,8 +1924,8 @@ manifestattachment:sw->manifest:n - - + + @@ -1925,8 +1933,8 @@ warehouse:e->company:n - - + + @@ -1934,8 +1942,8 @@ price:w->customergroup:n - - + + @@ -1943,8 +1951,8 @@ price:e->article:n - - + + @@ -1952,8 +1960,8 @@ price:w->tax:n - - + + @@ -1961,78 +1969,78 @@ unit:w->company:n - - + + posterminalcategory - - -posterminalcategory + + +posterminalcategory + + +id +INT +PK -id -INT -PK +posterminal_id +INT +FK -posterminal_id +category_id INT FK - - -category_id -INT -FK posterminalcategory:e->category:n - - + + posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) -posterminalcategory:ne->posterminal:n +posterminalcategory:sw->posterminal:n - - + + @@ -2040,88 +2048,88 @@ posterminal:e->company:n - - + + printer - - -printer + + +printer + + +id +INT +PK -id -INT -PK - +company_id +INT +FK + -company_id -INT -FK - +address +NVARCHAR(255) + -address -NVARCHAR(255) - +type +NVARCHAR(50) + -type -NVARCHAR(50) - +model? +NVARCHAR(255) + -model? +serial? NVARCHAR(255) - + -serial? -NVARCHAR(255) - +sort +INT + -sort -INT - - -desc? -NVARCHAR(MAX) +desc? +NVARCHAR(MAX) printer:se->company:n - - + + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/scripts/test-api.js b/scripts/test-api.js index eeb6523..31865b7 100644 --- a/scripts/test-api.js +++ b/scripts/test-api.js @@ -369,6 +369,11 @@ const MENU = { label: 'setWeightBySku', hint: 'googleAuthId, sku, weight → row', }, + { + value: 'article.setTrackStockBySku', + label: 'setTrackStockBySku', + hint: 'googleAuthId, sku, trackStock (true|false) → row', + }, { value: 'article.setCategoryIdsBySku', label: 'setCategoryIdsBySku', @@ -1762,6 +1767,27 @@ async function runMethod(client, action) { ), ]; } + case 'article.setTrackStockBySku': { + const googleAuthId = await askInt('google_auth id'); + if (googleAuthId == null) return; + const sku = await askText('sku', { + validate: (v) => (!v?.trim() ? 'Required' : undefined), + }); + if (sku == null) return; + const trackStockRaw = await p.confirm({ + message: 'Track stock?', + initialValue: true, + }); + if (p.isCancel(trackStockRaw)) return; + return [ + 'result', + await client.article.setTrackStockBySku( + Number(googleAuthId), + sku, + Boolean(trackStockRaw), + ), + ]; + } case 'article.setCategoryIdsBySku': { const googleAuthId = await askInt('google_auth id'); if (googleAuthId == null) return; diff --git a/scripts/test-ledger.js b/scripts/test-ledger.js index c01c35f..f394832 100644 --- a/scripts/test-ledger.js +++ b/scripts/test-ledger.js @@ -229,25 +229,54 @@ async function run() { 'History should include second line goods receipt with details', ); - // 9. Test warehouse CRUD and linking with ledger - console.log('8. Testing warehouse CRUD and ledger association...'); + // 9. Test warehouse CRUD, sort, reorder, and linking with ledger + console.log('8. Testing warehouse CRUD, sort, reorder, and ledger association...'); const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); assert.equal(wh1.name, 'Hauptlager'); + assert.equal(wh1.sort, 0); assert(wh1.id > 0); const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' }); assert.equal(wh2.name, 'Filiale Nord'); + assert.equal(wh2.sort, 1); const whList = await store.warehouse.list(googleAuthId); assert.equal(whList.length, 2); - assert(whList.some((w) => w.name === 'Hauptlager')); - assert(whList.some((w) => w.name === 'Filiale Nord')); + assert.equal(whList[0].id, wh1.id); + assert.equal(whList[0].sort, 0); + assert.equal(whList[1].id, wh2.id); + assert.equal(whList[1].sort, 1); const whGet = await store.warehouse.getById(googleAuthId, wh1.id); assert.equal(whGet.name, 'Hauptlager'); + assert.equal(whGet.sort, 0); - const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd' }); + const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd', sort: 5 }); assert.equal(whUpdated.name, 'Filiale Süd'); + assert.equal(whUpdated.sort, 5); + + // Test warehouse order + await store.warehouse.order(googleAuthId, { warehouseIds: [wh2.id, wh1.id] }); + const whListReordered = await store.warehouse.list(googleAuthId); + assert.equal(whListReordered[0].id, wh2.id); + assert.equal(whListReordered[0].sort, 0); + assert.equal(whListReordered[1].id, wh1.id); + assert.equal(whListReordered[1].sort, 1); + + // Test invalid warehouse order + await assert.rejects( + async () => store.warehouse.order(googleAuthId, { warehouseIds: [wh1.id] }), + /Invalid warehouse order/, + ); + + // Verify article detail includes warehouses with sort + const articleDetail = await store.article.getBySku(googleAuthId, article.sku); + assert(Array.isArray(articleDetail.article.warehouses), 'Article should have warehouses array'); + assert.equal(articleDetail.article.warehouses.length, 2); + assert.equal(articleDetail.article.warehouses[0].id, wh2.id); + assert.equal(articleDetail.article.warehouses[0].sort, 0); + assert.equal(articleDetail.article.warehouses[1].id, wh1.id); + assert.equal(articleDetail.article.warehouses[1].sort, 1); // Add ledger row with warehouse const rowWithWh = await store.ledger.add(googleAuthId, { diff --git a/scripts/test-sqlite-storage.js b/scripts/test-sqlite-storage.js index f8bad7a..e9ab55b 100644 --- a/scripts/test-sqlite-storage.js +++ b/scripts/test-sqlite-storage.js @@ -154,6 +154,37 @@ async function runTests() { parentId: cat.id, name: 'Test Article', }); + assert.equal(article.track_stock, true, 'Default track_stock must be true'); + + const fetchedArt = await store.article.getBySku(googleAuthId, article.sku); + assert.equal(fetchedArt.article.track_stock, true, 'getBySku must return track_stock: true'); + + await store.article.setTrackStockBySku(googleAuthId, article.sku, false); + const afterSet = await store.article.getBySku(googleAuthId, article.sku); + assert.equal(afterSet.article.track_stock, false, 'setTrackStockBySku false must update track_stock'); + + await store.article.updateBySku(googleAuthId, article.sku, { track_stock: true }); + const afterUpdate = await store.article.getBySku(googleAuthId, article.sku); + assert.equal(afterUpdate.article.track_stock, true, 'updateBySku track_stock: true must update track_stock'); + + const history = await store.article.getHistoryBySku(googleAuthId, article.sku); + assert(history.some((h) => h.text.includes('Bestandsführung geändert von "Nein" auf "Ja"'))); + + const nonTracked = await store.article.createUnderParent({ + googleAuthId, + parentId: cat.id, + name: 'Service Item', + track_stock: false, + }); + assert.equal(nonTracked.track_stock, false, 'Explicit track_stock: false must be preserved'); + const fetchedNonTracked = await store.article.getBySku(googleAuthId, nonTracked.sku); + assert.equal(fetchedNonTracked.article.track_stock, false); + + const exported = await store.article.listExport(googleAuthId, cat.id); + const foundExport = exported.find((a) => a.sku === article.sku); + assert.equal(foundExport.track_stock, true, 'listExport must include track_stock'); + + console.log('Article track_stock tests passed!'); const sampleImage = await sharp({ create: { diff --git a/src/index.js b/src/index.js index 005c0e5..f6a7dda 100644 --- a/src/index.js +++ b/src/index.js @@ -28,6 +28,7 @@ import articleSetPricesBySku from './store/article/setPricesBySku.js'; import articleSetEansBySku from './store/article/setEansBySku.js'; import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js'; import articleSetWeightBySku from './store/article/setWeightBySku.js'; +import articleSetTrackStockBySku from './store/article/setTrackStockBySku.js'; import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js'; import articleSetFatherBySku from './store/article/setFatherBySku.js'; import articleImportRows from './store/article/importRows.js'; @@ -131,6 +132,7 @@ import warehouseGetById from './store/warehouse/getById.js'; import warehouseList from './store/warehouse/list.js'; import warehouseUpdate from './store/warehouse/update.js'; import warehouseDelete from './store/warehouse/delete.js'; +import warehouseOrder from './store/warehouse/order.js'; import printerCreate from './store/printer/create.js'; import printerGetById from './store/printer/getById.js'; import printerList from './store/printer/list.js'; @@ -194,6 +196,7 @@ export default class LibDashStore { setEansBySku: articleSetEansBySku(provider), setBrandIdBySku: articleSetBrandIdBySku(provider), setWeightBySku: articleSetWeightBySku(provider), + setTrackStockBySku: articleSetTrackStockBySku(provider), setCategoryIdsBySku: articleSetCategoryIdsBySku(provider), setFatherBySku: articleSetFatherBySku(provider), importRows: articleImportRows(provider), @@ -362,6 +365,7 @@ export default class LibDashStore { list: warehouseList(provider), update: warehouseUpdate(provider), delete: warehouseDelete(provider), + order: warehouseOrder(provider), }; this.printer = { diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index c612c37..97153ae 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -31,6 +31,7 @@ export const ARTICLE_LIST_COLUMNS = ` at.slug, pr.net, t.rate AS taxRate, + a.track_stock, CASE WHEN a.optionslist_id IS NOT NULL THEN ( SELECT COUNT(*) @@ -133,6 +134,12 @@ SET weight = @weight WHERE id = @articleId `; +export const UPDATE_TRACK_STOCK = ` +UPDATE article +SET track_stock = @trackStock +WHERE id = @articleId +`; + export const UPDATE_BRAND = ` UPDATE article SET brand_id = @brandId @@ -158,9 +165,9 @@ WHERE id = @articleId `; export const CREATE = ` -INSERT INTO article (company_id, sku, unit_id, unitamount, weight) +INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock) OUTPUT INSERTED.id -VALUES (@companyId, @sku, @unitId, @unitamount, @weight) +VALUES (@companyId, @sku, @unitId, @unitamount, @weight, @trackStock) `; export const DELETE_EANS_BY_ARTICLE = ` @@ -287,6 +294,7 @@ SELECT u.si AS unitSi, ut.symbol AS unitSymbol, a.unitamount, + a.track_stock, b.name AS brandName, b.name AS brandName, pr.net AS net, @@ -371,6 +379,7 @@ SELECT at.slug, a.unitamount, a.weight, + a.track_stock, u.si AS unitSi, u.basesi AS unitBasesi, u.basesifactor AS unitBasesifactor, @@ -467,6 +476,7 @@ SELECT SELECT w.id, w.name, + w.sort, COALESCE(stock_calc.stock, 0) AS stock FROM warehouse w LEFT JOIN ( @@ -482,7 +492,7 @@ SELECT GROUP BY l.warehouse_id ) stock_calc ON stock_calc.warehouse_id = w.id WHERE w.company_id = a.company_id - ORDER BY w.name ASC + ORDER BY w.sort ASC, w.name ASC FOR JSON PATH ) AS warehousesJson FROM article a diff --git a/src/providers/mssql/queries/articleDetail.js b/src/providers/mssql/queries/articleDetail.js index f995020..a97c800 100644 --- a/src/providers/mssql/queries/articleDetail.js +++ b/src/providers/mssql/queries/articleDetail.js @@ -5,6 +5,7 @@ export const ARTICLE_DETAIL_COLUMNS = ` at.slug, a.unitamount, a.weight, + a.track_stock, u.si AS unitSi, u.basesi AS unitBasesi, u.basesifactor AS unitBasesifactor, diff --git a/src/providers/mssql/queries/warehouse.js b/src/providers/mssql/queries/warehouse.js index 33534fb..b9333f5 100644 --- a/src/providers/mssql/queries/warehouse.js +++ b/src/providers/mssql/queries/warehouse.js @@ -2,17 +2,19 @@ export const LIST_BY_COMPANY = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE company_id = @companyId -ORDER BY name ASC; +ORDER BY sort ASC, name ASC; `; export const GET_BY_COMPANY_AND_ID = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE id = @id AND company_id = @companyId; `; @@ -21,20 +23,39 @@ export const GET_BY_COMPANY_AND_NAME = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE company_id = @companyId AND name = @name; `; +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM warehouse +WHERE company_id = @companyId; +`; + export const CREATE = ` -INSERT INTO warehouse (company_id, name) -OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name -VALUES (@companyId, @name); +INSERT INTO warehouse (company_id, name, sort) +OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort +VALUES (@companyId, @name, @sort); `; export const UPDATE_BY_ID = ` UPDATE warehouse -SET name = @name +SET name = @name, sort = @sort +WHERE id = @id AND company_id = @companyId; +`; + +export const LIST_IDS_BY_COMPANY = ` +SELECT id +FROM warehouse +WHERE company_id = @companyId; +`; + +export const UPDATE_SORT = ` +UPDATE warehouse +SET sort = @sort WHERE id = @id AND company_id = @companyId; `; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index dd3051e..354e963 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -317,6 +317,7 @@ BEGIN unit_id INT NOT NULL, unitamount FLOAT NOT NULL, weight FLOAT NOT NULL, + track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1, brand_id INT NULL, articlelist_id INT NULL, optionslist_id INT NULL, @@ -340,6 +341,10 @@ BEGIN ON article (fatherarticle_id, fathersort) WHERE fatherarticle_id IS NOT NULL; END +ELSE IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock') +BEGIN + ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1; +END `; export const INIT_ARTICLEHISTORY = ` @@ -599,10 +604,15 @@ BEGIN id INT IDENTITY(1,1) PRIMARY KEY, company_id INT NOT NULL, name NVARCHAR(255) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_warehouse_sort DEFAULT 0, CONSTRAINT FK_warehouse_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, CONSTRAINT UQ_warehouse_company_name UNIQUE (company_id, name) ); END +ELSE IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('warehouse') AND name = 'sort') +BEGIN + ALTER TABLE warehouse ADD sort INT NOT NULL CONSTRAINT DF_warehouse_sort DEFAULT 0; +END `; export const INIT_LEDGER = ` diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 639e547..9921ee0 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -399,6 +399,20 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { `ALTER TABLE ledger ADD COLUMN warehouse_id INTEGER REFERENCES warehouse(id) ON DELETE NO ACTION`, ); } + + const articleCols = this.db.pragma('table_info("article")').map((c) => c.name); + if (articleCols.length > 0 && !articleCols.includes('track_stock')) { + this.db.exec( + `ALTER TABLE article ADD COLUMN track_stock INTEGER NOT NULL DEFAULT 1`, + ); + } + + const warehouseCols = this.db.pragma('table_info("warehouse")').map((c) => c.name); + if (warehouseCols.length > 0 && !warehouseCols.includes('sort')) { + this.db.exec( + `ALTER TABLE warehouse ADD COLUMN sort INTEGER NOT NULL DEFAULT 0`, + ); + } } catch { // ignore } diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index eca1ff7..f848d37 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -8,7 +8,7 @@ export const { GET_BRAND_BY_COMPANY_AND_ID, GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU, SET_FATHER_SORT, - SET_IS_FATHER, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_UNIT, + SET_IS_FATHER, UPDATE_BRAND, UPDATE_FATHER_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT, UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT, } = queries; @@ -83,7 +83,7 @@ WITH RECURSIVE category_tree AS ( INNER JOIN category_tree ct ON c.parent_id = ct.id ) SELECT DISTINCT a.id, a.sku, at.name, at.slug, u.si AS unitSi, - ut.symbol AS unitSymbol, a.unitamount, b.name AS brandName, + ut.symbol AS unitSymbol, a.unitamount, a.track_stock, b.name AS brandName, pr.net, t.rate AS taxRate, ${JSON_EANS} AS eansJson, ${JSON_CATEGORIES} AS categoryIdsJson, @@ -122,6 +122,7 @@ SELECT auth.*, json_object( 'id', a.id, 'sku', a.sku, 'name', at.name, 'slug', at.slug, 'unitamount', a.unitamount, 'weight', a.weight, + 'track_stock', a.track_stock, 'unitSi', u.si, 'unitBasesi', u.basesi, 'unitBasesifactor', u.basesifactor, 'unitSymbol', ut.symbol, 'unitName', ut.name, @@ -169,7 +170,7 @@ SELECT auth.*, WHERE s.article_id = a.id ORDER BY s.sort, s.id ))), 'warehousesJson', json((SELECT COALESCE(json_group_array(json_object( - 'id', w.id, 'name', w.name, 'stock', COALESCE(stock_calc.stock, 0) + 'id', w.id, 'name', w.name, 'sort', w.sort, 'stock', COALESCE(stock_calc.stock, 0) )), '[]') FROM warehouse w LEFT JOIN ( SELECT l.warehouse_id, SUM(l.amount) AS stock @@ -184,7 +185,7 @@ SELECT auth.*, GROUP BY l.warehouse_id ) stock_calc ON stock_calc.warehouse_id = w.id WHERE w.company_id = a.company_id - ORDER BY w.name ASC)) + ORDER BY w.sort ASC, w.name ASC)) ) AS articleJson FROM auth INNER JOIN article a ON a.id = auth.articleId diff --git a/src/providers/sqlite3/queries/warehouse.js b/src/providers/sqlite3/queries/warehouse.js index 94c8c9f..55ccf86 100644 --- a/src/providers/sqlite3/queries/warehouse.js +++ b/src/providers/sqlite3/queries/warehouse.js @@ -2,17 +2,19 @@ export const LIST_BY_COMPANY = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE company_id = @companyId -ORDER BY name ASC; +ORDER BY sort ASC, name ASC; `; export const GET_BY_COMPANY_AND_ID = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE id = @id AND company_id = @companyId; `; @@ -21,20 +23,39 @@ export const GET_BY_COMPANY_AND_NAME = ` SELECT id, company_id, - name + name, + sort FROM warehouse WHERE company_id = @companyId AND name = @name; `; +export const GET_NEXT_SORT = ` +SELECT IFNULL(MAX(sort), -1) + 1 AS sort +FROM warehouse +WHERE company_id = @companyId; +`; + export const CREATE = ` -INSERT INTO warehouse (company_id, name) -VALUES (@companyId, @name) -RETURNING id, company_id, name; +INSERT INTO warehouse (company_id, name, sort) +VALUES (@companyId, @name, @sort) +RETURNING id, company_id, name, sort; `; export const UPDATE_BY_ID = ` UPDATE warehouse -SET name = @name +SET name = @name, sort = @sort +WHERE id = @id AND company_id = @companyId; +`; + +export const LIST_IDS_BY_COMPANY = ` +SELECT id +FROM warehouse +WHERE company_id = @companyId; +`; + +export const UPDATE_SORT = ` +UPDATE warehouse +SET sort = @sort WHERE id = @id AND company_id = @companyId; `; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 366cec1..93e8b94 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -216,6 +216,7 @@ CREATE TABLE IF NOT EXISTS article ( unit_id INTEGER NOT NULL, unitamount REAL NOT NULL, weight REAL NOT NULL, + track_stock INTEGER NOT NULL DEFAULT 1, brand_id INTEGER, articlelist_id INTEGER, optionslist_id INTEGER, @@ -431,6 +432,7 @@ CREATE TABLE IF NOT EXISTS warehouse ( id INTEGER PRIMARY KEY, company_id INTEGER NOT NULL, name TEXT NOT NULL COLLATE NOCASE, + sort INTEGER NOT NULL DEFAULT 0, UNIQUE (company_id, name), FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE ); diff --git a/src/store/article/createUnderParent.js b/src/store/article/createUnderParent.js index 89d0857..d206758 100644 --- a/src/store/article/createUnderParent.js +++ b/src/store/article/createUnderParent.js @@ -32,14 +32,18 @@ export default function createUnderParent(provider) { * @param {number} input.googleAuthId - The id of the Google auth user. * @param {number} input.parentId - The id of the parent category. * @param {string} input.name - The article name. + * @param {boolean|number} [input.track_stock=true] - Whether inventory stock is tracked. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, parentId: number, name: string, sku: string}>} The created article. + * @returns {Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>} The created article. * * @example * const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); - * // => { id, parentId, name, sku } + * // => { id, parentId, name, sku, track_stock } */ - return async ({ googleAuthId, parentId, name }, tx) => { + return async ({ googleAuthId, parentId, name, track_stock, trackStock }, tx) => { + const rawTrack = track_stock ?? trackStock; + const finalTrackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0); + const runCreate = async (runner) => { const { companyId, languageId, companyuserId } = await access( googleAuthId, @@ -59,6 +63,7 @@ export default function createUnderParent(provider) { unitId, unitamount: 1, weight: 0, + trackStock: finalTrackStock, }); } catch (err) { if (isUniqueConstraintError(err)) { @@ -92,6 +97,7 @@ export default function createUnderParent(provider) { parentId, name, sku, + track_stock: Boolean(finalTrackStock), }; }; diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 52d3316..ac71097 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -18,7 +18,7 @@ export default function getBySku(provider) { * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. * @param {object} [tx] - Optional transaction. - * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail. + * @returns {Promise<{article: {id: number, sku: string, name: string, slug: string, 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, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>} The full article detail. * @throws {Error} If the article is not found or the user is not authorized. * * @example @@ -56,6 +56,7 @@ export default function getBySku(provider) { slug: a.slug, unitamount: a.unitamount, weight: a.weight, + track_stock: Boolean(a.track_stock ?? 1), unit: { si: a.unitSi, basesi: a.unitBasesi, @@ -92,6 +93,7 @@ export default function getBySku(provider) { warehouses: parseJsonArray(a.warehousesJson).map((w) => ({ id: w.id, name: w.name, + sort: Number(w.sort ?? 0), stock: Number(w.stock ?? 0), })), optionsCount: a.optionsCount, diff --git a/src/store/article/importRows.js b/src/store/article/importRows.js index 03ed802..f2e2d0f 100644 --- a/src/store/article/importRows.js +++ b/src/store/article/importRows.js @@ -103,6 +103,7 @@ export default function importRows(provider) { * @param {number|string} [input.rows[].net] - The POS net price (accepts German decimals). * @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`). * @param {Array|string} [input.rows[].eans] - The EANs to set. + * @param {boolean|number} [input.rows[].track_stock=true] - Whether inventory stock is tracked. * @param {string} [input.rows[].fatherSku] - The father article SKU for a V/K compound link. * @param {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored. * @param {object} [tx] - Optional transaction. @@ -168,12 +169,16 @@ export default function importRows(provider) { const unitId = await ensureUnit(runner, companyId, languageId); const slug = slugify(name); + const rawTrack = row.track_stock ?? row.trackStock; + const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0); + const article = await runner.query(articleQueries.CREATE, { companyId, sku, unitId, unitamount: 1, weight: 0, + trackStock, }); const articleId = article.rows[0]?.id ?? article.insertId; diff --git a/src/store/article/listExport.js b/src/store/article/listExport.js index 3e968f2..dc5a448 100644 --- a/src/store/article/listExport.js +++ b/src/store/article/listExport.js @@ -94,6 +94,7 @@ export default function listExport(provider) { unitSi: row.unitSi, unitSymbol: row.unitSymbol, unitamount: row.unitamount, + track_stock: Boolean(row.track_stock ?? 1), brandName: row.brandName, net: row.net, taxRate: row.taxRate, diff --git a/src/store/article/queryBySku.js b/src/store/article/queryBySku.js index beb31ce..e3116a3 100644 --- a/src/store/article/queryBySku.js +++ b/src/store/article/queryBySku.js @@ -10,6 +10,7 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) { slug: row.slug, unitamount: row.unitamount, weight: row.weight, + track_stock: Boolean(row.track_stock ?? 1), unit: { si: row.unitSi, basesi: row.unitBasesi, diff --git a/src/store/article/setTrackStockBySku.js b/src/store/article/setTrackStockBySku.js new file mode 100644 index 0000000..9103344 --- /dev/null +++ b/src/store/article/setTrackStockBySku.js @@ -0,0 +1,53 @@ +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 queryBySku from './queryBySku.js'; +import resolveArticleBySku from './resolveArticleBySku.js'; +import makeRunner from '../runner.js'; + +export default function setTrackStockBySku(provider) { + const resolve = resolveArticleBySku(provider); + const load = queryBySku(provider); + const sql = + provider?.dialect === 'sqlite3' + ? SQLITE_UPDATE_TRACK_STOCK + : MSSQL_UPDATE_TRACK_STOCK; + + /** + * Sets the track_stock flag of an article by its SKU. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {string} sku - The article SKU. + * @param {boolean|number} trackStock - Whether stock tracking is enabled. + * @param {object} [tx] - Optional transaction. + * @returns {Promise} The updated article. + * @throws {Error} If trackStock is invalid or the article is not found. + * + * @example + * const article = await store.article.setTrackStockBySku(42, 'SKU-1', true); + * // => { id, sku, name, track_stock, ... } + */ + return async (googleAuthId, sku, trackStock, tx) => { + if (typeof trackStock !== 'boolean' && trackStock !== 0 && trackStock !== 1) { + throw new Error('Invalid track_stock'); + } + + const runner = makeRunner(provider, tx); + const { articleId, languageId } = await resolve(googleAuthId, sku, runner); + + const result = await runner.query(sql, { + articleId, + trackStock: trackStock ? 1 : 0, + }); + + if (!result.rowsAffected) { + throw new Error('Article not found'); + } + + const article = await load(articleId, languageId, runner); + if (!article) { + throw new Error('Article not found'); + } + + return article; + }; +} diff --git a/src/store/article/updateBySku.js b/src/store/article/updateBySku.js index 79021af..67ea0f8 100644 --- a/src/store/article/updateBySku.js +++ b/src/store/article/updateBySku.js @@ -41,20 +41,24 @@ export default function updateBySku(provider) { * @param {string} [patch.name] - The new article name. * @param {string} [patch.unitSi] - The new unit SI code. * @param {number} [patch.unitamount] - The new unit amount (must be > 0). + * @param {boolean|number} [patch.track_stock] - Whether inventory stock is tracked. * @param {object} [tx] - Optional transaction to run within. * @returns {Promise} The updated article. * @throws {Error} If no valid field is provided or the article is not found. * * @example - * const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1 }); + * const article = await store.article.updateBySku(42, 'SKU-1', { name: 'Cola 1L', unitSi: 'l', unitamount: 1, track_stock: true }); * // => { id, sku, name, ... } */ return async (googleAuthId, sku, patch = {}, tx) => { const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount'); const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi'); + const hasTrackStock = + Object.prototype.hasOwnProperty.call(patch, 'track_stock') || + Object.prototype.hasOwnProperty.call(patch, 'trackStock'); - if (!hasName && !hasUnitamount && !hasUnitSi) { + if (!hasName && !hasUnitamount && !hasUnitSi && !hasTrackStock) { throw new Error('Name required'); } @@ -66,6 +70,13 @@ export default function updateBySku(provider) { throw new Error('Invalid unitamount'); } + const rawTrackStock = Object.prototype.hasOwnProperty.call(patch, 'track_stock') + ? patch.track_stock + : patch.trackStock; + if (hasTrackStock && typeof rawTrackStock !== 'boolean' && rawTrackStock !== 0 && rawTrackStock !== 1) { + throw new Error('Invalid track_stock'); + } + const runUpdate = async (runner) => { const { articleId, languageId, companyId, companyuserId } = await resolve( googleAuthId, @@ -122,6 +133,15 @@ export default function updateBySku(provider) { historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`); } } + if (hasTrackStock) { + const oldTrack = Boolean(current.track_stock ?? 1); + const newTrack = Boolean(rawTrackStock); + if (oldTrack !== newTrack) { + historyLines.push( + `Bestandsführung geändert von "${oldTrack ? 'Ja' : 'Nein'}" auf "${newTrack ? 'Ja' : 'Nein'}"`, + ); + } + } if (hasName) { const slug = slugify(patch.name); @@ -155,6 +175,13 @@ export default function updateBySku(provider) { }); } + if (hasTrackStock) { + await runner.query(articleQueries.UPDATE_TRACK_STOCK, { + articleId, + trackStock: Boolean(rawTrackStock) ? 1 : 0, + }); + } + if (historyLines.length) { await runner.query(insertHistorySql, { articleId, diff --git a/src/store/category/queryCategoryArticles.js b/src/store/category/queryCategoryArticles.js index 7e76256..366b0b6 100644 --- a/src/store/category/queryCategoryArticles.js +++ b/src/store/category/queryCategoryArticles.js @@ -24,6 +24,7 @@ export default function queryCategoryArticles(provider) { } = row; return { ...rest, + track_stock: Boolean(rest.track_stock ?? 1), brand: brandName ? { id: brandId ?? null, diff --git a/src/store/warehouse/create.js b/src/store/warehouse/create.js index 8654d43..1f47607 100644 --- a/src/store/warehouse/create.js +++ b/src/store/warehouse/create.js @@ -1,17 +1,24 @@ -import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/warehouse.js'; -import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/warehouse.js'; +import { + CREATE as MSSQL_CREATE, + GET_NEXT_SORT as MSSQL_GET_NEXT_SORT, +} from '../../providers/mssql/queries/warehouse.js'; +import { + CREATE as SQLITE_CREATE, + GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, +} from '../../providers/sqlite3/queries/warehouse.js'; import { isUniqueConstraintError } from '../../providers/provider.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; function shape(row) { - return { id: row.id, name: row.name }; + return { id: row.id, name: row.name, sort: row.sort }; } export default function create(provider) { const access = resolveSelectedCompanyAccess(provider); - const sql = - provider?.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; + const isSqlite = provider?.dialect === 'sqlite3'; + const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; + const sql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; /** * Creates a new warehouse for the user's selected company. @@ -19,15 +26,16 @@ export default function create(provider) { * @param {number} googleAuthId - The id of the Google auth user. * @param {object} input - Warehouse input. * @param {string} input.name - The warehouse name. + * @param {number} [input.sort] - Optional sort order (auto-increments if omitted). * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, name: string}>} The created warehouse. - * @throws {Error} If name is missing or already exists. + * @returns {Promise<{id: number, name: string, sort: number}>} The created warehouse. + * @throws {Error} If name is missing, sort is invalid, or name already exists. * * @example * const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); - * // => { id: 1, name: 'Hauptlager' } + * // => { id: 1, name: 'Hauptlager', sort: 0 } */ - return async (googleAuthId, { name } = {}, tx) => { + return async (googleAuthId, { name, sort: explicitSort } = {}, tx) => { const cleaned = typeof name === 'string' ? name.trim() : ''; if (!cleaned) { throw new Error('Name required'); @@ -36,13 +44,27 @@ export default function create(provider) { const runner = makeRunner(provider, tx); const { companyId } = await access(googleAuthId, runner); + let sort = explicitSort; + if (sort === undefined || sort === null) { + const next = await runner.query(getSortSql, { companyId }); + sort = next.rows[0]?.sort ?? 0; + } else if (!Number.isFinite(sort)) { + throw new Error('Invalid sort'); + } + try { const result = await runner.query(sql, { companyId, name: cleaned, + sort, }); - const row = result.rows[0] ?? { id: result.insertId, name: cleaned }; + const row = result.rows[0] ?? { + id: result.insertId, + company_id: companyId, + name: cleaned, + sort, + }; return shape(row); } catch (err) { if ( diff --git a/src/store/warehouse/getById.js b/src/store/warehouse/getById.js index fda077e..052726e 100644 --- a/src/store/warehouse/getById.js +++ b/src/store/warehouse/getById.js @@ -4,7 +4,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import makeRunner from '../runner.js'; function shape(row) { - return { id: row.id, name: row.name }; + return { id: row.id, name: row.name, sort: row.sort }; } export default function getById(provider) { @@ -18,11 +18,11 @@ export default function getById(provider) { * @param {number} googleAuthId - The id of the Google auth user. * @param {number} id - The warehouse ID. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, name: string}|undefined>} The warehouse or undefined. + * @returns {Promise<{id: number, name: string, sort: number}|undefined>} The warehouse or undefined. * * @example * const wh = await store.warehouse.getById(googleAuthId, 1); - * // => { id: 1, name: 'Hauptlager' } + * // => { id: 1, name: 'Hauptlager', sort: 0 } */ return async (googleAuthId, id, tx) => { const numericId = Number(id); diff --git a/src/store/warehouse/list.js b/src/store/warehouse/list.js index c9b6917..c96e6d0 100644 --- a/src/store/warehouse/list.js +++ b/src/store/warehouse/list.js @@ -4,7 +4,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import makeRunner from '../runner.js'; function shape(row) { - return { id: row.id, name: row.name }; + return { id: row.id, name: row.name, sort: row.sort }; } export default function list(provider) { @@ -17,11 +17,11 @@ export default function list(provider) { * * @param {number} googleAuthId - The id of the Google auth user. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise>} List of warehouses. + * @returns {Promise>} List of warehouses. * * @example * const warehouses = await store.warehouse.list(googleAuthId); - * // => [{ id: 1, name: 'Hauptlager' }] + * // => [{ id: 1, name: 'Hauptlager', sort: 0 }] */ return async (googleAuthId, tx) => { const runner = makeRunner(provider, tx); diff --git a/src/store/warehouse/order.js b/src/store/warehouse/order.js new file mode 100644 index 0000000..1e39f5b --- /dev/null +++ b/src/store/warehouse/order.js @@ -0,0 +1,76 @@ +import { + LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS_BY_COMPANY, + UPDATE_SORT as MSSQL_UPDATE_SORT, +} from '../../providers/mssql/queries/warehouse.js'; +import { + LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS_BY_COMPANY, + UPDATE_SORT as SQLITE_UPDATE_SORT, +} from '../../providers/sqlite3/queries/warehouse.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function order(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const listIdsSql = isSqlite + ? SQLITE_LIST_IDS_BY_COMPANY + : MSSQL_LIST_IDS_BY_COMPANY; + const updateSortSql = isSqlite ? SQLITE_UPDATE_SORT : MSSQL_UPDATE_SORT; + + /** + * Reorders the warehouses of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The reorder input. + * @param {Array} input.warehouseIds - The warehouse ids in their new order. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{warehouseIds: Array}>} The applied order. + * @throws {Error} If the order is invalid. + */ + return async (googleAuthId, { warehouseIds } = {}, tx) => { + if (!Array.isArray(warehouseIds)) { + throw new Error('Invalid warehouse order'); + } + + const runOrder = async (runner) => { + const { companyId } = await access(googleAuthId, runner); + + const items = await runner.query(listIdsSql, { companyId }); + const existing = new Set(items.rows.map((r) => r.id)); + + if (warehouseIds.length !== existing.size) { + throw new Error('Invalid warehouse order'); + } + + const seen = new Set(); + for (const warehouseId of warehouseIds) { + if ( + !Number.isFinite(warehouseId) || + !existing.has(warehouseId) || + seen.has(warehouseId) + ) { + throw new Error('Invalid warehouse order'); + } + seen.add(warehouseId); + } + + for (let sort = 0; sort < warehouseIds.length; sort++) { + await runner.query(updateSortSql, { + id: warehouseIds[sort], + companyId, + sort, + }); + } + + return { warehouseIds }; + }; + + if (tx) { + return runOrder(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runOrder); + } + return runOrder(makeRunner(provider)); + }; +} diff --git a/src/store/warehouse/update.js b/src/store/warehouse/update.js index cfb0055..ce88a92 100644 --- a/src/store/warehouse/update.js +++ b/src/store/warehouse/update.js @@ -11,7 +11,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA import makeRunner from '../runner.js'; function shape(row) { - return { id: row.id, name: row.name }; + return { id: row.id, name: row.name, sort: row.sort }; } export default function update(provider) { @@ -21,47 +21,66 @@ export default function update(provider) { const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; /** - * Updates a warehouse name for the user's selected company. + * Updates a warehouse name and/or sort order for the user's selected company. * * @param {number} googleAuthId - The id of the Google auth user. * @param {number} id - The warehouse ID to update. * @param {object} input - Update payload. - * @param {string} input.name - The new warehouse name. + * @param {string} [input.name] - The new warehouse name. + * @param {number} [input.sort] - The new sort order. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, name: string}>} The updated warehouse. - * @throws {Error} If warehouse not found, name is missing, or name already exists. + * @returns {Promise<{id: number, name: string, sort: number}>} The updated warehouse. + * @throws {Error} If warehouse not found, name is empty, sort is invalid, or name already exists. * * @example - * const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); - * // => { id: 1, name: 'Zentrallager' } + * const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager', sort: 1 }); + * // => { id: 1, name: 'Zentrallager', sort: 1 } */ - return async (googleAuthId, id, { name } = {}, tx) => { + return async (googleAuthId, id, input = {}, tx) => { const numericId = Number(id); if (!Number.isInteger(numericId) || numericId <= 0) { throw new Error('Warehouse not found'); } - const cleaned = typeof name === 'string' ? name.trim() : ''; - if (!cleaned) { - throw new Error('Name required'); - } - const runner = makeRunner(provider, tx); const { companyId } = await access(googleAuthId, runner); const existing = await runner.query(getSql, { id: numericId, companyId }); - if (!existing.rows[0]) { + const current = existing.rows[0]; + if (!current) { throw new Error('Warehouse not found'); } + let name = current.name; + if (input.name !== undefined) { + const cleaned = typeof input.name === 'string' ? input.name.trim() : ''; + if (!cleaned) { + throw new Error('Name required'); + } + name = cleaned; + } + + let sort = current.sort; + if (input.sort !== undefined) { + if (!Number.isFinite(input.sort)) { + throw new Error('Invalid sort'); + } + sort = input.sort; + } + + if (input.name === undefined && input.sort === undefined) { + throw new Error('Name required'); + } + try { await runner.query(updateSql, { id: numericId, companyId, - name: cleaned, + name, + sort, }); - return shape({ id: numericId, name: cleaned }); + return shape({ id: numericId, name, sort }); } catch (err) { if ( isUniqueConstraintError(err) ||