From e7f92ebef8cbe25aa8072e4a0d87c5ad38469778 Mon Sep 17 00:00:00 2001 From: seb Date: Sat, 26 Sep 2026 23:06:05 +0200 Subject: [PATCH] u --- API.ledger.md | 24 +- API.md | 24 +- API.printer.md | 165 + API.stock.md | 28 + API.warehouse.md | 141 + generate-api-doc.js | 6 +- generate-schema-graph.js | 4 +- schema-graph.dot | 151 +- schema-graph.svg | 3384 ++++++++++--------- scripts/test-ledger.js | 63 + src/index.js | 34 + src/providers/mssql/index.js | 12 + src/providers/mssql/purgeSql.js | 2 + src/providers/mssql/queries/article.js | 24 +- src/providers/mssql/queries/ledger.js | 14 + src/providers/mssql/queries/printer.js | 99 + src/providers/mssql/queries/warehouse.js | 48 + src/providers/mssql/schema.js | 38 +- src/providers/sqlite3/index.js | 7 +- src/providers/sqlite3/queries/article.js | 19 +- src/providers/sqlite3/queries/ledger.js | 14 + src/providers/sqlite3/queries/printer.js | 15 + src/providers/sqlite3/queries/warehouse.js | 48 + src/providers/sqlite3/schema.js | 31 +- src/store/article/getBySku.js | 5 + src/store/ledger/add.js | 36 +- src/store/ledger/listByManifestarticleId.js | 18 +- src/store/ledger/listBySku.js | 16 +- src/store/numbers/list.js | 2 +- src/store/printer/create.js | 111 + src/store/printer/delete.js | 38 + src/store/printer/getById.js | 53 + src/store/printer/list.js | 44 + src/store/printer/order.js | 76 + src/store/printer/update.js | 124 + src/store/stock/adjust.js | 190 ++ src/store/warehouse/create.js | 59 + src/store/warehouse/delete.js | 62 + src/store/warehouse/getById.js | 44 + src/store/warehouse/list.js | 33 + src/store/warehouse/update.js | 77 + 41 files changed, 3632 insertions(+), 1751 deletions(-) create mode 100644 API.printer.md create mode 100644 API.stock.md create mode 100644 API.warehouse.md create mode 100644 src/providers/mssql/queries/printer.js create mode 100644 src/providers/mssql/queries/warehouse.js create mode 100644 src/providers/sqlite3/queries/printer.js create mode 100644 src/providers/sqlite3/queries/warehouse.js create mode 100644 src/store/printer/create.js create mode 100644 src/store/printer/delete.js create mode 100644 src/store/printer/getById.js create mode 100644 src/store/printer/list.js create mode 100644 src/store/printer/order.js create mode 100644 src/store/printer/update.js create mode 100644 src/store/stock/adjust.js create mode 100644 src/store/warehouse/create.js create mode 100644 src/store/warehouse/delete.js create mode 100644 src/store/warehouse/getById.js create mode 100644 src/store/warehouse/list.js create mode 100644 src/store/warehouse/update.js diff --git a/API.ledger.md b/API.ledger.md index 7f44d6f..3d5f326 100644 --- a/API.ledger.md +++ b/API.ledger.md @@ -15,6 +15,7 @@ Appends a new append-only entry to the ledger table. | `googleAuthId` | `number` | The id of the Google auth user. | | `input` | `object` | Ledger entry payload. | | `input.manifestarticle_id` | `number` | ID of the referenced manifest line (can also pass manifestarticleId). | +| `[input.warehouse_id]` | `number|null` | Optional warehouse ID (can also pass warehouseId). | | `input.transaction_type` | `string` | Transaction type (can also pass transactionType). | | `input.amount` | `number` | Movement amount. | | `input.transaction_text` | `string` | Transaction memo/details (can also pass transactionText). | @@ -22,7 +23,7 @@ Appends a new append-only entry to the ledger table. **Returns** -`Promise<{id: number, manifestarticle_id: number, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The created ledger row. +`Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>` — The created ledger row. **Throws** @@ -33,7 +34,8 @@ Appends a new append-only entry to the ledger table. ```js const entry = await store.ledger.add(googleAuthId, { manifestarticle_id: 1, - transaction_type: 'PAYMENT', + warehouse_id: 1, + transaction_type: 'DELIVERY', amount: 100.5, transaction_text: 'Invoice paid', }); @@ -43,7 +45,7 @@ const entry = await store.ledger.add(googleAuthId, { *Source: `src/store/ledger/listByManifestarticleId.js`* -Lists ledger entries for a specific manifest article line, optionally filtered by transaction type. +Lists ledger entries for a specific manifest article line, optionally filtered by transaction type and/or warehouse. **Parameters** @@ -51,12 +53,12 @@ Lists ledger entries for a specific manifest article line, optionally filtered b | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | | `manifestarticleId` | `number` | The id of the manifest article line. | -| `[optionsOrType]` | `string|object` | Transaction type string or options object { transaction_type?: string, transactionType?: string }. | +| `[optionsOrType]` | `string|object` | Transaction type string or options object { transaction_type?: string, transactionType?: string, warehouse_id?: number, warehouseId?: number }. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise>` +`Promise>` **Throws** @@ -65,15 +67,15 @@ Lists ledger entries for a specific manifest article line, optionally filtered b **Example** ```js -const entries = await store.ledger.listByManifestarticleId(googleAuthId, 42, 'PAYMENT'); -// => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] +const entries = await store.ledger.listByManifestarticleId(googleAuthId, 42, 'DELIVERY'); +// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] ``` ## `ledger.listBySku` *Source: `src/store/ledger/listBySku.js`* -Lists ledger entries for a given article SKU, optionally filtered by transaction type. +Lists ledger entries for a given article SKU, optionally filtered by transaction type and/or warehouse. **Parameters** @@ -81,12 +83,12 @@ Lists ledger entries for a given article SKU, optionally filtered by transaction | --- | --- | --- | | `googleAuthId` | `number` | The id of the Google auth user. | | `sku` | `string` | The article SKU. | -| `[optionsOrType]` | `string|object` | Transaction type string or options object { transaction_type?: string, transactionType?: string }. | +| `[optionsOrType]` | `string|object` | Transaction type string or options object { transaction_type?: string, transactionType?: string, warehouse_id?: number, warehouseId?: number }. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise>` +`Promise>` **Throws** @@ -96,5 +98,5 @@ Lists ledger entries for a given article SKU, optionally filtered by transaction ```js const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY'); -// => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] +// => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] ``` diff --git a/API.md b/API.md index 872ebd3..601db82 100644 --- a/API.md +++ b/API.md @@ -33,7 +33,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - **file**: `id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt` - **google_auth**: `id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company` - **language**: `id PK, iso639:s(10) UQ` -- **ledger**: `id PK, manifestarticle_id->manifestarticle, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]` +- **ledger**: `id PK, manifestarticle_id->manifestarticle, warehouse_id?->warehouse, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]` - **manifest**: `id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt` - **manifestarticle**: `id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0` - **manifestattachment**: `id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)` @@ -49,6 +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)` ## Table of Contents @@ -171,9 +172,17 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `order(googleAuthId: number, manifestId: number, input: {ids?: Array}, tx?: object): Promise<{ids: Array}>` - `remove(googleAuthId: number, manifestId: number, id: number, tx?: object): Promise` - [ledger](API.ledger.md) - - `add(googleAuthId: number, input: {manifestarticle_id: number, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, 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>` - - `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise>` + - `add(googleAuthId: number, input: {manifestarticle_id: number, warehouse_id?: number|null, transaction_type: string, amount: number, transaction_text: string}, tx?: object): Promise<{id: number, manifestarticle_id: number, warehouse_id: number|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>` + - `listBySku(googleAuthId: number, sku: string, optionsOrType?: string|object, tx?: object): Promise>` +- [warehouse](API.warehouse.md) + - `create(googleAuthId: number, input: {name: string}, tx?: object): Promise<{id: number, name: string}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, name: string}|undefined>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `update(googleAuthId: number, id: number, input: {name: string}, tx?: object): Promise<{id: number, name: string}>` +- [stock](API.stock.md) + - `adjust(googleAuthId: number, input: {sku?: string, warehouseId?: number, amount?: number, text?: string, externalNumber?: string, date?: string|Date, lines?: Array}, tx?: object): Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array, ledger: Array}>` - [posterminal](API.posterminal.md) - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `list(googleAuthId: number, tx?: object): Promise>` @@ -182,6 +191,13 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string}>` +- [printer](API.printer.md) + - `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` + - `delete(googleAuthId: number, id: number, tx?: object): Promise` + - `getById(googleAuthId: number, id: number, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}|undefined>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `order(googleAuthId: number, input: {printerIds: Array}, tx?: object): Promise<{printerIds: Array}>` + - `update(googleAuthId: number, id: number, input: {address?: string, type?: string, model?: string|null, serial?: string|null, sort?: number, desc?: string|null}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` - [google_auth](API.google_auth.md) - `add(email: string): Promise` - `getByEmail(email: string): Promise` diff --git a/API.printer.md b/API.printer.md new file mode 100644 index 0000000..ff95967 --- /dev/null +++ b/API.printer.md @@ -0,0 +1,165 @@ +# printer + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `printer.create` + +*Source: `src/store/printer/create.js`* + +Creates a new printer for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Printer input. | +| `input.address` | `string` | Printer address (network IP/host/path). | +| `input.type` | `string` | Printer type ("esc/pos" or "zpl"). | +| `[input.model]` | `string` | Optional printer model name. | +| `[input.serial]` | `string` | Optional printer serial number. | +| `[input.sort]` | `number` | Optional sort order (auto-increments if omitted). | +| `[input.desc]` | `string` | Optional description. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` + +**Throws** + +`Error` — If address is missing, type is invalid, or sort is invalid. + +**Example** + +```js +const printer = await store.printer.create(googleAuthId, { + address: '192.168.1.200', + type: 'esc/pos', + model: 'TM-T88VI', + desc: 'Bar receipt printer', +}); +``` + +## `printer.delete` + +*Source: `src/store/printer/delete.js`* + +Deletes a printer for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The printer ID to delete. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If printer is not found. + +## `printer.getById` + +*Source: `src/store/printer/getById.js`* + +Retrieves a printer by its ID for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The printer ID. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}|undefined>` + +**Example** + +```js +const printer = await store.printer.getById(googleAuthId, 1); +// => { id: 1, company_id: 1, address: '192.168.1.100', type: 'esc/pos', ... } +``` + +## `printer.list` + +*Source: `src/store/printer/list.js`* + +Lists all printers for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` + +**Example** + +```js +const printers = await store.printer.list(42); +// => [{ id: 1, company_id: 1, address: '192.168.1.100', type: 'esc/pos', ... }] +``` + +## `printer.order` + +*Source: `src/store/printer/order.js`* + +Reorders the printers 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.printerIds` | `Array` | The printer ids in their new order. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{printerIds: Array}>` — The applied order. + +**Throws** + +`Error` — If the order is invalid. + +## `printer.update` + +*Source: `src/store/printer/update.js`* + +Updates a printer for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The printer ID to update. | +| `input` | `object` | Update payload. | +| `[input.address]` | `string` | The printer address. | +| `[input.type]` | `string` | The printer type ("esc/pos" or "zpl"). | +| `[input.model]` | `string|null` | The printer model. | +| `[input.serial]` | `string|null` | The printer serial. | +| `[input.sort]` | `number` | The sort order. | +| `[input.desc]` | `string|null` | The description. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` + +**Throws** + +`Error` — If printer not found, address is empty, type is invalid, or sort is invalid. diff --git a/API.stock.md b/API.stock.md new file mode 100644 index 0000000..6783af1 --- /dev/null +++ b/API.stock.md @@ -0,0 +1,28 @@ +# stock + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `stock.adjust` + +*Source: `src/store/stock/adjust.js`* + +Adjusts article stock manually by creating an implicit manifest of type "adjustment", allocating a number from the "adjustment" number scheme, and appending to the ledger. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Adjustment payload. | +| `[input.sku]` | `string` | Article SKU (if single-item adjustment). | +| `[input.warehouseId]` | `number` | Warehouse ID (can also pass warehouse_id). | +| `[input.amount]` | `number` | Signed adjustment amount (+ to add, - to remove). | +| `[input.text]` | `string` | Optional adjustment reason/memo. | +| `[input.externalNumber]` | `string` | Optional external reference. | +| `[input.date]` | `string|Date` | Optional date (defaults to now). | +| `[input.lines]` | `Array` | Optional array of items to adjust in one manifest. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array, ledger: Array}>` diff --git a/API.warehouse.md b/API.warehouse.md new file mode 100644 index 0000000..3cc7cc3 --- /dev/null +++ b/API.warehouse.md @@ -0,0 +1,141 @@ +# warehouse + +This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`. + +## `warehouse.create` + +*Source: `src/store/warehouse/create.js`* + +Creates a new warehouse for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `input` | `object` | Warehouse input. | +| `input.name` | `string` | The warehouse name. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, name: string}>` — The created warehouse. + +**Throws** + +`Error` — If name is missing or already exists. + +**Example** + +```js +const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); +// => { id: 1, name: 'Hauptlager' } +``` + +## `warehouse.delete` + +*Source: `src/store/warehouse/delete.js`* + +Deletes a warehouse for the user's selected company, unless referenced in the ledger. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The warehouse ID to delete. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` + +**Throws** + +`Error` — If warehouse not found or is in use in the ledger. + +**Example** + +```js +await store.warehouse.delete(googleAuthId, 1); +``` + +## `warehouse.getById` + +*Source: `src/store/warehouse/getById.js`* + +Retrieves a warehouse by its ID for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `id` | `number` | The warehouse ID. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, name: string}|undefined>` — The warehouse or undefined. + +**Example** + +```js +const wh = await store.warehouse.getById(googleAuthId, 1); +// => { id: 1, name: 'Hauptlager' } +``` + +## `warehouse.list` + +*Source: `src/store/warehouse/list.js`* + +Lists all warehouses for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `googleAuthId` | `number` | The id of the Google auth user. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise>` — List of warehouses. + +**Example** + +```js +const warehouses = await store.warehouse.list(googleAuthId); +// => [{ id: 1, name: 'Hauptlager' }] +``` + +## `warehouse.update` + +*Source: `src/store/warehouse/update.js`* + +Updates a warehouse name for the user's selected company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `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. | +| `[tx]` | `object` | 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. + +**Example** + +```js +const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); +// => { id: 1, name: 'Zentrallager' } +``` diff --git a/generate-api-doc.js b/generate-api-doc.js index 9f1c10d..9bc25bc 100644 --- a/generate-api-doc.js +++ b/generate-api-doc.js @@ -29,7 +29,10 @@ const MODULE_ORDER = [ 'order', 'manifestattachment', 'ledger', + 'warehouse', + 'stock', 'posterminal', + 'printer', 'google_auth', 'pic', 'numbers', @@ -335,7 +338,7 @@ const SCHEMA_TABLES = [ ['file', 'id PK, hash:s(64), mimetype:s(255), name:s(255), size:bi, created:dt, modified:dt, content:bin, thumbavif?:bin, thumbx?:i, thumby?:i, meta?:txt'], ['google_auth', 'id PK, email:s(320) UQ, picture?:s(2048), picturebinary?:bin, sub?:s(255) UQ, created_at:dt, selected_company?->company'], ['language', 'id PK, iso639:s(10) UQ'], - ['ledger', 'id PK, manifestarticle_id->manifestarticle, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]'], + ['ledger', 'id PK, manifestarticle_id->manifestarticle, warehouse_id?->warehouse, transaction_type:s(100), amount:f, transaction_text:txt, hash_version:i, prev_hash?:s(128) [append-only]'], ['manifest', 'id PK, company_id->company, number:s(100), type:s(100), entity_id?->entity, external_number?:s(100), date:dt, created_at:dt'], ['manifestarticle', 'id PK, manifest_id->manifest, sku?:s(100), name?:s(255), pu:f=1, amount:f, net:f, comment?:txt, rebate:f=0, taxrate:f=0'], ['manifestattachment', 'id PK, manifest_id->manifest, file_id->file, sort:i=0, UQ(manifest_id, sort)'], @@ -351,6 +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)'], ]; // Write the index API.md linking to each section file. diff --git a/generate-schema-graph.js b/generate-schema-graph.js index dadb7a6..5750f13 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -366,7 +366,7 @@ const CLUSTERS = [ { id: 'manifests', label: 'Documents', - tables: ['manifest', 'manifestarticle', 'manifestattachment', 'ledger'], + tables: ['manifest', 'manifestarticle', 'manifestattachment', 'ledger', 'warehouse'], }, { id: 'commerce', @@ -374,7 +374,7 @@ const CLUSTERS = [ tables: ['tax', 'price'], }, { id: 'units', label: 'Units', tables: ['unit'] }, - { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory'] }, + { id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer'] }, ]; const COLORS = { diff --git a/schema-graph.dot b/schema-graph.dot index 2bba96b..cf04835 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -16,10 +16,10 @@ digraph schema { entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
number?NVARCHAR(100)
created_atDATETIME2
updated_atDATETIME2
>]; companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
sortINT
>]; + company [label=<
company
idINT PK
poscustomergroup_idINT FK
root_category_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
writeonlyBIT
>]; - company [label=<
company
idINT PK
poscustomergroup_idINT FK
default_language_id?INT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
root_category_id?INT FK
nameNVARCHAR(255)
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { @@ -27,51 +27,52 @@ digraph schema { style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; - unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; - categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; - articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + articletext [label=<
articletext
idINT PK
article_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + categorytext [label=<
categorytext
idINT PK
category_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + unittext [label=<
unittext
idINT PK
unit_idINT FK
language_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; } subgraph cluster_catalog { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; - category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; - categoryarticle [label=<
categoryarticle
idINT PK
category_idINT FK
article_idINT FK
>]; - articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; - article [label=<
article
idINT PK
company_idINT FK
unit_idINT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
price_id?INT FK
article_idINT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; - articlelist [label=<
articlelist
idINT PK
>]; - articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; - optionslist [label=<
optionslist
idINT PK
>]; - optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + articlelist [label=<
articlelist
idINT PK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; + optionslist [label=<
optionslist
idINT PK
>]; + optionslistarticle [label=<
optionslistarticle
idINT PK
optionslist_idINT FK
article_idINT FK
countINT
sortINT
mandatedBIT
>]; + articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; + article [label=<
article
idINT PK
company_idINT FK
brand_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
fatherarticle_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articlesupplier [label=<
articlesupplier
idINT PK
entity_idINT FK
article_idINT FK
price_id?INT FK
sku?NVARCHAR(100)
stock?FLOAT
puFLOAT
sortINT
>]; + category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; + categoryarticle [label=<
categoryarticle
idINT PK
article_idINT FK
category_idINT FK
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; + brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; - articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; - brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; file [label=<
file
idINT PK
hashNVARCHAR(64)
mimetypeNVARCHAR(255)
nameNVARCHAR(255)
sizeBIGINT
createdDATETIME2
modifiedDATETIME2
contentVARBINARY(MAX)
thumbavif?VARBINARY(MAX)
thumbx?INT
thumby?INT
meta?NVARCHAR(MAX)
>]; } subgraph cluster_manifests { label="Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; + ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
warehouse_id?INT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
created_atDATETIME2
>]; manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
sku?NVARCHAR(100)
name?NVARCHAR(255)
puFLOAT
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
taxrateFLOAT
>]; - ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
hash_versionINT
prev_hash?NVARCHAR(128)
created_atDATETIME2
>]; manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
external_number?NVARCHAR(100)
dateDATETIME2
created_atDATETIME2
>]; manifestattachment [label=<
manifestattachment
idINT PK
manifest_idINT FK
file_idINT FK
sortINT
>]; + warehouse [label=<
warehouse
idINT PK
company_idINT FK
nameNVARCHAR(255)
>]; } subgraph cluster_commerce { label="Pricing"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - price [label=<
price
idINT PK
customergroup_idINT FK
tax_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + price [label=<
price
idINT PK
customergroup_idINT FK
article_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -83,73 +84,77 @@ digraph schema { label="POS"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#202a3a"; color="#3f4c5f"; fontcolor="#e2e8f0"; margin=14; + posterminalcategory [label=<
posterminalcategory
idINT PK
posterminal_idINT FK
category_idINT FK
>]; posterminal [label=<
posterminal
idINT PK
company_idINT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpinNVARCHAR(50)
>]; - posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; + printer [label=<
printer
idINT PK
company_idINT FK
addressNVARCHAR(255)
typeNVARCHAR(50)
model?NVARCHAR(255)
serial?NVARCHAR(255)
sortINT
desc?NVARCHAR(MAX)
>]; } __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:sw -> company:in4:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; + google_auth:selected_company:w -> company:in9:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_google_auth_selected_company", weight=3]; category:parent_id:e -> category:id:e [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_category_parent", constraint=false]; company:root_category_id:e -> category:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_company_root_category", weight=3]; - company:default_language_id:ne -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; - company:taxrate:e -> tax:in3:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; - company:reducedtaxrate:e -> tax:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; - company:taxfreerate:e -> tax:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; - customergroup:company_id:w -> company:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; + company:default_language_id:w -> language:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_company_default_language", constraint=false, weight=1]; + company:taxrate:w -> tax:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_company_taxrate", constraint=false, weight=1]; + company:reducedtaxrate:w -> tax:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_company_reducedtaxrate", constraint=false, weight=1]; + company:taxfreerate:w -> tax:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; + customergroup:company_id:w -> company:in4:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_customergroup_company", weight=3]; company:poscustomergroup_id:w -> customergroup:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; - entity:customergroup_id:w -> customergroup:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; - entityaddress:entity_id:ne -> entity:in0:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; - entitycontact:entity_id:sw -> entity:in1:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; - entityhistory:entity_id:sw -> entity:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; - entityhistory:companyuser_id:se -> companyuser:in1:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; - unit:company_id:e -> company:in7:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; - unittext:unit_id:sw -> unit:in0:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; - unittext:language_id:w -> language:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; - brand:company_id:w -> company:in9:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; - article:company_id:w -> company:in8:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; - article:unit_id:w -> unit:in1:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; - article:brand_id:e -> brand:in1:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; - article:articlelist_id:e -> articlelist:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_articlelist", constraint=false, weight=1]; - article:optionslist_id:e -> optionslist:in0:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; + entity:customergroup_id:sw -> customergroup:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_entity_customergroup", weight=3]; + entityaddress:entity_id:ne -> entity:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_entityaddress_entity", weight=3]; + entitycontact:entity_id:sw -> entity:in2:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_entitycontact_entity", weight=3]; + entityhistory:entity_id:sw -> entity:in3:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_entityhistory_entity", weight=3]; + entityhistory:companyuser_id:se -> companyuser:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; + unit:company_id:w -> company:in12:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_unit_company", weight=3]; + unittext:unit_id:w -> unit:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_unittext_unit", weight=3]; + unittext:language_id:e -> language:in3:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_unittext_language", constraint=false, weight=1]; + brand:company_id:sw -> company:in10:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_brand_company", weight=3]; + article:company_id:w -> company:in11:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_article_company", weight=3]; + article:unit_id:e -> unit:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_article_unit", constraint=false, weight=1]; + article:brand_id:w -> brand:in0:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_article_brand", constraint=false, weight=1]; + article:articlelist_id:w -> articlelist:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_article_articlelist", constraint=false, weight=1]; + article:optionslist_id:w -> optionslist:in1:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_article_optionslist", constraint=false, weight=1]; article:fatherarticle_id:e -> article:id:e [color="#c79bff", fontcolor="#c79bff", tooltip="FK_article_fatherarticle", constraint=false]; - articlehistory:article_id:se -> article:in4:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlehistory_article", weight=3]; - articlehistory:companyuser_id:w -> companyuser:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; - articleean:article_id:se -> article:in6:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articleean_article", weight=3]; - articlepic:article_id:se -> article:in1:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articlepic_article", weight=3]; - articlepic:pic_id:w -> pic:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlepic_pic", constraint=false, weight=1]; - brandpic:brand_id:e -> brand:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_brandpic_brand", weight=3]; + articlehistory:article_id:se -> article:in2:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlehistory_article", weight=3]; + articlehistory:companyuser_id:w -> companyuser:in1:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; + articleean:article_id:se -> article:in3:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_articleean_article", weight=3]; + articlepic:article_id:sw -> article:in7:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_articlepic_article", weight=3]; + articlepic:pic_id:e -> pic:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlepic_pic", constraint=false, weight=1]; + brandpic:brand_id:e -> brand:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_brandpic_brand", weight=3]; brandpic:pic_id:w -> pic:in2:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_brandpic_pic", constraint=false, weight=1]; articlelistarticle:articlelist_id:sw -> articlelist:in0:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_articlelistarticle_articlelist", weight=3]; - articlelistarticle:article_id:e -> article:in8:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_optionslistarticle_optionslist", weight=3]; - optionslistarticle:article_id:e -> article:in9:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; - categoryarticle:category_id:se -> category:in4:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_categoryarticle_category", weight=3]; - categoryarticle:article_id:se -> article:in3:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; - categorypic:category_id:se -> category:in2:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categorypic_category", weight=3]; - categorypic:pic_id:e -> pic:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categorypic_pic", constraint=false, weight=1]; - categorytext:category_id:e -> category:in1:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_category", weight=3]; - categorytext:language_id:w -> language:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorytext_language", constraint=false, weight=1]; - articletext:article_id:e -> article:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_articletext_article", weight=3]; - articletext:language_id:w -> language:in3:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_articletext_language", constraint=false, weight=1]; - price:tax_id:w -> tax:in0:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_tax", constraint=false, weight=1]; - price:article_id:sw -> article:in2:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_article", weight=3]; - price:customergroup_id:e -> customergroup:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_price_customergroup", constraint=false, weight=1]; - articlesupplier:article_id:sw -> article:in7:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_article", weight=3]; + articlelistarticle:article_id:w -> article:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + optionslistarticle:optionslist_id:se -> optionslist:in0:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_optionslistarticle_optionslist", weight=3]; + optionslistarticle:article_id:w -> article:in1:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + categoryarticle:category_id:nw -> category:in3:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_categoryarticle_category", weight=3]; + categoryarticle:article_id:sw -> article:in6:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; + categorypic:category_id:sw -> category:in4:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_categorypic_category", weight=3]; + categorypic:pic_id:w -> pic:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_categorypic_pic", constraint=false, weight=1]; + categorytext:category_id:e -> category:in5:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_categorytext_category", weight=3]; + categorytext:language_id:w -> language:in2:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_categorytext_language", constraint=false, weight=1]; + articletext:article_id:w -> article:in9:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_articletext_article", weight=3]; + articletext:language_id:e -> language:in1:n [color="#56c8c8", fontcolor="#56c8c8", tooltip="FK_articletext_language", constraint=false, weight=1]; + price:tax_id:w -> tax:in3:n [color="#ff5f9e", fontcolor="#ff5f9e", tooltip="FK_price_tax", constraint=false, weight=1]; + price:article_id:e -> article:in8:n [color="#ffb25a", fontcolor="#ffb25a", tooltip="FK_price_article", weight=3]; + price:customergroup_id:w -> customergroup:in1:n [color="#56d68a", fontcolor="#56d68a", tooltip="FK_price_customergroup", constraint=false, weight=1]; + articlesupplier:article_id:ne -> article:in5:n [color="#66b3ff", fontcolor="#66b3ff", tooltip="FK_articlesupplier_article", weight=3]; articlesupplier:entity_id:w -> entity:in4:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_articlesupplier_entity", constraint=false, weight=1]; articlesupplier:price_id:e -> price:in0:n [color="#ff6b6b", fontcolor="#ff6b6b", tooltip="FK_articlesupplier_price", constraint=false, weight=1]; - manifest:company_id:sw -> company:in6:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_company", weight=3]; - manifest:entity_id:w -> entity:in3:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifest_entity", constraint=false, weight=1]; + manifest:company_id:e -> company:in0:n [color="#6bdf9a", fontcolor="#6bdf9a", tooltip="FK_manifest_company", weight=3]; + manifest:entity_id:e -> entity:in0:n [color="#5aa7ff", fontcolor="#5aa7ff", tooltip="FK_manifest_entity", constraint=false, weight=1]; manifestarticle:manifest_id:se -> manifest:in0:n [color="#f5c15c", fontcolor="#f5c15c", tooltip="FK_manifestarticle_manifest", weight=3]; - ledger:manifestarticle_id:sw -> manifestarticle:in0:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_ledger_manifestarticle", weight=3]; - numbers:company_id:e -> company:in1:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_numbers_company", weight=3]; - companyuser:google_auth_id:e -> google_auth:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; - companyuser:company_id:w -> company:in3:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_companyuser_company", weight=3]; - syncpos:company_id:se -> company:in2:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_syncpos_company", weight=3]; - companysetting:company_id:se -> company:in5:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_companysetting_company", weight=3]; - posterminal:company_id:w -> company:in10:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_posterminal_company", weight=3]; - posterminalcategory:posterminal_id:sw -> posterminal:in0:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_posterminalcategory_posterminal", weight=3]; - posterminalcategory:category_id:w -> category:in5:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; - manifestattachment:manifest_id:sw -> manifest:in1:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_manifestattachment_manifest", weight=3]; - manifestattachment:file_id:e -> file:in0:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; + warehouse:company_id:e -> company:in1:n [color="#c79bff", fontcolor="#c79bff", tooltip="FK_warehouse_company", weight=3]; + ledger:manifestarticle_id:sw -> manifestarticle:in0:n [color="#ff9f5a", fontcolor="#ff9f5a", tooltip="FK_ledger_manifestarticle", weight=3]; + ledger:warehouse_id:e -> warehouse:in0:n [color="#5cd6d6", fontcolor="#5cd6d6", tooltip="FK_ledger_warehouse", constraint=false, weight=1]; + numbers:company_id:se -> company:in5:n [color="#ff7fc2", fontcolor="#ff7fc2", tooltip="FK_numbers_company", weight=3]; + companyuser:google_auth_id:e -> google_auth:in0:n [color="#8f95ff", fontcolor="#8f95ff", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; + companyuser:company_id:se -> company:in6:n [color="#58d18a", fontcolor="#58d18a", tooltip="FK_companyuser_company", weight=3]; + syncpos:company_id:nw -> company:in7:n [color="#ff8f66", fontcolor="#ff8f66", tooltip="FK_syncpos_company", weight=3]; + companysetting:company_id:sw -> company:in8:n [color="#4fc4e8", fontcolor="#4fc4e8", tooltip="FK_companysetting_company", weight=3]; + posterminal:company_id:e -> company:in2:n [color="#a98cff", fontcolor="#a98cff", tooltip="FK_posterminal_company", weight=3]; + posterminalcategory:posterminal_id:ne -> posterminal:in0:n [color="#ff6f6f", fontcolor="#ff6f6f", tooltip="FK_posterminalcategory_posterminal", weight=3]; + posterminalcategory:category_id:e -> category:in1:n [color="#4ecb8f", fontcolor="#4ecb8f", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; + printer:company_id:se -> company:in3:n [color="#5aa7e6", fontcolor="#5aa7e6", tooltip="FK_printer_company", weight=3]; + manifestattachment:manifest_id:sw -> manifest:in1:n [color="#ff7aa8", fontcolor="#ff7aa8", tooltip="FK_manifestattachment_manifest", weight=3]; + manifestattachment:file_id:e -> file:in0:n [color="#e0b25a", fontcolor="#e0b25a", tooltip="FK_manifestattachment_file", constraint=false, weight=1]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index 336889d..af841d5 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,562 +4,439 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_manifests - -Documents + +Documents cluster_commerce - -Pricing + +Pricing cluster_units - -Units + +Units cluster_pos - -POS + +POS meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) - - -sort -INT + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) + + +sort +INT - + company - - - - - - - - - - - - - -company - - -id -INT -PK - - -poscustomergroup_id -INT -FK - - -default_language_id -? -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -root_category_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + + + + + +company + + +id +INT +PK + + +poscustomergroup_id +INT +FK + + +root_category_id +? +INT +FK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +default_language_id +? +INT +FK + + +name +NVARCHAR(255) customergroup:w->company:n - - + + numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - -numbers:e->company:n - - - + +numbers:se->company:n + + + entityaddress - - -entityaddress - - -id -INT -PK - - -entity_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name? -NVARCHAR(255) - - -street? -NVARCHAR(255) - - -streetnumber? -NVARCHAR(50) - - -zip? -NVARCHAR(50) - - -city? -NVARCHAR(255) - - -county? -NVARCHAR(255) - - -addresstype -NVARCHAR(50) - - -sort -INT + + +entityaddress + + +id +INT +PK + + +entity_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name? +NVARCHAR(255) + + +street? +NVARCHAR(255) + + +streetnumber? +NVARCHAR(50) + + +zip? +NVARCHAR(50) + + +city? +NVARCHAR(255) + + +county? +NVARCHAR(255) + + +addresstype +NVARCHAR(50) + + +sort +INT entity - - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -number? -NVARCHAR(100) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +number? +NVARCHAR(100) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 entityaddress:ne->entity:n - - + + -entity:w->customergroup:n +entity:sw->customergroup:n - - + + companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK - -companyuser:w->company:n - - - + +companyuser:se->company:n + + + google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 - + companyuser:e->google_auth:n - - - + + + entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - -sort -INT + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + +sort +INT entitycontact:sw->entity:n - - - - - - - -syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) - - - -syncpos:se->company:n - - - - - - - - -entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -entityhistory:sw->entity:n - - - - - - - - -entityhistory:se->companyuser:n - - - - - - - - -companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - -writeonly -BIT - - - -companysetting:se->company:n - - - + + @@ -567,256 +444,442 @@ company:w->customergroup:n - - + + language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) -company:ne->language:n +company:w->language:n - - + + - + category - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT company:e->category:n - - + + - + tax - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) -company:e->tax:n +company:w->tax:n - - + + -company:e->tax:n +company:w->tax:n - - + + -company:e->tax:n +company:w->tax:n - - + + + + + + + +syncpos + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) + + + +syncpos:nw->company:n + + + + + + + + +entityhistory + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +entityhistory:sw->entity:n + + + + + + + + +entityhistory:se->companyuser:n + + + + + + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + +writeonly +BIT + + + +companysetting:sw->company:n + + + -google_auth:sw->company:n +google_auth:w->company:n - - + + - + -unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) +articletext + + +articletext + + +id +INT +PK + + +article_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) - - -unittext:w->language:n - - - + + +articletext:e->language:n + + + - - -unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT + + +article + + + + + + + + + + + + +article + + +id +INT +PK + + +company_id +INT +FK + + +brand_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +unit_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 - - -unittext:sw->unit:n - - - + + +articletext:w->article:n + + + categorytext - - -categorytext - - -id -INT -PK - - -language_id -INT -FK - - -category_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +categorytext + + +id +INT +PK + + +category_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) categorytext:w->language:n - - + + @@ -824,237 +887,291 @@ categorytext:e->category:n - - + + - + -articletext - - -articletext - - -id -INT -PK - - -language_id -INT -FK - - -article_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) +unittext + + +unittext + + +id +INT +PK + + +unit_id +INT +FK + + +language_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) - - -articletext:w->language:n - - - + + +unittext:e->language:n + + + - - -article - - - - - - - - - - - - -article - - -id -INT -PK - - -company_id -INT -FK - - -unit_id -INT -FK - - -fatherarticle_id -? -INT -FK - - -articlelist_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + +unit + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT - - -articletext:e->article:n - - - + + +unittext:w->unit:n + + + - - -category:e->category:e - - - + + +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) + + + +brand:sw->company:n + + + - + -categoryarticle - - -categoryarticle - - -id -INT -PK - - -category_id -INT -FK - - -article_id -INT -FK +articlelist + + + + +articlelist + + +id +INT +PK - - -categoryarticle:se->category:n - - - + + +articlelistarticle + + +articlelistarticle + + +id +INT +PK + + +articlelist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT + + + +articlelistarticle:sw->articlelist:n + + + - - -categoryarticle:se->article:n - - - + + +articlelistarticle:w->article:n + + + + + + + + +optionslist + + + + +optionslist + + +id +INT +PK + + + +optionslistarticle + + +optionslistarticle + + +id +INT +PK + + +optionslist_id +INT +FK + + +article_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT + + + +optionslistarticle:se->optionslist:n + + + + + + + + +optionslistarticle:w->article:n + + + - + articlehistory - - -articlehistory - - -id -INT -PK - - -companyuser_id -? -INT -FK - - -article_id -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) + + +articlehistory + + +id +INT +PK + + +companyuser_id +? +INT +FK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) articlehistory:w->companyuser:n - - + + @@ -1062,8 +1179,38 @@ articlehistory:se->article:n - - + + + + + + + +articleean + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) + + + +articleean:se->article:n + + + @@ -1071,8 +1218,35 @@ article:w->company:n - - + + + + + + + +article:w->brand:n + + + + + + + + +article:w->articlelist:n + + + + + + + + +article:w->optionslist:n + + + @@ -1080,523 +1254,263 @@ article:e->article:e - - - - - - - -articlelist - - - - -articlelist - - -id -INT -PK - - - -article:e->articlelist:n - - - - - - - - -optionslist - - - - -optionslist - - -id -INT -PK - - - -article:e->optionslist:n - - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:e->brand:n - - - + + -article:w->unit:n +article:e->unit:n - - - - - - - -articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) - - - -articleean:se->article:n - - - + + - + articlesupplier - - -articlesupplier - - -id -INT -PK - - -entity_id -INT -FK - - -price_id -? -INT -FK - - -article_id -INT -FK - - -sku? -NVARCHAR(100) - - -stock? -FLOAT - - -pu -FLOAT - - -sort -INT + + +articlesupplier + + +id +INT +PK + + +entity_id +INT +FK + + +article_id +INT +FK + + +price_id +? +INT +FK + + +sku? +NVARCHAR(100) + + +stock? +FLOAT + + +pu +FLOAT + + +sort +INT articlesupplier:w->entity:n - - + + -articlesupplier:sw->article:n +articlesupplier:ne->article:n - - + + - + price - - - -price - - -id -INT -PK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -article_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT + + + +price + + +id +INT +PK + + +customergroup_id +INT +FK + + +article_id +INT +FK + + +tax_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT articlesupplier:e->price:n - - + + - - -articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -article_id -INT -FK - - -articlelist_id -INT -FK - - -count -INT - - -sort -INT - - - -articlelistarticle:e->article:n - - - + + +category:e->category:e + + + - - -articlelistarticle:sw->articlelist:n - - - + + +categoryarticle + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK + + + +categoryarticle:sw->article:n + + + - - -optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT - - - -optionslistarticle:e->article:n - - - - - - - - -optionslistarticle:sw->optionslist:n - - - - - - - - -brand:w->company:n - - - + + +categoryarticle:nw->category:n + + + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) - - - -categorypic - - -categorypic - - -id -INT -PK - - -pic_id -INT -FK - - -category_id -INT -FK - - -sort -INT - - - -categorypic:se->category:n - - - - - - - - -categorypic:e->pic:n - - - - - - - - -articlepic - - -articlepic - - -id -INT -PK - - -pic_id -INT -FK - - -article_id -INT -FK - - -sort -INT - - - -articlepic:se->article:n - - - - - - - - -articlepic:w->pic:n - - - - - + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) - + brandpic - - -brandpic - - -id -INT -PK - - -pic_id -INT -FK - - -brand_id -INT -FK - - -sort -INT + + +brandpic + + +id +INT +PK + + +brand_id +INT +FK + + +pic_id +INT +FK + + +sort +INT brandpic:e->brand:n - - + + @@ -1604,299 +1518,433 @@ brandpic:w->pic:n - - + + + + + + + +articlepic + + +articlepic + + +id +INT +PK + + +article_id +INT +FK + + +pic_id +INT +FK + + +sort +INT + + + +articlepic:sw->article:n + + + + + + + + +articlepic:e->pic:n + + + + + + + + +categorypic + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT + + + +categorypic:sw->category:n + + + + + + + + +categorypic:w->pic:n + + + file - - - -file - - -id -INT -PK - - -hash -NVARCHAR(64) - - -mimetype -NVARCHAR(255) - - -name -NVARCHAR(255) - - -size -BIGINT - - -created -DATETIME2 - - -modified -DATETIME2 - - -content -VARBINARY(MAX) - - -thumbavif? -VARBINARY(MAX) - - -thumbx? -INT - - -thumby? -INT - - -meta? -NVARCHAR(MAX) + + + +file + + +id +INT +PK + + +hash +NVARCHAR(64) + + +mimetype +NVARCHAR(255) + + +name +NVARCHAR(255) + + +size +BIGINT + + +created +DATETIME2 + + +modified +DATETIME2 + + +content +VARBINARY(MAX) + + +thumbavif? +VARBINARY(MAX) + + +thumbx? +INT + + +thumby? +INT + + +meta? +NVARCHAR(MAX) + + + +ledger + + +ledger + + +id +INT +PK + + +manifestarticle_id +INT +FK + + +warehouse_id +? +INT +FK + + +transaction_type +NVARCHAR(100) + + +amount +FLOAT + + +transaction_text +NVARCHAR(MAX) + + +hash_version +INT + + +prev_hash? +NVARCHAR(128) + + +created_at +DATETIME2 - + manifestarticle - - - -manifestarticle - - -id -INT -PK - - -manifest_id -INT -FK - - -sku? -NVARCHAR(100) - - -name? -NVARCHAR(255) - - -pu -FLOAT - - -amount -FLOAT - - -net -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT - - -taxrate -FLOAT + + + +manifestarticle + + +id +INT +PK + + +manifest_id +INT +FK + + +sku? +NVARCHAR(100) + + +name? +NVARCHAR(255) + + +pu +FLOAT + + +amount +FLOAT + + +net +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT + + +taxrate +FLOAT + + + +ledger:sw->manifestarticle:n + + + + + + + + +warehouse + + + +warehouse + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + + +ledger:e->warehouse:n + + + + + manifest - - - - -manifest - - -id -INT -PK - - -entity_id -? -INT -FK - - -company_id -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -external_number? -NVARCHAR(100) - - -date -DATETIME2 - - -created_at -DATETIME2 + + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +company_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +external_number? +NVARCHAR(100) + + +date +DATETIME2 + + +created_at +DATETIME2 manifestarticle:se->manifest:n - - - - - - - -ledger - - -ledger - - -id -INT -PK - - -manifestarticle_id -INT -FK - - -transaction_type -NVARCHAR(100) - - -amount -FLOAT - - -transaction_text -NVARCHAR(MAX) - - -hash_version -INT - - -prev_hash? -NVARCHAR(128) - - -created_at -DATETIME2 - - - -ledger:sw->manifestarticle:n - - - + + -manifest:w->entity:n +manifest:e->entity:n - - + + -manifest:sw->company:n +manifest:e->company:n - - + + manifestattachment - - -manifestattachment - - -id -INT -PK - - -manifest_id -INT -FK - - -file_id -INT -FK - - -sort -INT + + +manifestattachment + + +id +INT +PK + + +manifest_id +INT +FK + + +file_id +INT +FK + + +sort +INT - + manifestattachment:e->file:n - - - + + + - + manifestattachment:sw->manifest:n - - - + + + + + + + + +warehouse:e->company:n + + + -price:e->customergroup:n +price:w->customergroup:n - - + + -price:sw->article:n +price:e->article:n - - + + @@ -1904,126 +1952,176 @@ price:w->tax:n - - + + -unit:e->company:n +unit:w->company:n - - - - - - - -posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) - - - -posterminal:w->company:n - - - + + posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -category_id -INT -FK - - -posterminal_id -INT -FK + + +posterminalcategory + + +id +INT +PK + + +posterminal_id +INT +FK + + +category_id +INT +FK - -posterminalcategory:w->category:n - - - + +posterminalcategory:e->category:n + + + + + +posterminal + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) + - -posterminalcategory:sw->posterminal:n - - - + +posterminalcategory:ne->posterminal:n + + + + + + + + +posterminal:e->company:n + + + + + + + + +printer + + +printer + + +id +INT +PK + + +company_id +INT +FK + + +address +NVARCHAR(255) + + +type +NVARCHAR(50) + + +model? +NVARCHAR(255) + + +serial? +NVARCHAR(255) + + +sort +INT + + +desc? +NVARCHAR(MAX) + + + +printer:se->company:n + + + - + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/scripts/test-ledger.js b/scripts/test-ledger.js index 1ed1f6f..c01c35f 100644 --- a/scripts/test-ledger.js +++ b/scripts/test-ledger.js @@ -112,6 +112,7 @@ async function run() { JSON.stringify({ id: 1, manifestarticle_id: manifestarticleId1, + warehouse_id: null, transaction_type: 'GOODS_RECEIPT', amount: 5, transaction_text: 'Initial delivery movement', @@ -228,6 +229,68 @@ 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...'); + const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); + assert.equal(wh1.name, 'Hauptlager'); + assert(wh1.id > 0); + + const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' }); + assert.equal(wh2.name, 'Filiale Nord'); + + 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')); + + const whGet = await store.warehouse.getById(googleAuthId, wh1.id); + assert.equal(whGet.name, 'Hauptlager'); + + const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd' }); + assert.equal(whUpdated.name, 'Filiale Süd'); + + // Add ledger row with warehouse + const rowWithWh = await store.ledger.add(googleAuthId, { + manifestarticle_id: manifestarticleId1, + warehouse_id: wh1.id, + transaction_type: 'DELIVERY', + amount: 25, + transaction_text: 'Delivered directly to Hauptlager', + }); + assert.equal(rowWithWh.warehouse_id, wh1.id); + assert.equal(rowWithWh.amount, 25); + + // Filter by warehouse in listBySku and listByManifestarticleId + const whRowsBySku = await store.ledger.listBySku(googleAuthId, 'SKU-ABC', { warehouse_id: wh1.id }); + assert.equal(whRowsBySku.length, 1); + assert.equal(whRowsBySku[0].id, rowWithWh.id); + assert.equal(whRowsBySku[0].warehouse_id, wh1.id); + + const whRowsByLine = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1, { warehouse_id: wh1.id }); + assert.equal(whRowsByLine.length, 1); + assert.equal(whRowsByLine[0].id, rowWithWh.id); + + // Verify warehouse is recorded in article history + const historyWithWh = await store.article.getHistoryBySku(googleAuthId, article.sku); + assert( + historyWithWh.some((h) => h.text.includes('Lager: "Hauptlager"') && h.text.includes('Lieferant: "Supplier X"')), + 'History should include warehouse name (Lager: "Hauptlager")', + ); + + // Trying to delete wh1 (used in ledger) must fail + await assert.rejects( + async () => store.warehouse.delete(googleAuthId, wh1.id), + /Warehouse in use in ledger/, + 'Should reject deleting warehouse referenced in ledger', + ); + + // Deleting wh2 (not used in ledger) must succeed + await store.warehouse.delete(googleAuthId, wh2.id); + const whListAfterDelete = await store.warehouse.list(googleAuthId); + assert.equal(whListAfterDelete.length, 1); + assert.equal(whListAfterDelete[0].id, wh1.id); + console.log('✓ Warehouse CRUD and ledger enforcement passed'); + console.log('✓ All ledger tests passed successfully!'); } finally { try { diff --git a/src/index.js b/src/index.js index c1a4292..8656d06 100644 --- a/src/index.js +++ b/src/index.js @@ -124,6 +124,18 @@ import manifestattachmentOrder from './store/manifestattachment/order.js'; import ledgerAdd from './store/ledger/add.js'; import ledgerListByManifestarticleId from './store/ledger/listByManifestarticleId.js'; import ledgerListBySku from './store/ledger/listBySku.js'; +import warehouseCreate from './store/warehouse/create.js'; +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 printerCreate from './store/printer/create.js'; +import printerGetById from './store/printer/getById.js'; +import printerList from './store/printer/list.js'; +import printerUpdate from './store/printer/update.js'; +import printerDelete from './store/printer/delete.js'; +import printerOrder from './store/printer/order.js'; +import stockAdjust from './store/stock/adjust.js'; export default class LibDashStore { /** @@ -340,6 +352,28 @@ export default class LibDashStore { listBySku: ledgerListBySku(provider), }; + this.warehouse = { + create: warehouseCreate(provider), + getById: warehouseGetById(provider), + list: warehouseList(provider), + update: warehouseUpdate(provider), + delete: warehouseDelete(provider), + }; + + this.printer = { + create: printerCreate(provider), + getById: printerGetById(provider), + list: printerList(provider), + update: printerUpdate(provider), + delete: printerDelete(provider), + order: printerOrder(provider), + }; + + this.stock = { + adjust: stockAdjust(provider), + }; + this.adjustment = this.stock; + this.purge = storePurge(provider); this.initialize = storeInitialize(provider); diff --git a/src/providers/mssql/index.js b/src/providers/mssql/index.js index 9437184..8c3d1ff 100644 --- a/src/providers/mssql/index.js +++ b/src/providers/mssql/index.js @@ -159,6 +159,18 @@ export class MssqlProvider { await pool.request().query(script); } } + + try { + await pool.request().query(` + IF OBJECT_ID(N'dbo.ledger', N'U') IS NOT NULL AND COL_LENGTH('ledger', 'warehouse_id') IS NULL + BEGIN + ALTER TABLE ledger ADD warehouse_id INT NULL; + ALTER TABLE ledger ADD CONSTRAINT FK_ledger_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION; + END + `); + } catch { + // ignore + } } async purge() { diff --git a/src/providers/mssql/purgeSql.js b/src/providers/mssql/purgeSql.js index 973c214..c1e7bb5 100644 --- a/src/providers/mssql/purgeSql.js +++ b/src/providers/mssql/purgeSql.js @@ -7,8 +7,10 @@ export const TABLES = [ 'companysetting', 'posterminalcategory', 'posterminal', + 'printer', 'numbers', 'ledger', + 'warehouse', 'articlesupplier', 'manifestarticle', 'manifest', diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 8f94271..3f9c3ac 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -446,7 +446,29 @@ SELECT WHERE asup.article_id = a.id ORDER BY asup.sort ASC, asup.id ASC FOR JSON PATH - ) AS suppliersJson + ) AS suppliersJson, + ( + SELECT + w.id, + w.name, + COALESCE(stock_calc.stock, 0) AS stock + FROM warehouse w + LEFT JOIN ( + SELECT l.warehouse_id, SUM(l.amount) AS stock + FROM ledger l + INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id + INNER JOIN manifest m ON m.id = ma.manifest_id + WHERE m.company_id = a.company_id + AND ( + ma.sku = a.sku + OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) + ) + 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 + FOR JSON PATH + ) AS warehousesJson FROM article a LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = auth.languageId diff --git a/src/providers/mssql/queries/ledger.js b/src/providers/mssql/queries/ledger.js index 0ac018a..607ee2c 100644 --- a/src/providers/mssql/queries/ledger.js +++ b/src/providers/mssql/queries/ledger.js @@ -17,6 +17,12 @@ LEFT JOIN entity e ON e.id = m.entity_id WHERE ma.id = @manifestarticleId AND m.company_id = @companyId; `; +export const CHECK_WAREHOUSE_ACCESS = ` +SELECT id, name +FROM warehouse +WHERE id = @warehouseId AND company_id = @companyId; +`; + export const FIND_ARTICLES_FOR_HISTORY = ` SELECT DISTINCT asup.article_id AS articleId FROM articlesupplier asup @@ -35,6 +41,7 @@ export const GET_LATEST_ROW = ` SELECT TOP 1 id, manifestarticle_id, + warehouse_id, transaction_type, amount, transaction_text, @@ -48,6 +55,7 @@ ORDER BY id DESC; export const INSERT_ROW = ` INSERT INTO ledger ( manifestarticle_id, + warehouse_id, transaction_type, amount, transaction_text, @@ -57,6 +65,7 @@ INSERT INTO ledger ( OUTPUT INSERTED.id, INSERTED.manifestarticle_id, + INSERTED.warehouse_id, INSERTED.transaction_type, INSERTED.amount, INSERTED.transaction_text, @@ -65,6 +74,7 @@ OUTPUT INSERTED.created_at VALUES ( @manifestarticleId, + @warehouseId, @transactionType, @amount, @transactionText, @@ -77,6 +87,7 @@ export const LIST_BY_MANIFESTARTICLE = ` SELECT l.id, l.manifestarticle_id, + l.warehouse_id, ma.sku, l.transaction_type, l.amount, @@ -90,6 +101,7 @@ INNER JOIN manifest m ON ma.manifest_id = m.id WHERE l.manifestarticle_id = @manifestarticleId AND m.company_id = @companyId AND (@transactionType IS NULL OR l.transaction_type = @transactionType) + AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId) ORDER BY l.id ASC; `; @@ -97,6 +109,7 @@ export const LIST_BY_SKU = ` SELECT l.id, l.manifestarticle_id, + l.warehouse_id, ma.sku, l.transaction_type, l.amount, @@ -110,5 +123,6 @@ INNER JOIN manifest m ON ma.manifest_id = m.id WHERE ma.sku = @sku AND m.company_id = @companyId AND (@transactionType IS NULL OR l.transaction_type = @transactionType) + AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId) ORDER BY l.id ASC; `; diff --git a/src/providers/mssql/queries/printer.js b/src/providers/mssql/queries/printer.js new file mode 100644 index 0000000..0ee131c --- /dev/null +++ b/src/providers/mssql/queries/printer.js @@ -0,0 +1,99 @@ +export const PRINTER_COLUMNS = ` + id, + company_id, + address, + type, + model, + serial, + sort, + [desc] +`; + +export const LIST_BY_COMPANY = ` +SELECT +${PRINTER_COLUMNS} +FROM printer +WHERE company_id = @companyId +ORDER BY sort ASC, id ASC +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT +${PRINTER_COLUMNS} +FROM printer +WHERE id = @id AND company_id = @companyId +`; + +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM printer +WHERE company_id = @companyId +`; + +export const LIST_IDS_BY_COMPANY = ` +SELECT id +FROM printer +WHERE company_id = @companyId +`; + +export const CREATE = ` +INSERT INTO printer ( + company_id, + address, + type, + model, + serial, + sort, + [desc] +) +OUTPUT + INSERTED.id, + INSERTED.company_id, + INSERTED.address, + INSERTED.type, + INSERTED.model, + INSERTED.serial, + INSERTED.sort, + INSERTED.[desc] +VALUES ( + @companyId, + @address, + @type, + @model, + @serial, + @sort, + @desc +) +`; + +export const UPDATE = ` +UPDATE printer +SET + address = @address, + type = @type, + model = @model, + serial = @serial, + sort = @sort, + [desc] = @desc +OUTPUT + INSERTED.id, + INSERTED.company_id, + INSERTED.address, + INSERTED.type, + INSERTED.model, + INSERTED.serial, + INSERTED.sort, + INSERTED.[desc] +WHERE id = @id AND company_id = @companyId +`; + +export const UPDATE_SORT = ` +UPDATE printer +SET sort = @sort +WHERE id = @id AND company_id = @companyId +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM printer +WHERE id = @id AND company_id = @companyId +`; diff --git a/src/providers/mssql/queries/warehouse.js b/src/providers/mssql/queries/warehouse.js new file mode 100644 index 0000000..33534fb --- /dev/null +++ b/src/providers/mssql/queries/warehouse.js @@ -0,0 +1,48 @@ +export const LIST_BY_COMPANY = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE company_id = @companyId +ORDER BY name ASC; +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE id = @id AND company_id = @companyId; +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE company_id = @companyId AND name = @name; +`; + +export const CREATE = ` +INSERT INTO warehouse (company_id, name) +OUTPUT INSERTED.id, INSERTED.company_id, INSERTED.name +VALUES (@companyId, @name); +`; + +export const UPDATE_BY_ID = ` +UPDATE warehouse +SET name = @name +WHERE id = @id AND company_id = @companyId; +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM warehouse +WHERE id = @id AND company_id = @companyId; +`; + +export const CHECK_LEDGER_USAGE = ` +SELECT TOP 1 id FROM ledger WHERE warehouse_id = @warehouseId; +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index f47264a..70d9ca3 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -592,19 +592,34 @@ END `; +export const INIT_WAREHOUSE = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'warehouse') +BEGIN + CREATE TABLE warehouse ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + name NVARCHAR(255) NOT NULL, + CONSTRAINT FK_warehouse_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT UQ_warehouse_company_name UNIQUE (company_id, name) + ); +END +`; + export const INIT_LEDGER = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ledger') BEGIN CREATE TABLE ledger ( id INT IDENTITY(1,1) PRIMARY KEY, manifestarticle_id INT NOT NULL, + warehouse_id INT NULL, transaction_type NVARCHAR(100) NOT NULL, amount FLOAT NOT NULL, transaction_text NVARCHAR(MAX) NOT NULL, hash_version INT NOT NULL, prev_hash NVARCHAR(128) NULL, created_at DATETIME2 NOT NULL CONSTRAINT DF_ledger_created_at DEFAULT SYSUTCDATETIME(), - CONSTRAINT FK_ledger_manifestarticle FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION + CONSTRAINT FK_ledger_manifestarticle FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION, + CONSTRAINT FK_ledger_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION ) WITH (LEDGER = ON (APPEND_ONLY = ON)); END `; @@ -772,6 +787,25 @@ BEGIN END `; +export const INIT_PRINTER = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'printer') +BEGIN + CREATE TABLE printer ( + id INT IDENTITY(1,1) PRIMARY KEY, + company_id INT NOT NULL, + address NVARCHAR(255) NOT NULL, + type NVARCHAR(50) NOT NULL, + model NVARCHAR(255) NULL, + serial NVARCHAR(255) NULL, + sort INT NOT NULL CONSTRAINT DF_printer_sort DEFAULT 0, + [desc] NVARCHAR(MAX) NULL, + CONSTRAINT FK_printer_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE, + CONSTRAINT CK_printer_type CHECK (type IN ('esc/pos', 'zpl')) + ); +END +`; + + export const INIT_FILE = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'file') BEGIN @@ -862,6 +896,7 @@ export const SCHEMA_SCRIPTS = [ INIT_ARTICLE_SUPPLIER, INIT_MANIFEST, INIT_MANIFESTARTICLE, + INIT_WAREHOUSE, INIT_LEDGER, INIT_NUMBERS, INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, @@ -873,6 +908,7 @@ export const SCHEMA_SCRIPTS = [ INIT_COMPANYSETTING, INIT_POSTERMINAL, INIT_POSTERMINALCATEGORY, + INIT_PRINTER, INIT_FILE, INIT_MANIFESTATTACHMENT, INIT_MANIFESTATTACHMENT_TRIGGER, diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 0c6eecc..639e547 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -386,7 +386,7 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { } } - // Ensure created_at exists on ledger table if created previously + // Ensure created_at and warehouse_id exist on ledger table if created previously try { const ledgerCols = this.db.pragma('table_info("ledger")').map((c) => c.name); if (ledgerCols.length > 0 && !ledgerCols.includes('created_at')) { @@ -394,6 +394,11 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { `ALTER TABLE ledger ADD COLUMN created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`, ); } + if (ledgerCols.length > 0 && !ledgerCols.includes('warehouse_id')) { + this.db.exec( + `ALTER TABLE ledger ADD COLUMN warehouse_id INTEGER REFERENCES warehouse(id) ON DELETE NO ACTION`, + ); + } } catch { // ignore } diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 926b575..61bc398 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -160,7 +160,24 @@ SELECT auth.*, LEFT JOIN price pr ON pr.id = s.price_id LEFT JOIN tax t ON t.id = pr.tax_id 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) + )), '[]') FROM warehouse w + LEFT JOIN ( + SELECT l.warehouse_id, SUM(l.amount) AS stock + FROM ledger l + INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id + INNER JOIN manifest m ON m.id = ma.manifest_id + WHERE m.company_id = a.company_id + AND ( + ma.sku = a.sku + OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) + ) + 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)) ) AS articleJson FROM auth INNER JOIN article a ON a.id = auth.articleId diff --git a/src/providers/sqlite3/queries/ledger.js b/src/providers/sqlite3/queries/ledger.js index 08bd456..f0d08cb 100644 --- a/src/providers/sqlite3/queries/ledger.js +++ b/src/providers/sqlite3/queries/ledger.js @@ -17,6 +17,12 @@ LEFT JOIN entity e ON e.id = m.entity_id WHERE ma.id = @manifestarticleId AND m.company_id = @companyId; `; +export const CHECK_WAREHOUSE_ACCESS = ` +SELECT id, name +FROM warehouse +WHERE id = @warehouseId AND company_id = @companyId; +`; + export const FIND_ARTICLES_FOR_HISTORY = ` SELECT DISTINCT asup.article_id AS articleId FROM articlesupplier asup @@ -35,6 +41,7 @@ export const GET_LATEST_ROW = ` SELECT id, manifestarticle_id, + warehouse_id, transaction_type, amount, transaction_text, @@ -49,6 +56,7 @@ LIMIT 1; export const INSERT_ROW = ` INSERT INTO ledger ( manifestarticle_id, + warehouse_id, transaction_type, amount, transaction_text, @@ -57,6 +65,7 @@ INSERT INTO ledger ( ) VALUES ( @manifestarticleId, + @warehouseId, @transactionType, @amount, @transactionText, @@ -66,6 +75,7 @@ VALUES ( RETURNING id, manifestarticle_id, + warehouse_id, transaction_type, amount, transaction_text, @@ -78,6 +88,7 @@ export const LIST_BY_MANIFESTARTICLE = ` SELECT l.id, l.manifestarticle_id, + l.warehouse_id, ma.sku, l.transaction_type, l.amount, @@ -91,6 +102,7 @@ INNER JOIN manifest m ON ma.manifest_id = m.id WHERE l.manifestarticle_id = @manifestarticleId AND m.company_id = @companyId AND (@transactionType IS NULL OR l.transaction_type = @transactionType) + AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId) ORDER BY l.id ASC; `; @@ -98,6 +110,7 @@ export const LIST_BY_SKU = ` SELECT l.id, l.manifestarticle_id, + l.warehouse_id, ma.sku, l.transaction_type, l.amount, @@ -111,5 +124,6 @@ INNER JOIN manifest m ON ma.manifest_id = m.id WHERE ma.sku = @sku AND m.company_id = @companyId AND (@transactionType IS NULL OR l.transaction_type = @transactionType) + AND (@warehouseId IS NULL OR l.warehouse_id = @warehouseId) ORDER BY l.id ASC; `; diff --git a/src/providers/sqlite3/queries/printer.js b/src/providers/sqlite3/queries/printer.js new file mode 100644 index 0000000..4529251 --- /dev/null +++ b/src/providers/sqlite3/queries/printer.js @@ -0,0 +1,15 @@ +import * as source from '../../mssql/queries/printer.js'; +import { translateQueries } from './translate.js'; + +const queries = translateQueries(source, 'printer'); +export const { + CREATE, + DELETE_BY_COMPANY_AND_ID, + GET_BY_COMPANY_AND_ID, + GET_NEXT_SORT, + LIST_BY_COMPANY, + LIST_IDS_BY_COMPANY, + PRINTER_COLUMNS, + UPDATE, + UPDATE_SORT, +} = queries; diff --git a/src/providers/sqlite3/queries/warehouse.js b/src/providers/sqlite3/queries/warehouse.js new file mode 100644 index 0000000..94c8c9f --- /dev/null +++ b/src/providers/sqlite3/queries/warehouse.js @@ -0,0 +1,48 @@ +export const LIST_BY_COMPANY = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE company_id = @companyId +ORDER BY name ASC; +`; + +export const GET_BY_COMPANY_AND_ID = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE id = @id AND company_id = @companyId; +`; + +export const GET_BY_COMPANY_AND_NAME = ` +SELECT + id, + company_id, + name +FROM warehouse +WHERE company_id = @companyId AND name = @name; +`; + +export const CREATE = ` +INSERT INTO warehouse (company_id, name) +VALUES (@companyId, @name) +RETURNING id, company_id, name; +`; + +export const UPDATE_BY_ID = ` +UPDATE warehouse +SET name = @name +WHERE id = @id AND company_id = @companyId; +`; + +export const DELETE_BY_COMPANY_AND_ID = ` +DELETE FROM warehouse +WHERE id = @id AND company_id = @companyId; +`; + +export const CHECK_LEDGER_USAGE = ` +SELECT id FROM ledger WHERE warehouse_id = @warehouseId LIMIT 1; +`; diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index b699f03..56dc4d2 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -426,18 +426,30 @@ CREATE TABLE IF NOT EXISTS manifestarticle ( ); `; +export const INIT_WAREHOUSE = ` +CREATE TABLE IF NOT EXISTS warehouse ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + name TEXT NOT NULL COLLATE NOCASE, + UNIQUE (company_id, name), + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + /* SQLite has no SQL Server ledger tables; constraints preserve the base shape. */ export const INIT_LEDGER = ` CREATE TABLE IF NOT EXISTS ledger ( id INTEGER PRIMARY KEY, manifestarticle_id INTEGER NOT NULL, + warehouse_id INTEGER, transaction_type TEXT NOT NULL, amount REAL NOT NULL, transaction_text TEXT NOT NULL, hash_version INTEGER NOT NULL, prev_hash TEXT, created_at TEXT NOT NULL DEFAULT (${UTC_NOW}), - FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION + FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION, + FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) ON DELETE NO ACTION ); `; @@ -536,6 +548,21 @@ CREATE TABLE IF NOT EXISTS posterminalcategory ( ); `; +export const INIT_PRINTER = ` +CREATE TABLE IF NOT EXISTS printer ( + id INTEGER PRIMARY KEY, + company_id INTEGER NOT NULL, + address TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('esc/pos', 'zpl')), + model TEXT, + serial TEXT, + sort INTEGER NOT NULL DEFAULT 0, + "desc" TEXT, + FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE +); +`; + + export const INIT_FILE = ` CREATE TABLE IF NOT EXISTS "file" ( id INTEGER PRIMARY KEY, @@ -608,6 +635,7 @@ export const SCHEMA_SCRIPTS = [ INIT_ARTICLE_SUPPLIER, INIT_MANIFEST, INIT_MANIFESTARTICLE, + INIT_WAREHOUSE, INIT_LEDGER, INIT_NUMBERS, INIT_COMPANYUSER, @@ -617,6 +645,7 @@ export const SCHEMA_SCRIPTS = [ INIT_COMPANYSETTING, INIT_POSTERMINAL, INIT_POSTERMINALCATEGORY, + INIT_PRINTER, INIT_FILE, INIT_MANIFESTATTACHMENT, INIT_MANIFESTATTACHMENT_TRIGGER, diff --git a/src/store/article/getBySku.js b/src/store/article/getBySku.js index 915ae64..e2ea795 100644 --- a/src/store/article/getBySku.js +++ b/src/store/article/getBySku.js @@ -87,6 +87,11 @@ export default function getBySku(provider) { priceId: s.priceId ?? null, price: s.priceId != null && s.price?.net != null ? s.price : null, })), + warehouses: parseJsonArray(a.warehousesJson).map((w) => ({ + id: w.id, + name: w.name, + stock: Number(w.stock ?? 0), + })), optionsCount: a.optionsCount, articleListCount: a.articleListCount, childCount: a.childCount, diff --git a/src/store/ledger/add.js b/src/store/ledger/add.js index 2cb95eb..7a602e3 100644 --- a/src/store/ledger/add.js +++ b/src/store/ledger/add.js @@ -10,6 +10,7 @@ export function shape(row) { return { id: row.id, manifestarticle_id: row.manifestarticle_id, + warehouse_id: row.warehouse_id ?? null, transaction_type: row.transaction_type, amount: row.amount, transaction_text: row.transaction_text, @@ -36,6 +37,7 @@ export function computePrevHash(row) { const payload = JSON.stringify({ id: row.id, manifestarticle_id: row.manifestarticle_id, + warehouse_id: row.warehouse_id ?? null, transaction_type: row.transaction_type, amount: row.amount, transaction_text: row.transaction_text, @@ -52,17 +54,19 @@ export function computePrevHash(row) { * @param {number} googleAuthId - The id of the Google auth user. * @param {object} input - Ledger entry payload. * @param {number} input.manifestarticle_id - ID of the referenced manifest line (can also pass manifestarticleId). + * @param {number|null} [input.warehouse_id] - Optional warehouse ID (can also pass warehouseId). * @param {string} input.transaction_type - Transaction type (can also pass transactionType). * @param {number} input.amount - Movement amount. * @param {string} input.transaction_text - Transaction memo/details (can also pass transactionText). * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, manifestarticle_id: number, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>} The created ledger row. + * @returns {Promise<{id: number, manifestarticle_id: number, warehouse_id: number|null, transaction_type: string, amount: number, transaction_text: string, hash_version: number, prev_hash: string, created_at: string}>} The created ledger row. * @throws {Error} If user not authorized, company not selected, manifest article not found, or input invalid. * * @example * const entry = await store.ledger.add(googleAuthId, { * manifestarticle_id: 1, - * transaction_type: 'PAYMENT', + * warehouse_id: 1, + * transaction_type: 'DELIVERY', * amount: 100.5, * transaction_text: 'Invoice paid', * }); @@ -75,6 +79,8 @@ export default function add(provider) { return async (googleAuthId, input = {}, tx) => { const rawManifestarticleId = input.manifestarticle_id ?? input.manifestarticleId; + const rawWarehouseId = + input.warehouse_id ?? input.warehouseId; const rawTransactionType = input.transaction_type ?? input.transactionType; const rawAmount = input.amount; @@ -86,6 +92,14 @@ export default function add(provider) { throw new Error('Invalid ledger entry'); } + let warehouseId = null; + if (rawWarehouseId !== undefined && rawWarehouseId !== null) { + warehouseId = Number(rawWarehouseId); + if (!Number.isInteger(warehouseId) || warehouseId <= 0) { + throw new Error('Invalid ledger entry'); + } + } + const transactionType = typeof rawTransactionType === 'string' ? rawTransactionType.trim() @@ -117,12 +131,25 @@ export default function add(provider) { throw new Error('Manifest article not found'); } + let warehouseName = null; + if (warehouseId != null) { + const whCheck = await runner.query(queries.CHECK_WAREHOUSE_ACCESS, { + warehouseId, + companyId, + }); + if (!whCheck.rows[0]) { + throw new Error('Warehouse not found'); + } + warehouseName = whCheck.rows[0].name; + } + const latest = await runner.query(queries.GET_LATEST_ROW); const prevRow = latest.rows[0]; const prevHash = computePrevHash(prevRow); const inserted = await runner.query(queries.INSERT_ROW, { manifestarticleId, + warehouseId, transactionType, amount, transactionText, @@ -157,6 +184,9 @@ export default function add(provider) { if (manifestArticle.supplierName) { details.push(`Lieferant: "${manifestArticle.supplierName}"`); } + if (warehouseName) { + details.push(`Lager: "${warehouseName}"`); + } if (manifestArticle.net != null && Number.isFinite(Number(manifestArticle.net))) { const net = Number(manifestArticle.net); const rebate = Number(manifestArticle.rebate ?? 0); @@ -174,6 +204,8 @@ export default function add(provider) { transactionText?.trim() || (manifestArticle.manifestNumber ? `Lieferung ${manifestArticle.manifestNumber}` : 'Lieferung'); historyText = `${base}${suffix}`; + } else if (warehouseName && !transactionText.includes(warehouseName)) { + historyText = `${transactionText} (Lager: "${warehouseName}")`; } for (const match of articleMatches.rows) { diff --git a/src/store/ledger/listByManifestarticleId.js b/src/store/ledger/listByManifestarticleId.js index cc769fd..ef5472e 100644 --- a/src/store/ledger/listByManifestarticleId.js +++ b/src/store/ledger/listByManifestarticleId.js @@ -7,6 +7,7 @@ function shape(row) { return { id: row.id, manifestarticle_id: row.manifestarticle_id, + warehouse_id: row.warehouse_id ?? null, sku: row.sku ?? null, transaction_type: row.transaction_type, amount: row.amount, @@ -21,18 +22,18 @@ function shape(row) { } /** - * Lists ledger entries for a specific manifest article line, optionally filtered by transaction type. + * Lists ledger entries for a specific manifest article line, optionally filtered by transaction type and/or warehouse. * * @param {number} googleAuthId - The id of the Google auth user. * @param {number} manifestarticleId - The id of the manifest article line. - * @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string }. + * @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string, warehouse_id?: number, warehouseId?: number }. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise>} + * @returns {Promise>} * @throws {Error} If user not authorized, company not selected, or manifest article not found. * * @example - * const entries = await store.ledger.listByManifestarticleId(googleAuthId, 42, 'PAYMENT'); - * // => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] + * const entries = await store.ledger.listByManifestarticleId(googleAuthId, 42, 'DELIVERY'); + * // => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] */ export default function listByManifestarticleId(provider) { const access = resolveSelectedCompanyAccess(provider); @@ -41,6 +42,7 @@ export default function listByManifestarticleId(provider) { return async (googleAuthId, manifestarticleId, optionsOrType, tx) => { let transactionType = null; + let warehouseId = null; let explicitTx = tx; if (typeof optionsOrType === 'string') { @@ -54,6 +56,11 @@ export default function listByManifestarticleId(provider) { if (typeof rawType === 'string') { transactionType = rawType.trim() || null; } + const rawWarehouse = + optionsOrType.warehouse_id ?? optionsOrType.warehouseId; + if (rawWarehouse != null) { + warehouseId = Number(rawWarehouse) || null; + } } } @@ -78,6 +85,7 @@ export default function listByManifestarticleId(provider) { manifestarticleId: id, companyId, transactionType: transactionType || null, + warehouseId: warehouseId || null, }); return res.rows.map(shape); diff --git a/src/store/ledger/listBySku.js b/src/store/ledger/listBySku.js index 1719dd0..986fef2 100644 --- a/src/store/ledger/listBySku.js +++ b/src/store/ledger/listBySku.js @@ -7,6 +7,7 @@ function shape(row) { return { id: row.id, manifestarticle_id: row.manifestarticle_id, + warehouse_id: row.warehouse_id ?? null, sku: row.sku ?? null, transaction_type: row.transaction_type, amount: row.amount, @@ -21,18 +22,18 @@ function shape(row) { } /** - * Lists ledger entries for a given article SKU, optionally filtered by transaction type. + * Lists ledger entries for a given article SKU, optionally filtered by transaction type and/or warehouse. * * @param {number} googleAuthId - The id of the Google auth user. * @param {string} sku - The article SKU. - * @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string }. + * @param {string|object} [optionsOrType] - Transaction type string or options object { transaction_type?: string, transactionType?: string, warehouse_id?: number, warehouseId?: number }. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise>} + * @returns {Promise>} * @throws {Error} If user not authorized, company not selected, or SKU invalid. * * @example * const entries = await store.ledger.listBySku(googleAuthId, 'SKU-001', 'DELIVERY'); - * // => [{ id, manifestarticle_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] + * // => [{ id, manifestarticle_id, warehouse_id, sku, transaction_type, amount, transaction_text, hash_version, prev_hash, created_at }] */ export default function listBySku(provider) { const access = resolveSelectedCompanyAccess(provider); @@ -41,6 +42,7 @@ export default function listBySku(provider) { return async (googleAuthId, sku, optionsOrType, tx) => { let transactionType = null; + let warehouseId = null; let explicitTx = tx; if (typeof optionsOrType === 'string') { @@ -54,6 +56,11 @@ export default function listBySku(provider) { if (typeof rawType === 'string') { transactionType = rawType.trim() || null; } + const rawWarehouse = + optionsOrType.warehouse_id ?? optionsOrType.warehouseId; + if (rawWarehouse != null) { + warehouseId = Number(rawWarehouse) || null; + } } } @@ -69,6 +76,7 @@ export default function listBySku(provider) { sku: trimmedSku, companyId, transactionType: transactionType || null, + warehouseId: warehouseId || null, }); return res.rows.map(shape); diff --git a/src/store/numbers/list.js b/src/store/numbers/list.js index 7109f33..66fa439 100644 --- a/src/store/numbers/list.js +++ b/src/store/numbers/list.js @@ -9,7 +9,7 @@ import { import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; import makeRunner from '../runner.js'; -const DEFAULT_TYPES = ['sku', 'entity', 'delivery']; +const DEFAULT_TYPES = ['sku', 'entity', 'delivery', 'adjustment']; function shape(row) { return { diff --git a/src/store/printer/create.js b/src/store/printer/create.js new file mode 100644 index 0000000..8538349 --- /dev/null +++ b/src/store/printer/create.js @@ -0,0 +1,111 @@ +import { + CREATE as MSSQL_CREATE, + GET_NEXT_SORT as MSSQL_GET_NEXT_SORT, +} from '../../providers/mssql/queries/printer.js'; +import { + CREATE as SQLITE_CREATE, + GET_NEXT_SORT as SQLITE_GET_NEXT_SORT, +} from '../../providers/sqlite3/queries/printer.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +const VALID_TYPES = new Set(['esc/pos', 'zpl']); + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + address: row.address, + type: row.type, + model: row.model ?? null, + serial: row.serial ?? null, + sort: row.sort, + desc: row.desc ?? null, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; + const createSql = isSqlite ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates a new printer for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Printer input. + * @param {string} input.address - Printer address (network IP/host/path). + * @param {string} input.type - Printer type ("esc/pos" or "zpl"). + * @param {string} [input.model] - Optional printer model name. + * @param {string} [input.serial] - Optional printer serial number. + * @param {number} [input.sort] - Optional sort order (auto-increments if omitted). + * @param {string} [input.desc] - Optional description. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>} + * @throws {Error} If address is missing, type is invalid, or sort is invalid. + * + * @example + * const printer = await store.printer.create(googleAuthId, { + * address: '192.168.1.200', + * type: 'esc/pos', + * model: 'TM-T88VI', + * desc: 'Bar receipt printer', + * }); + */ + return async (googleAuthId, input = {}, tx) => { + const address = typeof input.address === 'string' ? input.address.trim() : ''; + if (!address) { + throw new Error('Address required'); + } + + const type = typeof input.type === 'string' ? input.type.trim() : ''; + if (!type || !VALID_TYPES.has(type)) { + throw new Error('Invalid printer type: must be "esc/pos" or "zpl"'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + let sort = input.sort; + if (sort === undefined || sort === null) { + const next = await runner.query(getSortSql, { companyId }); + sort = next.rows[0].sort; + } else if (!Number.isFinite(sort)) { + throw new Error('Invalid sort'); + } + + const model = optionalString(input.model); + const serial = optionalString(input.serial); + const desc = optionalString(input.desc); + + const result = await runner.query(createSql, { + companyId, + address, + type, + model, + serial, + sort, + desc, + }); + + const row = result.rows[0] ?? { + id: result.insertId, + company_id: companyId, + address, + type, + model, + serial, + sort, + desc, + }; + + return shape(row); + }; +} diff --git a/src/store/printer/delete.js b/src/store/printer/delete.js new file mode 100644 index 0000000..8a15b1f --- /dev/null +++ b/src/store/printer/delete.js @@ -0,0 +1,38 @@ +import { DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE } from '../../providers/mssql/queries/printer.js'; +import { DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE } from '../../providers/sqlite3/queries/printer.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function del(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE; + + /** + * Deletes a printer for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The printer ID to delete. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} + * @throws {Error} If printer is not found. + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Printer not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { + id: numericId, + companyId, + }); + + if (!result.rowsAffected) { + throw new Error('Printer not found'); + } + }; +} diff --git a/src/store/printer/getById.js b/src/store/printer/getById.js new file mode 100644 index 0000000..615bfab --- /dev/null +++ b/src/store/printer/getById.js @@ -0,0 +1,53 @@ +import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/printer.js'; +import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/printer.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + address: row.address, + type: row.type, + model: row.model ?? null, + serial: row.serial ?? null, + sort: row.sort, + desc: row.desc ?? null, + }; +} + +export default function getById(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + + /** + * Retrieves a printer by its ID for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The printer ID. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}|undefined>} + * + * @example + * const printer = await store.printer.getById(googleAuthId, 1); + * // => { id: 1, company_id: 1, address: '192.168.1.100', type: 'esc/pos', ... } + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + return undefined; + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { + id: numericId, + companyId, + }); + + const row = result.rows[0]; + return row ? shape(row) : undefined; + }; +} diff --git a/src/store/printer/list.js b/src/store/printer/list.js new file mode 100644 index 0000000..d0a8e08 --- /dev/null +++ b/src/store/printer/list.js @@ -0,0 +1,44 @@ +import { LIST_BY_COMPANY as MSSQL_LIST_BY_COMPANY } from '../../providers/mssql/queries/printer.js'; +import { LIST_BY_COMPANY as SQLITE_LIST_BY_COMPANY } from '../../providers/sqlite3/queries/printer.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + address: row.address, + type: row.type, + model: row.model ?? null, + serial: row.serial ?? null, + sort: row.sort, + desc: row.desc ?? null, + }; +} + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' + ? SQLITE_LIST_BY_COMPANY + : MSSQL_LIST_BY_COMPANY; + + /** + * Lists all printers for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} + * + * @example + * const printers = await store.printer.list(42); + * // => [{ id: 1, company_id: 1, address: '192.168.1.100', type: 'esc/pos', ... }] + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId }); + return result.rows.map(shape); + }; +} diff --git a/src/store/printer/order.js b/src/store/printer/order.js new file mode 100644 index 0000000..3103017 --- /dev/null +++ b/src/store/printer/order.js @@ -0,0 +1,76 @@ +import { + LIST_IDS_BY_COMPANY as MSSQL_LIST_IDS_BY_COMPANY, + UPDATE_SORT as MSSQL_UPDATE_SORT, +} from '../../providers/mssql/queries/printer.js'; +import { + LIST_IDS_BY_COMPANY as SQLITE_LIST_IDS_BY_COMPANY, + UPDATE_SORT as SQLITE_UPDATE_SORT, +} from '../../providers/sqlite3/queries/printer.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 printers of the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - The reorder input. + * @param {Array} input.printerIds - The printer ids in their new order. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{printerIds: Array}>} The applied order. + * @throws {Error} If the order is invalid. + */ + return async (googleAuthId, { printerIds } = {}, tx) => { + if (!Array.isArray(printerIds)) { + throw new Error('Invalid printer 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 (printerIds.length !== existing.size) { + throw new Error('Invalid printer order'); + } + + const seen = new Set(); + for (const printerId of printerIds) { + if ( + !Number.isFinite(printerId) || + !existing.has(printerId) || + seen.has(printerId) + ) { + throw new Error('Invalid printer order'); + } + seen.add(printerId); + } + + for (let sort = 0; sort < printerIds.length; sort++) { + await runner.query(updateSortSql, { + id: printerIds[sort], + companyId, + sort, + }); + } + + return { printerIds }; + }; + + if (tx) { + return runOrder(makeRunner(provider, tx)); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction(runOrder); + } + return runOrder(makeRunner(provider)); + }; +} diff --git a/src/store/printer/update.js b/src/store/printer/update.js new file mode 100644 index 0000000..51d6e33 --- /dev/null +++ b/src/store/printer/update.js @@ -0,0 +1,124 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID, + UPDATE as MSSQL_UPDATE, +} from '../../providers/mssql/queries/printer.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID, + UPDATE as SQLITE_UPDATE, +} from '../../providers/sqlite3/queries/printer.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +const VALID_TYPES = new Set(['esc/pos', 'zpl']); + +function shape(row) { + return { + id: row.id, + company_id: row.company_id ?? row.companyId, + address: row.address, + type: row.type, + model: row.model ?? null, + serial: row.serial ?? null, + sort: row.sort, + desc: row.desc ?? null, + }; +} + +function optionalString(value) { + if (value == null || value === '') return null; + const s = String(value).trim(); + return s || null; +} + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates a printer for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The printer ID to update. + * @param {object} input - Update payload. + * @param {string} [input.address] - The printer address. + * @param {string} [input.type] - The printer type ("esc/pos" or "zpl"). + * @param {string|null} [input.model] - The printer model. + * @param {string|null} [input.serial] - The printer serial. + * @param {number} [input.sort] - The sort order. + * @param {string|null} [input.desc] - The description. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>} + * @throws {Error} If printer not found, address is empty, type is invalid, or sort is invalid. + */ + return async (googleAuthId, id, input = {}, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Printer not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const existing = await runner.query(getSql, { id: numericId, companyId }); + const current = existing.rows[0]; + if (!current) { + throw new Error('Printer not found'); + } + + let address = current.address; + if (input.address !== undefined) { + const cleaned = typeof input.address === 'string' ? input.address.trim() : ''; + if (!cleaned) { + throw new Error('Address required'); + } + address = cleaned; + } + + let type = current.type; + if (input.type !== undefined) { + const cleaned = typeof input.type === 'string' ? input.type.trim() : ''; + if (!cleaned || !VALID_TYPES.has(cleaned)) { + throw new Error('Invalid printer type: must be "esc/pos" or "zpl"'); + } + type = cleaned; + } + + let sort = current.sort; + if (input.sort !== undefined) { + if (!Number.isFinite(input.sort)) { + throw new Error('Invalid sort'); + } + sort = input.sort; + } + + const model = input.model !== undefined ? optionalString(input.model) : (current.model ?? null); + const serial = input.serial !== undefined ? optionalString(input.serial) : (current.serial ?? null); + const desc = input.desc !== undefined ? optionalString(input.desc) : (current.desc ?? null); + + const result = await runner.query(updateSql, { + id: numericId, + companyId, + address, + type, + model, + serial, + sort, + desc, + }); + + const row = result.rows[0] ?? { + id: numericId, + company_id: companyId, + address, + type, + model, + serial, + sort, + desc, + }; + + return shape(row); + }; +} diff --git a/src/store/stock/adjust.js b/src/store/stock/adjust.js new file mode 100644 index 0000000..31b8c2f --- /dev/null +++ b/src/store/stock/adjust.js @@ -0,0 +1,190 @@ +import * as mssqlQueries from '../../providers/mssql/queries/manifestDoc.js'; +import * as sqliteQueries from '../../providers/sqlite3/queries/manifestDoc.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import resolveArticleBySku from '../article/resolveArticleBySku.js'; +import nextNumber from '../numbers/nextNumber.js'; +import ledgerAdd from '../ledger/add.js'; +import makeRunner from '../runner.js'; +import { parseDate } from '../manifestLines.js'; + +const ADJUSTMENT_TYPE = 'adjustment'; + +const GET_ARTICLE_NAME_SQL = ` +SELECT at.name +FROM article a +LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId +WHERE a.id = @articleId; +`; + +export default function adjust(provider) { + const access = resolveSelectedCompanyAccess(provider); + const resolveArticle = resolveArticleBySku(provider); + const allocateNumber = nextNumber(provider); + const addLedger = ledgerAdd(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const queries = isSqlite ? sqliteQueries : mssqlQueries; + + /** + * Adjusts article stock manually by creating an implicit manifest of type "adjustment", + * allocating a number from the "adjustment" number scheme, and appending to the ledger. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Adjustment payload. + * @param {string} [input.sku] - Article SKU (if single-item adjustment). + * @param {number} [input.warehouseId] - Warehouse ID (can also pass warehouse_id). + * @param {number} [input.amount] - Signed adjustment amount (+ to add, - to remove). + * @param {string} [input.text] - Optional adjustment reason/memo. + * @param {string} [input.externalNumber] - Optional external reference. + * @param {string|Date} [input.date] - Optional date (defaults to now). + * @param {Array} [input.lines] - Optional array of items to adjust in one manifest. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise<{manifest: {id: number, number: string, type: string, date: string, createdAt: string}, lines: Array, ledger: Array}>} + */ + return async (googleAuthId, input = {}, tx) => { + const rawLines = Array.isArray(input.lines) + ? input.lines + : [ + { + sku: input.sku, + warehouseId: input.warehouseId ?? input.warehouse_id, + amount: input.amount, + text: input.text ?? input.transactionText ?? input.transaction_text, + }, + ]; + + if (rawLines.length === 0) { + throw new Error('No items to adjust'); + } + + const defaultWarehouseId = input.warehouseId ?? input.warehouse_id; + const defaultText = input.text ?? input.transactionText ?? input.transaction_text; + const externalNumber = input.externalNumber?.trim?.() ? input.externalNumber.trim() : null; + const date = input.date ? parseDate(input.date) ?? new Date() : new Date(); + const createdAt = new Date(); + + const normalizedLines = rawLines.map((line, idx) => { + const sku = typeof line.sku === 'string' ? line.sku.trim() : ''; + if (!sku) { + throw new Error(`SKU required for item ${idx + 1}`); + } + + const rawWhId = line.warehouseId ?? line.warehouse_id ?? defaultWarehouseId; + const warehouseId = Number(rawWhId); + if (!Number.isInteger(warehouseId) || warehouseId <= 0) { + throw new Error(`Warehouse required for item ${idx + 1} (${sku})`); + } + + const amount = Number(line.amount); + if (!Number.isFinite(amount) || amount === 0) { + throw new Error(`Invalid amount for item ${idx + 1} (${sku}): must be non-zero number`); + } + + const text = (line.text ?? line.transactionText ?? line.transaction_text ?? defaultText)?.trim?.() || + (amount > 0 ? `Manual stock addition (+${amount})` : `Manual stock removal (${amount})`); + + return { + sku, + warehouseId, + amount, + text, + }; + }); + + const run = async (runner, activeTx) => { + const { companyId } = await access(googleAuthId, runner); + + // Validate all articles exist and belong to the user's company + const resolvedArticles = []; + for (const item of normalizedLines) { + const resolved = await resolveArticle(googleAuthId, item.sku, activeTx); + const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, { + articleId: resolved.articleId, + languageId: resolved.languageId, + }); + const articleName = nameRes.rows[0]?.name ?? item.sku; + resolvedArticles.push({ ...item, ...resolved, articleName }); + } + + // Allocate unique adjustment number using the dedicated adjustment number scheme + const number = await allocateNumber(companyId, ADJUSTMENT_TYPE, activeTx); + + // Create the implicit adjustment manifest + const manifestRes = await runner.query(queries.CREATE_MANIFEST, { + companyId, + number, + type: ADJUSTMENT_TYPE, + entityId: null, + externalNumber, + date, + createdAt, + }); + + const manifestId = manifestRes.rows[0]?.id ?? manifestRes.insertId; + + const createdLines = []; + const createdLedgerEntries = []; + + for (const item of resolvedArticles) { + // Create manifestarticle row + const lineRes = await runner.query(queries.CREATE_LINE, { + manifestId, + sku: item.sku, + name: item.articleName, + pu: 1, + amount: Math.abs(item.amount), + net: 0, + comment: item.text, + rebate: 0, + taxrate: 0, + }); + + const manifestarticleId = lineRes.rows[0]?.id ?? lineRes.insertId; + + // Append to ledger + const ledgerRow = await addLedger( + googleAuthId, + { + manifestarticle_id: manifestarticleId, + warehouse_id: item.warehouseId, + transaction_type: ADJUSTMENT_TYPE, + amount: item.amount, + transaction_text: item.text, + }, + activeTx, + ); + + createdLines.push({ + manifestarticle_id: manifestarticleId, + sku: item.sku, + name: item.articleName, + warehouse_id: item.warehouseId, + amount: item.amount, + text: item.text, + }); + + createdLedgerEntries.push(ledgerRow); + } + + return { + manifest: { + id: manifestId, + number, + type: ADJUSTMENT_TYPE, + date: date.toISOString(), + createdAt: createdAt.toISOString(), + externalNumber, + }, + lines: createdLines, + ledger: createdLedgerEntries, + }; + }; + + if (tx) { + return run(makeRunner(provider, tx), tx); + } + if (typeof provider?.transaction === 'function') { + return provider.transaction((runner, activeTx) => run(runner, activeTx)); + } + return run(makeRunner(provider)); + }; +} diff --git a/src/store/warehouse/create.js b/src/store/warehouse/create.js new file mode 100644 index 0000000..8654d43 --- /dev/null +++ b/src/store/warehouse/create.js @@ -0,0 +1,59 @@ +import { CREATE as MSSQL_CREATE } from '../../providers/mssql/queries/warehouse.js'; +import { CREATE as SQLITE_CREATE } 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 }; +} + +export default function create(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_CREATE : MSSQL_CREATE; + + /** + * Creates a new warehouse for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} input - Warehouse input. + * @param {string} input.name - The warehouse name. + * @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. + * + * @example + * const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); + * // => { id: 1, name: 'Hauptlager' } + */ + return async (googleAuthId, { name } = {}, tx) => { + 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); + + try { + const result = await runner.query(sql, { + companyId, + name: cleaned, + }); + + const row = result.rows[0] ?? { id: result.insertId, name: cleaned }; + return shape(row); + } catch (err) { + if ( + isUniqueConstraintError(err) || + err?.number === 2627 || + err?.number === 2601 || + err?.code === 'SQLITE_CONSTRAINT_UNIQUE' + ) { + throw new Error('Warehouse already exists'); + } + throw err; + } + }; +} diff --git a/src/store/warehouse/delete.js b/src/store/warehouse/delete.js new file mode 100644 index 0000000..e67a87f --- /dev/null +++ b/src/store/warehouse/delete.js @@ -0,0 +1,62 @@ +import { + DELETE_BY_COMPANY_AND_ID as MSSQL_DELETE, + CHECK_LEDGER_USAGE as MSSQL_CHECK_USAGE, +} from '../../providers/mssql/queries/warehouse.js'; +import { + DELETE_BY_COMPANY_AND_ID as SQLITE_DELETE, + CHECK_LEDGER_USAGE as SQLITE_CHECK_USAGE, +} from '../../providers/sqlite3/queries/warehouse.js'; +import { isForeignKeyConstraintError } from '../../providers/provider.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +export default function del(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE; + const checkUsageSql = isSqlite ? SQLITE_CHECK_USAGE : MSSQL_CHECK_USAGE; + + /** + * Deletes a warehouse for the user's selected company, unless referenced in the ledger. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {number} id - The warehouse ID to delete. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} + * @throws {Error} If warehouse not found or is in use in the ledger. + * + * @example + * await store.warehouse.delete(googleAuthId, 1); + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + throw new Error('Warehouse not found'); + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + // Pre-check if warehouse is referenced in the ledger + const usage = await runner.query(checkUsageSql, { warehouseId: numericId }); + if (usage.rows && usage.rows.length > 0) { + throw new Error('Warehouse in use in ledger'); + } + + try { + const result = await runner.query(deleteSql, { + id: numericId, + companyId, + }); + + if (!result.rowsAffected) { + throw new Error('Warehouse not found'); + } + } catch (err) { + if (isForeignKeyConstraintError(err)) { + throw new Error('Warehouse in use in ledger'); + } + throw err; + } + }; +} diff --git a/src/store/warehouse/getById.js b/src/store/warehouse/getById.js new file mode 100644 index 0000000..fda077e --- /dev/null +++ b/src/store/warehouse/getById.js @@ -0,0 +1,44 @@ +import { GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID } from '../../providers/mssql/queries/warehouse.js'; +import { GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID } from '../../providers/sqlite3/queries/warehouse.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { id: row.id, name: row.name }; +} + +export default function getById(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + + /** + * Retrieves a warehouse by its ID for the user's selected company. + * + * @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. + * + * @example + * const wh = await store.warehouse.getById(googleAuthId, 1); + * // => { id: 1, name: 'Hauptlager' } + */ + return async (googleAuthId, id, tx) => { + const numericId = Number(id); + if (!Number.isInteger(numericId) || numericId <= 0) { + return undefined; + } + + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { + id: numericId, + companyId, + }); + + const row = result.rows[0]; + return row ? shape(row) : undefined; + }; +} diff --git a/src/store/warehouse/list.js b/src/store/warehouse/list.js new file mode 100644 index 0000000..c9b6917 --- /dev/null +++ b/src/store/warehouse/list.js @@ -0,0 +1,33 @@ +import { LIST_BY_COMPANY as MSSQL_LIST } from '../../providers/mssql/queries/warehouse.js'; +import { LIST_BY_COMPANY as SQLITE_LIST } from '../../providers/sqlite3/queries/warehouse.js'; +import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyAccess.js'; +import makeRunner from '../runner.js'; + +function shape(row) { + return { id: row.id, name: row.name }; +} + +export default function list(provider) { + const access = resolveSelectedCompanyAccess(provider); + const sql = + provider?.dialect === 'sqlite3' ? SQLITE_LIST : MSSQL_LIST; + + /** + * Lists all warehouses for the user's selected company. + * + * @param {number} googleAuthId - The id of the Google auth user. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise>} List of warehouses. + * + * @example + * const warehouses = await store.warehouse.list(googleAuthId); + * // => [{ id: 1, name: 'Hauptlager' }] + */ + return async (googleAuthId, tx) => { + const runner = makeRunner(provider, tx); + const { companyId } = await access(googleAuthId, runner); + + const result = await runner.query(sql, { companyId }); + return result.rows.map(shape); + }; +} diff --git a/src/store/warehouse/update.js b/src/store/warehouse/update.js new file mode 100644 index 0000000..cfb0055 --- /dev/null +++ b/src/store/warehouse/update.js @@ -0,0 +1,77 @@ +import { + GET_BY_COMPANY_AND_ID as MSSQL_GET_BY_ID, + UPDATE_BY_ID as MSSQL_UPDATE, +} from '../../providers/mssql/queries/warehouse.js'; +import { + GET_BY_COMPANY_AND_ID as SQLITE_GET_BY_ID, + UPDATE_BY_ID as SQLITE_UPDATE, +} 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 }; +} + +export default function update(provider) { + const access = resolveSelectedCompanyAccess(provider); + const isSqlite = provider?.dialect === 'sqlite3'; + const getSql = isSqlite ? SQLITE_GET_BY_ID : MSSQL_GET_BY_ID; + const updateSql = isSqlite ? SQLITE_UPDATE : MSSQL_UPDATE; + + /** + * Updates a warehouse name 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 {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. + * + * @example + * const updated = await store.warehouse.update(googleAuthId, 1, { name: 'Zentrallager' }); + * // => { id: 1, name: 'Zentrallager' } + */ + return async (googleAuthId, id, { name } = {}, 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]) { + throw new Error('Warehouse not found'); + } + + try { + await runner.query(updateSql, { + id: numericId, + companyId, + name: cleaned, + }); + + return shape({ id: numericId, name: cleaned }); + } catch (err) { + if ( + isUniqueConstraintError(err) || + err?.number === 2627 || + err?.number === 2601 || + err?.code === 'SQLITE_CONSTRAINT_UNIQUE' + ) { + throw new Error('Warehouse already exists'); + } + throw err; + } + }; +}