This commit is contained in:
seb
2026-09-27 21:01:30 +02:00
parent f9c570a036
commit 0cafa11a32
34 changed files with 2010 additions and 1534 deletions

View File

@@ -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.googleAuthId` | `number` | The id of the Google auth user. |
| `input.parentId` | `number` | The id of the parent category. | | `input.parentId` | `number` | The id of the parent category. |
| `input.name` | `string` | The article name. | | `input.name` | `string` | The article name. |
| `[input.track_stock=true]` | `boolean|number` | Whether inventory stock is tracked. |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **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** **Example**
```js ```js
const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' });
// => { id, parentId, name, sku } // => { id, parentId, name, sku, track_stock }
``` ```
## `article.delete` ## `article.delete`
@@ -128,7 +129,7 @@ Returns the full detail of an article by its SKU, including unit, brand, categor
**Returns** **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** **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[].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[].tax]` | `number|string` | The tax rate in percent (e.g. `19`). |
| `[input.rows[].eans]` | `Array<string>|string` | The EANs to set. | | `[input.rows[].eans]` | `Array<string>|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[].fatherSku]` | `string` | The father article SKU for a V/K compound link. |
| `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. | | `[input.rows[].deposit]` | `number|string` | The deposit (Pfand) value; currently ignored. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
@@ -443,6 +445,36 @@ const article = await store.article.setPricesBySku(42, 'SKU-1', [{ price: 1.5, t
// => { id, sku, name, ... } // => { 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<object>` — 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` ## `article.setWeightBySku`
*Source: `src/store/article/setWeightBySku.js`* *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.name]` | `string` | The new article name. |
| `[patch.unitSi]` | `string` | The new unit SI code. | | `[patch.unitSi]` | `string` | The new unit SI code. |
| `[patch.unitamount]` | `number` | The new unit amount (must be > 0). | | `[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. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
@@ -502,6 +535,6 @@ Updates an article's name and/or unit (SI and amount), recording history.
**Example** **Example**
```js ```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, ... } // => { id, sku, name, ... }
``` ```

View File

@@ -2,6 +2,25 @@
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. 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<object>}>`
## `brand.delete` ## `brand.delete`
*Source: `src/store/brand/delete.js`* *Source: `src/store/brand/delete.js`*

View File

@@ -34,7 +34,7 @@ const setting = await store.companysetting.get(42, 'ui.theme');
*Source: `src/store/companysetting/list.js`* *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** **Parameters**

21
API.md
View File

