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.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>|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<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`
*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, ... }
```

View File

@@ -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<object>}>`
## `brand.delete`
*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`*
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**

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
- **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<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}>>`
- `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>>`
@@ -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>`
- `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>`
- `setTrackStockBySku(googleAuthId: number, sku: string, trackStock: boolean|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)
- `create(iso639: string, tx?: object): Promise<number>`
- `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}>>`
- `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<object>}>`
- `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}>}>>`
- `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}>>`
- `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)
- `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>`
- `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string}|undefined>`
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string}>>`
- `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<Array<{id: number, name: string, sort: number}>>`
- `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)
- `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)

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. |
| `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<Array<{id: number, name: string}>>` — List of warehouses.
`Promise<Array<{id: number, name: string, sort: number}>>` — 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<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`
*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 }
```

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) |
| `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)` |

View File

@@ -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.

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',
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;

View File

@@ -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, {

View File

@@ -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: {

View File

@@ -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 = {

View File

@@ -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

View File

@@ -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,

View File

@@ -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;
`;

View File

@@ -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 = `

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`,
);
}
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
}

View File

@@ -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

View File

@@ -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;
`;

View File

@@ -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
);

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.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),
};
};

View File

@@ -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,

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[].tax] - The tax rate in percent (e.g. `19`).
* @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 {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;

View File

@@ -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,

View File

@@ -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,

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.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<object>} 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,

View File

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

View File

@@ -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 (

View File

@@ -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);

View File

@@ -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<Array<{id: number, name: string}>>} List of warehouses.
* @returns {Promise<Array<{id: number, name: string, sort: number}>>} 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);

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';
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) ||