@@ -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 > **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` - **articleean**: `id PK, article_id->article, ean:s(50) UQ`
- **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt` - **articlehistory**: `id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt`
- **articlelist**: `id PK` - **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` - **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)` - **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)` - **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 ## 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}>` - `rename(input: {googleAuthId: number, id: number, name: string}, tx?: object): Promise<{id: number, name: string, slug: string}>`
- [article](API.article.md) - [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}>` - `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<void>` - `delete(googleAuthId: number, sku: string, tx?: object): Promise<void>`
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>` - `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, 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<Array<{id: number, companyuserId: number|null, date: string, text: string}>>` - `getHistoryBySku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>` - `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>` - `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
@@ -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<string>, tx?: object): Promise<object>` - `setEansBySku(googleAuthId: number, sku: string, eans: Array<string>, tx?: object): Promise<object>`
- `setFatherBySku(googleAuthId: number, sku: string, fatherSku: string|null, tx?: object): Promise<object>` - `setFatherBySku(googleAuthId: number, sku: string, fatherSku: string|null, tx?: object): Promise<object>`
- `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise<object>` - `setPricesBySku(googleAuthId: number, sku: string, prices: Array<{price: number, tax: number, minmultiplier?: number}>, tx?: object): Promise<object>`
- `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|number, tx?: object): Promise<object>`
- `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise<object>` - `setWeightBySku(googleAuthId: number, sku: string, weight: number, tx?: object): Promise<object>`
- `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number}, tx?: object): Promise<object>` - `updateBySku(googleAuthId: number, sku: string, patch: {name?: string, unitSi?: string, unitamount?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- [lang](API.lang.md) - [lang](API.lang.md)
- `create(iso639: string, tx?: object): Promise<number>` - `create(iso639: string, tx?: object): Promise<number>`
- `delete(id: number): Promise<void>` - `delete(id: number): Promise<void>`
@@ -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<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>` - `list(googleAuthId: number, options?: {frontendOnly?: boolean, maskWriteonly?: boolean}, tx?: object): Promise<Array<{key: string, value: string, frontend: boolean, writeonly: boolean}>>`
- `set(googleAuthId: number, input: {key: string, value: string, frontend?: boolean, writeonly?: boolean}, tx?: object): Promise<{key: string, value: string, frontend: boolean, writeonly: boolean}>` - `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) - [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<object>}>`
- `delete(googleAuthId: number, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, gsprpinfo: string|null, pics: Array<{id: number, sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>}>>`
- `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}>}>` - `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<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>` - `listByManifestarticleId(googleAuthId: number, manifestarticleId: number, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
- `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>` - `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise<Array<{id: number, manifestarticle_id: number, warehouse_id: number|null, sku: string|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>>`
- [warehouse](API.warehouse.md) - [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<void>` - `delete(googleAuthId: number, id: number, tx?: object): Promise<void>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string}|undefined>` - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string, sort: number}|undefined>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string}>>` - `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, sort: number}>>`
- `update(googleAuthId: number, id: number, input: {name: string}, tx?: object): Promise<{id: number, name: string}>` - `order(googleAuthId: number, input: {warehouseIds: Array<number>}, tx?: object): Promise<{warehouseIds: Array<number>}>`
- `update(googleAuthId: number, id: number, input: {name?: string, sort?: number}, tx?: object): Promise<{id: number, name: string, sort: number}>`
- [stock](API.stock.md) - [stock](API.stock.md)
- `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>` - `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array<object>}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array<object>, ledger: Array<object>}>`
- [posterminal](API.posterminal.md) - [posterminal](API.posterminal.md)

View File

@@ -15,21 +15,22 @@ Creates a new warehouse for the user's selected company.
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `input` | `object` | Warehouse input. | | `input` | `object` | Warehouse input. |
| `input.name` | `string` | The warehouse name. | | `input.name` | `string` | The warehouse name. |
| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). |
| `[tx]` | `object` | Optional transaction to run within. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, name: string}>` — The created warehouse. `Promise<{id: number, name: string, sort: number}>` — The created warehouse.
**Throws** **Throws**
`Error` — If name is missing or already exists. `Error` — If name is missing, sort is invalid, or name already exists.
**Example** **Example**
```js ```js
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
// => { id: 1, name: 'Hauptlager' } // => { id: 1, name: 'Hauptlager', sort: 0 }
``` ```
## `warehouse.delete` ## `warehouse.delete`
@@ -76,13 +77,13 @@ Retrieves a warehouse by its ID for the user's selected company.
**Returns** **Returns**
`Promise<{id: number, name: string}|undefined>` — The warehouse or undefined. `Promise<{id: number, name: string, sort: number}|undefined>` — The warehouse or undefined.
**Example** **Example**
```js ```js
const wh = await store.warehouse.getById(googleAuthId, 1); const wh = await store.warehouse.getById(googleAuthId, 1);
// => { id: 1, name: 'Hauptlager' } // => { id: 1, name: 'Hauptlager', sort: 0 }
``` ```
## `warehouse.list` ## `warehouse.list`
@@ -100,20 +101,43 @@ Lists all warehouses for the user's selected company.
**Returns** **Returns**
`Promise<Array<{id: number, name: string}>>` — List of warehouses. `Promise<Array<{id: number, name: string, sort: number}>>` — List of warehouses.
**Example** **Example**
```js ```js
const warehouses = await store.warehouse.list(googleAuthId); 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<number>` | The warehouse ids in their new order. |
| `[tx]` | `object` | Optional transaction to run within. |
**Returns**
`Promise<{warehouseIds: Array<number>}>` — The applied order.
**Throws**
`Error` — If the order is invalid.
## `warehouse.update` ## `warehouse.update`
*Source: `src/store/warehouse/update.js`* *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** **Parameters**
@@ -122,20 +146,21 @@ Updates a warehouse name for the user's selected company.
| `googleAuthId` | `number` | The id of the Google auth user. | | `googleAuthId` | `number` | The id of the Google auth user. |
| `id` | `number` | The warehouse ID to update. | | `id` | `number` | The warehouse ID to update. |
| `input` | `object` | Update payload. | | `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. | | `[tx]` | `object` | Optional transaction to run within. |
**Returns** **Returns**
`Promise<{id: number, name: string}>` — The updated warehouse. `Promise<{id: number, name: string, sort: number}>` — The updated warehouse.
**Throws** **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** **Example**
```js ```js
const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager', sort: 1 });
// => { id: 1, name: 'Zentrallager' } // => { id: 1, name: 'Zentrallager', sort: 1 }
``` ```

View File

@@ -543,14 +543,15 @@ Company-scoped POS terminals. Auth via selected company (`google_auth.selected_c
| --- | --- | --- | | --- | --- | --- |
| `listSearchIndex` | `(googleAuthId: string, companyId: number)` | `SearchIndexEntry[]` (see below) | | `listSearchIndex` | `(googleAuthId: string, companyId: number)` | `SearchIndexEntry[]` (see below) |
| `listExport` | `(googleAuthId: string, companyId: number, categoryId?: number)` | `ArticleExportEntry[]` (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) | | `getBySku` | `(googleAuthId: string, sku: string)` | `ArticleDetail` (see below) |
| `getHistoryBySku` | `(googleAuthId: string, sku: string)` | `{ id: number, companyuserId: number \| null, date: Date, text: string }[]` | | `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` | | `setPricesBySku` | `(googleAuthId: string, sku: string, PriceInput[])` | `ArticleDetail` |
| `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` | | `setEansBySku` | `(googleAuthId: string, sku: string, string[])` | `ArticleDetail` |
| `setBrandIdBySku` | `(googleAuthId: string, sku: string, number \| null)` | `ArticleDetail` | | `setBrandIdBySku` | `(googleAuthId: string, sku: string, number \| null)` | `ArticleDetail` |
| `setWeightBySku` | `(googleAuthId: string, sku: string, number)` | `ArticleDetail` | | `setWeightBySku` | `(googleAuthId: string, sku: string, number)` | `ArticleDetail` |
| `setTrackStockBySku` | `(googleAuthId: string, sku: string, boolean)` | `ArticleDetail` |
| `setCategoryIdsBySku` | `(googleAuthId: string, sku: string, number[])` | `ArticleDetail` | | `setCategoryIdsBySku` | `(googleAuthId: string, sku: string, number[])` | `ArticleDetail` |
| `addImage` | `(googleAuthId: string, sku: string, Buffer)` | `ImageResult` (see below) | | `addImage` | `(googleAuthId: string, sku: string, Buffer)` | `ImageResult` (see below) |
| `deleteImage` | `(googleAuthId: string, sku: string, fullHash: string)` | `void` | | `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 ```ts
type ArticleDetail = { type ArticleDetail = {
@@ -615,6 +616,7 @@ type ArticleDetail = {
slug: string | null, slug: string | null,
unitamount: number, unitamount: number,
weight: number, weight: number,
track_stock: boolean,
unit: { unit: {
si: string, // e.g. "pc" si: string, // e.g. "pc"
basesi: 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`. `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`. `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`. `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/article/:sku/history` | `article.getHistoryBySku(googleAuthId, sku)` |
| `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId)` | | `GET /api/articleIndex` | `article.listSearchIndex(googleAuthId)` |
| `GET /api/article/export.zip` | `article.listExport(googleAuthId, categoryId?)` | | `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/prices` | `article.setPricesBySku(googleAuthId, sku, [{ price, tax, minmultiplier? }])` |
| `PUT /api/article/:sku/eans` | `article.setEansBySku(googleAuthId, sku, string[])` | | `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/brand` | `article.setBrandIdBySku(googleAuthId, sku, brandId \| null)` |
| `PUT /api/article/:sku/weight` | `article.setWeightBySku(googleAuthId, sku, weight)` | | `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[])` | | `PUT /api/article/:sku/categories` | `article.setCategoryIdsBySku(googleAuthId, sku, number[])` |
| `DELETE /api/article/:sku` | `article.delete(googleAuthId, sku)` | | `DELETE /api/article/:sku` | `article.delete(googleAuthId, sku)` |
| `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` | | `POST /api/article/:sku/image` | `article.addImage(googleAuthId, sku, imageBuffer)` |

View File

@@ -313,7 +313,7 @@ function render() {
} }
const SCHEMA_TABLES = [ 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'], ['articleean', 'id PK, article_id->article, ean:s(50) UQ'],
['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'], ['articlehistory', 'id PK, article_id->article, companyuser_id?->companyuser, date:dt, text:txt'],
['articlelist', 'id PK'], ['articlelist', 'id PK'],
@@ -354,7 +354,7 @@ const SCHEMA_TABLES = [
['tax', 'id PK, rate:d(18,4) UQ'], ['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)'], ['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)'], ['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. // Write the index API.md linking to each section file.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 224 KiB

View File

@@ -369,6 +369,11 @@ const MENU = {
label: 'setWeightBySku', label: 'setWeightBySku',
hint: 'googleAuthId, sku, weight → row', hint: 'googleAuthId, sku, weight → row',
}, },
{
value: 'article.setTrackStockBySku',
label: 'setTrackStockBySku',
hint: 'googleAuthId, sku, trackStock (true|false) → row',
},
{ {
value: 'article.setCategoryIdsBySku', value: 'article.setCategoryIdsBySku',
label: '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': { case 'article.setCategoryIdsBySku': {
const googleAuthId = await askInt('google_auth id'); const googleAuthId = await askInt('google_auth id');
if (googleAuthId == null) return; if (googleAuthId == null) return;

View File

@@ -229,25 +229,54 @@ async function run() {
'History should include second line goods receipt with details', 'History should include second line goods receipt with details',
); );
// 9. Test warehouse CRUD and linking with ledger // 9. Test warehouse CRUD, sort, reorder, and linking with ledger
console.log('8. Testing warehouse CRUD and ledger association...'); console.log('8. Testing warehouse CRUD, sort, reorder, and ledger association...');
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
assert.equal(wh1.name, 'Hauptlager'); assert.equal(wh1.name, 'Hauptlager');
assert.equal(wh1.sort, 0);
assert(wh1.id > 0); assert(wh1.id > 0);
const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' }); const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' });
assert.equal(wh2.name, 'Filiale Nord'); assert.equal(wh2.name, 'Filiale Nord');
assert.equal(wh2.sort, 1);
const whList = await store.warehouse.list(googleAuthId); const whList = await store.warehouse.list(googleAuthId);
assert.equal(whList.length, 2); assert.equal(whList.length, 2);
assert(whList.some((w) => w.name === 'Hauptlager')); assert.equal(whList[0].id, wh1.id);
assert(whList.some((w) => w.name === 'Filiale Nord')); 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); const whGet = await store.warehouse.getById(googleAuthId, wh1.id);
assert.equal(whGet.name, 'Hauptlager'); 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.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 // Add ledger row with warehouse
const rowWithWh = await store.ledger.add(googleAuthId, { const rowWithWh = await store.ledger.add(googleAuthId, {

View File

@@ -154,6 +154,37 @@ async function runTests() {
parentId: cat.id, parentId: cat.id,
name: 'Test Article', 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({ const sampleImage = await sharp({
create: { create: {

View File

@@ -28,6 +28,7 @@ import articleSetPricesBySku from './store/article/setPricesBySku.js';
import articleSetEansBySku from './store/article/setEansBySku.js'; import articleSetEansBySku from './store/article/setEansBySku.js';
import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js'; import articleSetBrandIdBySku from './store/article/setBrandIdBySku.js';
import articleSetWeightBySku from './store/article/setWeightBySku.js'; import articleSetWeightBySku from './store/article/setWeightBySku.js';
import articleSetTrackStockBySku from './store/article/setTrackStockBySku.js';
import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js'; import articleSetCategoryIdsBySku from './store/article/setCategoryIdsBySku.js';
import articleSetFatherBySku from './store/article/setFatherBySku.js'; import articleSetFatherBySku from './store/article/setFatherBySku.js';
import articleImportRows from './store/article/importRows.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 warehouseList from './store/warehouse/list.js';
import warehouseUpdate from './store/warehouse/update.js'; import warehouseUpdate from './store/warehouse/update.js';
import warehouseDelete from './store/warehouse/delete.js'; import warehouseDelete from './store/warehouse/delete.js';
import warehouseOrder from './store/warehouse/order.js';
import printerCreate from './store/printer/create.js'; import printerCreate from './store/printer/create.js';
import printerGetById from './store/printer/getById.js'; import printerGetById from './store/printer/getById.js';
import printerList from './store/printer/list.js'; import printerList from './store/printer/list.js';
@@ -194,6 +196,7 @@ export default class LibDashStore {
setEansBySku: articleSetEansBySku(provider), setEansBySku: articleSetEansBySku(provider),
setBrandIdBySku: articleSetBrandIdBySku(provider), setBrandIdBySku: articleSetBrandIdBySku(provider),
setWeightBySku: articleSetWeightBySku(provider), setWeightBySku: articleSetWeightBySku(provider),
setTrackStockBySku: articleSetTrackStockBySku(provider),
setCategoryIdsBySku: articleSetCategoryIdsBySku(provider), setCategoryIdsBySku: articleSetCategoryIdsBySku(provider),
setFatherBySku: articleSetFatherBySku(provider), setFatherBySku: articleSetFatherBySku(provider),
importRows: articleImportRows(provider), importRows: articleImportRows(provider),
@@ -362,6 +365,7 @@ export default class LibDashStore {
list: warehouseList(provider), list: warehouseList(provider),
update: warehouseUpdate(provider), update: warehouseUpdate(provider),
delete: warehouseDelete(provider), delete: warehouseDelete(provider),
order: warehouseOrder(provider),
}; };
this.printer = { this.printer = {

View File

@@ -31,6 +31,7 @@ export const ARTICLE_LIST_COLUMNS = `
at.slug, at.slug,
pr.net, pr.net,
t.rate AS taxRate, t.rate AS taxRate,
a.track_stock,
CASE CASE
WHEN a.optionslist_id IS NOT NULL THEN ( WHEN a.optionslist_id IS NOT NULL THEN (
SELECT COUNT(*) SELECT COUNT(*)
@@ -133,6 +134,12 @@ SET weight = @weight
WHERE id = @articleId WHERE id = @articleId
`; `;
export const UPDATE_TRACK_STOCK = `
UPDATE article
SET track_stock = @trackStock
WHERE id = @articleId
`;
export const UPDATE_BRAND = ` export const UPDATE_BRAND = `
UPDATE article UPDATE article
SET brand_id = @brandId SET brand_id = @brandId
@@ -158,9 +165,9 @@ WHERE id = @articleId
`; `;
export const CREATE = ` 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 OUTPUT INSERTED.id
VALUES (@companyId, @sku, @unitId, @unitamount, @weight) VALUES (@companyId, @sku, @unitId, @unitamount, @weight, @trackStock)
`; `;
export const DELETE_EANS_BY_ARTICLE = ` export const DELETE_EANS_BY_ARTICLE = `
@@ -287,6 +294,7 @@ SELECT
u.si AS unitSi, u.si AS unitSi,
ut.symbol AS unitSymbol, ut.symbol AS unitSymbol,
a.unitamount, a.unitamount,
a.track_stock,
b.name AS brandName, b.name AS brandName,
b.name AS brandName, b.name AS brandName,
pr.net AS net, pr.net AS net,
@@ -371,6 +379,7 @@ SELECT
at.slug, at.slug,
a.unitamount, a.unitamount,
a.weight, a.weight,
a.track_stock,
u.si AS unitSi, u.si AS unitSi,
u.basesi AS unitBasesi, u.basesi AS unitBasesi,
u.basesifactor AS unitBasesifactor, u.basesifactor AS unitBasesifactor,
@@ -467,6 +476,7 @@ SELECT
SELECT SELECT
w.id, w.id,
w.name, w.name,
w.sort,
COALESCE(stock_calc.stock, 0) AS stock COALESCE(stock_calc.stock, 0) AS stock
FROM warehouse w FROM warehouse w
LEFT JOIN ( LEFT JOIN (
@@ -482,7 +492,7 @@ SELECT
GROUP BY l.warehouse_id GROUP BY l.warehouse_id
) stock_calc ON stock_calc.warehouse_id = w.id ) stock_calc ON stock_calc.warehouse_id = w.id
WHERE w.company_id = a.company_id WHERE w.company_id = a.company_id
ORDER BY w.name ASC ORDER BY w.sort ASC, w.name ASC
FOR JSON PATH FOR JSON PATH
) AS warehousesJson ) AS warehousesJson
FROM article a FROM article a

View File

@@ -5,6 +5,7 @@ export const ARTICLE_DETAIL_COLUMNS = `
at.slug, at.slug,
a.unitamount, a.unitamount,
a.weight, a.weight,
a.track_stock,
u.si AS unitSi, u.si AS unitSi,
u.basesi AS unitBasesi, u.basesi AS unitBasesi,
u.basesifactor AS unitBasesifactor, u.basesifactor AS unitBasesifactor,

View File

@@ -2,17 +2,19 @@ export const LIST_BY_COMPANY = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE company_id = @companyId WHERE company_id = @companyId
ORDER BY name ASC; ORDER BY sort ASC, name ASC;
`; `;
export const GET_BY_COMPANY_AND_ID = ` export const GET_BY_COMPANY_AND_ID = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE id = @id AND company_id = @companyId; WHERE id = @id AND company_id = @companyId;
`; `;
@@ -21,20 +23,39 @@ export const GET_BY_COMPANY_AND_NAME = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE company_id = @companyId AND name = @name; 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 = ` export const CREATE = `
INSERT INTO warehouse (company_id, name) INSERT INTO warehouse (company_id, name, sort)
OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name, INSERTED.sort
VALUES (@companyId, @name); VALUES (@companyId, @name, @sort);
`; `;
export const UPDATE_BY_ID = ` export const UPDATE_BY_ID = `
UPDATE warehouse 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; WHERE id = @id AND company_id = @companyId;
`; `;

View File

@@ -317,6 +317,7 @@ BEGIN
unit_id INT NOT NULL, unit_id INT NOT NULL,
unitamount FLOAT NOT NULL, unitamount FLOAT NOT NULL,
weight FLOAT NOT NULL, weight FLOAT NOT NULL,
track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1,
brand_id INT NULL, brand_id INT NULL,
articlelist_id INT NULL, articlelist_id INT NULL,
optionslist_id INT NULL, optionslist_id INT NULL,
@@ -340,6 +341,10 @@ BEGIN
ON article (fatherarticle_id, fathersort) ON article (fatherarticle_id, fathersort)
WHERE fatherarticle_id IS NOT NULL; WHERE fatherarticle_id IS NOT NULL;
END 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 = ` export const INIT_ARTICLEHISTORY = `
@@ -599,10 +604,15 @@ BEGIN
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL, company_id INT NOT NULL,
name NVARCHAR(255) 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 FK_warehouse_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE,
CONSTRAINT UQ_warehouse_company_name UNIQUE (company_id, name) CONSTRAINT UQ_warehouse_company_name UNIQUE (company_id, name)
); );
END 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 = ` export const INIT_LEDGER = `

View File

@@ -399,6 +399,20 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
`ALTER TABLE ledger ADD COLUMN warehouse_id INTEGER REFERENCES warehouse(id) ON DELETE NO ACTION`, `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 { } catch {
// ignore // ignore
} }

View File

@@ -8,7 +8,7 @@ export const {
GET_BRAND_BY_COMPANY_AND_ID, GET_BRAND_BY_COMPANY_AND_ID,
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU, GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
SET_FATHER_SORT, 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, UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
} = queries; } = queries;
@@ -83,7 +83,7 @@ WITH RECURSIVE category_tree AS (
INNER JOIN category_tree ct ON c.parent_id = ct.id 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, 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, pr.net, t.rate AS taxRate,
${JSON_EANS} AS eansJson, ${JSON_EANS} AS eansJson,
${JSON_CATEGORIES} AS categoryIdsJson, ${JSON_CATEGORIES} AS categoryIdsJson,
@@ -122,6 +122,7 @@ SELECT auth.*,
json_object( json_object(
'id', a.id, 'sku', a.sku, 'name', at.name, 'slug', at.slug, 'id', a.id, 'sku', a.sku, 'name', at.name, 'slug', at.slug,
'unitamount', a.unitamount, 'weight', a.weight, 'unitamount', a.unitamount, 'weight', a.weight,
'track_stock', a.track_stock,
'unitSi', u.si, 'unitBasesi', u.basesi, 'unitSi', u.si, 'unitBasesi', u.basesi,
'unitBasesifactor', u.basesifactor, 'unitSymbol', ut.symbol, 'unitBasesifactor', u.basesifactor, 'unitSymbol', ut.symbol,
'unitName', ut.name, 'unitName', ut.name,
@@ -169,7 +170,7 @@ SELECT auth.*,
WHERE s.article_id = a.id ORDER BY s.sort, s.id WHERE s.article_id = a.id ORDER BY s.sort, s.id
))), ))),
'warehousesJson', json((SELECT COALESCE(json_group_array(json_object( '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 )), '[]') FROM warehouse w
LEFT JOIN ( LEFT JOIN (
SELECT l.warehouse_id, SUM(l.amount) AS stock SELECT l.warehouse_id, SUM(l.amount) AS stock
@@ -184,7 +185,7 @@ SELECT auth.*,
GROUP BY l.warehouse_id GROUP BY l.warehouse_id
) stock_calc ON stock_calc.warehouse_id = w.id ) stock_calc ON stock_calc.warehouse_id = w.id
WHERE w.company_id = a.company_id WHERE w.company_id = a.company_id
ORDER BY w.name ASC)) ORDER BY w.sort ASC, w.name ASC))
) AS articleJson ) AS articleJson
FROM auth FROM auth
INNER JOIN article a ON a.id = auth.articleId INNER JOIN article a ON a.id = auth.articleId

View File

@@ -2,17 +2,19 @@ export const LIST_BY_COMPANY = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE company_id = @companyId WHERE company_id = @companyId
ORDER BY name ASC; ORDER BY sort ASC, name ASC;
`; `;
export const GET_BY_COMPANY_AND_ID = ` export const GET_BY_COMPANY_AND_ID = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE id = @id AND company_id = @companyId; WHERE id = @id AND company_id = @companyId;
`; `;
@@ -21,20 +23,39 @@ export const GET_BY_COMPANY_AND_NAME = `
SELECT SELECT
id, id,
company_id, company_id,
name name,
sort
FROM warehouse FROM warehouse
WHERE company_id = @companyId AND name = @name; 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 = ` export const CREATE = `
INSERT INTO warehouse (company_id, name) INSERT INTO warehouse (company_id, name, sort)
VALUES (@companyId, @name) VALUES (@companyId, @name, @sort)
RETURNING id, company_id, name; RETURNING id, company_id, name, sort;
`; `;
export const UPDATE_BY_ID = ` export const UPDATE_BY_ID = `
UPDATE warehouse 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; WHERE id = @id AND company_id = @companyId;
`; `;

View File

@@ -216,6 +216,7 @@ CREATE TABLE IF NOT EXISTS article (
unit_id INTEGER NOT NULL, unit_id INTEGER NOT NULL,
unitamount REAL NOT NULL, unitamount REAL NOT NULL,
weight REAL NOT NULL, weight REAL NOT NULL,
track_stock INTEGER NOT NULL DEFAULT 1,
brand_id INTEGER, brand_id INTEGER,
articlelist_id INTEGER, articlelist_id INTEGER,
optionslist_id INTEGER, optionslist_id INTEGER,
@@ -431,6 +432,7 @@ CREATE TABLE IF NOT EXISTS warehouse (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
name TEXT NOT NULL COLLATE NOCASE, name TEXT NOT NULL COLLATE NOCASE,
sort INTEGER NOT NULL DEFAULT 0,
UNIQUE (company_id, name), UNIQUE (company_id, name),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
); );

View File

@@ -32,14 +32,18 @@ export default function createUnderParent(provider) {
* @param {number} input.googleAuthId - The id of the Google auth user. * @param {number} input.googleAuthId - The id of the Google auth user.
* @param {number} input.parentId - The id of the parent category. * @param {number} input.parentId - The id of the parent category.
* @param {string} input.name - The article name. * @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. * @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 * @example
* const article = await store.article.createUnderParent({ googleAuthId: 42, parentId: 5, name: 'Cola' }); * 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 runCreate = async (runner) => {
const { companyId, languageId, companyuserId } = await access( const { companyId, languageId, companyuserId } = await access(
googleAuthId, googleAuthId,
@@ -59,6 +63,7 @@ export default function createUnderParent(provider) {
unitId, unitId,
unitamount: 1, unitamount: 1,
weight: 0, weight: 0,
trackStock: finalTrackStock,
}); });
} catch (err) { } catch (err) {
if (isUniqueConstraintError(err)) { if (isUniqueConstraintError(err)) {
@@ -92,6 +97,7 @@ export default function createUnderParent(provider) {
parentId, parentId,
name, name,
sku, sku,
track_stock: Boolean(finalTrackStock),
}; };
}; };

View File

@@ -18,7 +18,7 @@ export default function getBySku(provider) {
* @param {number} googleAuthId - The id of the Google auth user. * @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU. * @param {string} sku - The article SKU.
* @param {object} [tx] - Optional transaction. * @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. * @throws {Error} If the article is not found or the user is not authorized.
* *
* @example * @example
@@ -56,6 +56,7 @@ export default function getBySku(provider) {
slug: a.slug, slug: a.slug,
unitamount: a.unitamount, unitamount: a.unitamount,
weight: a.weight, weight: a.weight,
track_stock: Boolean(a.track_stock ?? 1),
unit: { unit: {
si: a.unitSi, si: a.unitSi,
basesi: a.unitBasesi, basesi: a.unitBasesi,
@@ -92,6 +93,7 @@ export default function getBySku(provider) {
warehouses: parseJsonArray(a.warehousesJson).map((w) => ({ warehouses: parseJsonArray(a.warehousesJson).map((w) => ({
id: w.id, id: w.id,
name: w.name, name: w.name,
sort: Number(w.sort ?? 0),
stock: Number(w.stock ?? 0), stock: Number(w.stock ?? 0),
})), })),
optionsCount: a.optionsCount, optionsCount: a.optionsCount,

View File

@@ -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[].net] - The POS net price (accepts German decimals).
* @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`). * @param {number|string} [input.rows[].tax] - The tax rate in percent (e.g. `19`).
* @param {Array<string>|string} [input.rows[].eans] - The EANs to set. * @param {Array<string>|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 {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 {number|string} [input.rows[].deposit] - The deposit (Pfand) value; currently ignored.
* @param {object} [tx] - Optional transaction. * @param {object} [tx] - Optional transaction.
@@ -168,12 +169,16 @@ export default function importRows(provider) {
const unitId = await ensureUnit(runner, companyId, languageId); const unitId = await ensureUnit(runner, companyId, languageId);
const slug = slugify(name); 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, { const article = await runner.query(articleQueries.CREATE, {
companyId, companyId,
sku, sku,
unitId, unitId,
unitamount: 1, unitamount: 1,
weight: 0, weight: 0,
trackStock,
}); });
const articleId = article.rows[0]?.id ?? article.insertId; const articleId = article.rows[0]?.id ?? article.insertId;

View File

@@ -94,6 +94,7 @@ export default function listExport(provider) {
unitSi: row.unitSi, unitSi: row.unitSi,
unitSymbol: row.unitSymbol, unitSymbol: row.unitSymbol,
unitamount: row.unitamount, unitamount: row.unitamount,
track_stock: Boolean(row.track_stock ?? 1),
brandName: row.brandName, brandName: row.brandName,
net: row.net, net: row.net,
taxRate: row.taxRate, taxRate: row.taxRate,

View File

@@ -10,6 +10,7 @@ function shapeArticleDetail(row, categoryIds, photos, eans, prices) {
slug: row.slug, slug: row.slug,
unitamount: row.unitamount, unitamount: row.unitamount,
weight: row.weight, weight: row.weight,
track_stock: Boolean(row.track_stock ?? 1),
unit: { unit: {
si: row.unitSi, si: row.unitSi,
basesi: row.unitBasesi, basesi: row.unitBasesi,

View File

@@ -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<object>} 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;
};
}

View File

@@ -41,20 +41,24 @@ export default function updateBySku(provider) {
* @param {string} [patch.name] - The new article name. * @param {string} [patch.name] - The new article name.
* @param {string} [patch.unitSi] - The new unit SI code. * @param {string} [patch.unitSi] - The new unit SI code.
* @param {number} [patch.unitamount] - The new unit amount (must be > 0). * @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. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<object>} The updated article. * @returns {Promise<object>} The updated article.
* @throws {Error} If no valid field is provided or the article is not found. * @throws {Error} If no valid field is provided or the article is not found.
* *
* @example * @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, ... } * // => { id, sku, name, ... }
*/ */
return async (googleAuthId, sku, patch = {}, tx) => { return async (googleAuthId, sku, patch = {}, tx) => {
const hasName = Object.prototype.hasOwnProperty.call(patch, 'name'); const hasName = Object.prototype.hasOwnProperty.call(patch, 'name');
const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount'); const hasUnitamount = Object.prototype.hasOwnProperty.call(patch, 'unitamount');
const hasUnitSi = Object.prototype.hasOwnProperty.call(patch, 'unitSi'); 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'); throw new Error('Name required');
} }
@@ -66,6 +70,13 @@ export default function updateBySku(provider) {
throw new Error('Invalid unitamount'); 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 runUpdate = async (runner) => {
const { articleId, languageId, companyId, companyuserId } = await resolve( const { articleId, languageId, companyId, companyuserId } = await resolve(
googleAuthId, googleAuthId,
@@ -122,6 +133,15 @@ export default function updateBySku(provider) {
historyLines.push(`Einheit geändert von "${oldLabel}" auf "${newLabel}"`); 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) { if (hasName) {
const slug = slugify(patch.name); 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) { if (historyLines.length) {
await runner.query(insertHistorySql, { await runner.query(insertHistorySql, {
articleId, articleId,

View File

@@ -24,6 +24,7 @@ export default function queryCategoryArticles(provider) {
} = row; } = row;
return { return {
...rest, ...rest,
track_stock: Boolean(rest.track_stock ?? 1),
brand: brandName brand: brandName
? { ? {
id: brandId ?? null, id: brandId ?? null,

View File

@@ -1,17 +1,24 @@
import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/warehouse.js'; import {
import { CREATE as SQLITE_CREATE } from '../../providers/sqlite3/queries/warehouse.js'; 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 { isUniqueConstraintError } from '../../providers/provider.js';
import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
function shape(row) { 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) { export default function create(provider) {
const access = resolveSelectedCompanyAccess(provider); const access = resolveSelectedCompanyAccess(provider);
const sql = const isSqlite = provider?.dialect === 'sqlite3';
provider?.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; 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. * 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 {number} googleAuthId - The id of the Google auth user.
* @param {object} input - Warehouse input. * @param {object} input - Warehouse input.
* @param {string} input.name - The warehouse name. * @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. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string}>} The created warehouse. * @returns {Promise<{id: number, name: string, sort: number}>} The created warehouse.
* @throws {Error} If name is missing or already exists. * @throws {Error} If name is missing, sort is invalid, or name already exists.
* *
* @example * @example
* const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); * 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() : ''; const cleaned = typeof name === 'string' ? name.trim() : '';
if (!cleaned) { if (!cleaned) {
throw new Error('Name required'); throw new Error('Name required');
@@ -36,13 +44,27 @@ export default function create(provider) {
const runner = makeRunner(provider, tx); const runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner); 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 { try {
const result = await runner.query(sql, { const result = await runner.query(sql, {
companyId, companyId,
name: cleaned, 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); return shape(row);
} catch (err) { } catch (err) {
if ( if (

View File

@@ -4,7 +4,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
function shape(row) { 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) { 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} googleAuthId - The id of the Google auth user.
* @param {number} id - The warehouse ID. * @param {number} id - The warehouse ID.
* @param {object} [tx] - Optional transaction to run within. * @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 * @example
* const wh = await store.warehouse.getById(googleAuthId, 1); * const wh = await store.warehouse.getById(googleAuthId, 1);
* // => { id: 1, name: 'Hauptlager' } * // => { id: 1, name: 'Hauptlager', sort: 0 }
*/ */
return async (googleAuthId, id, tx) => { return async (googleAuthId, id, tx) => {
const numericId = Number(id); const numericId = Number(id);

View File

@@ -4,7 +4,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
function shape(row) { 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) { 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 {number} googleAuthId - The id of the Google auth user.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<Array<{id: number, name: string}>>} List of warehouses. * @returns {Promise<Array<{id: number, name: string, sort: number}>>} List of warehouses.
* *
* @example * @example
* const warehouses = await store.warehouse.list(googleAuthId); * const warehouses = await store.warehouse.list(googleAuthId);
* // => [{ id: 1, name: 'Hauptlager' }] * // => [{ id: 1, name: 'Hauptlager', sort: 0 }]
*/ */
return async (googleAuthId, tx) => { return async (googleAuthId, tx) => {
const runner = makeRunner(provider, tx); const runner = makeRunner(provider, tx);

View File

@@ -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<number>} input.warehouseIds - The warehouse ids in their new order.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{warehouseIds: Array<number>}>} 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));
};
}

View File

@@ -11,7 +11,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
function shape(row) { 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) { export default function update(provider) {
@@ -21,47 +21,66 @@ export default function update(provider) {
const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; 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} googleAuthId - The id of the Google auth user.
* @param {number} id - The warehouse ID to update. * @param {number} id - The warehouse ID to update.
* @param {object} input - Update payload. * @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. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, name: string}>} The updated warehouse. * @returns {Promise<{id: number, name: string, sort: number}>} The updated warehouse.
* @throws {Error} If warehouse not found, name is missing, or name already exists. * @throws {Error} If warehouse not found, name is empty, sort is invalid, or name already exists.
* *
* @example * @example
* const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); * const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager', sort: 1 });
* // => { id: 1, name: 'Zentrallager' } * // => { id: 1, name: 'Zentrallager', sort: 1 }
*/ */
return async (googleAuthId, id, { name } = {}, tx) => { return async (googleAuthId, id, input = {}, tx) => {
const numericId = Number(id); const numericId = Number(id);
if (!Number.isInteger(numericId) || numericId <= 0) { if (!Number.isInteger(numericId) || numericId <= 0) {
throw new Error('Warehouse not found'); 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 runner = makeRunner(provider, tx);
const { companyId } = await access(googleAuthId, runner); const { companyId } = await access(googleAuthId, runner);
const existing = await runner.query(getSql, { id: numericId, companyId }); 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'); 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 { try {
await runner.query(updateSql, { await runner.query(updateSql, {
id: numericId, id: numericId,
companyId, companyId,
name: cleaned, name,
sort,
}); });
return shape({ id: numericId, name: cleaned }); return shape({ id: numericId, name, sort });
} catch (err) { } catch (err) {
if ( if (
isUniqueConstraintError(err) || isUniqueConstraintError(err) ||