diff --git a/docs/api/README.md b/docs/api/README.md index 3f86b96..667c7dc 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -43,6 +43,10 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route | [`routes/article/delete-image.md`](routes/article/delete-image.md) | `DELETE /article/{sku}/image/{fullHash}` | | [`routes/article/order-images.md`](routes/article/order-images.md) | `PUT /article/{sku}/images/order` | | [`routes/article/set-prices.md`](routes/article/set-prices.md) | `PUT /article/{sku}/prices` | +| [`routes/article/supplier/list.md`](routes/article/supplier/list.md) | `GET /article/{sku}/supplier` | +| [`routes/article/supplier/create.md`](routes/article/supplier/create.md) | `POST /article/{sku}/supplier` | +| [`routes/article/supplier/delete.md`](routes/article/supplier/delete.md) | `DELETE /article/{sku}/supplier/{id}` | +| [`routes/article/supplier/update.md`](routes/article/supplier/update.md) | `PATCH /article/{sku}/supplier/{id}` | | [`routes/article/set-weight.md`](routes/article/set-weight.md) | `PUT /article/{sku}/weight` | | [`routes/articleIndex.md`](routes/articleIndex.md) | `GET /articleIndex` | | [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | @@ -132,7 +136,7 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A | Signal | Emitted by | |--------|------------| -| `article-` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `PUT /article/{sku}/weight` | +| `article-` | `PUT /article/{sku}/brand`, `PUT /article/{sku}/categories`, `PUT /article/{sku}/eans`, `POST /article/{sku}/image`, `DELETE /article/{sku}/image/{fullHash}`, `PUT /article/{sku}/images/order`, `PUT /article/{sku}/prices`, `POST /article/{sku}/supplier`, `DELETE /article/{sku}/supplier/{id}`, `PATCH /article/{sku}/supplier/{id}`, `PUT /article/{sku}/weight` | | `articles` | `POST /article`, `DELETE /article/{sku}`, `PATCH /article/{sku}` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` | diff --git a/docs/api/openapi.json b/docs/api/openapi.json index 4bb947b..c881230 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -762,7 +762,7 @@ "Quote" ], "summary": "Create quote", - "description": "Creates a quote with header and lines in one transaction.\n`date` is the document date (valid-until).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.\n", + "description": "Creates a quote with header and lines in one transaction.\n`date` is the document date (valid-until).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.\n", "security": [ { "cookieAuth": [] @@ -812,7 +812,7 @@ "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -2014,7 +2014,7 @@ "Order" ], "summary": "Create order", - "description": "Creates a sales order with header and lines in one transaction.\n`date` is the document date (expected delivery).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.\n", + "description": "Creates a sales order with header and lines in one transaction.\n`date` is the document date (expected delivery).\n`entityId` must be a company customer (not Lieferanten/POS), or null.\nEach line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`.\n", "security": [ { "cookieAuth": [] @@ -2064,7 +2064,7 @@ "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -4227,7 +4227,7 @@ "Delivery" ], "summary": "Create delivery", - "description": "Creates a goods receipt with header and lines in one transaction.\nThe manifest `number` is allocated automatically via\n`nextNumber('delivery')`; the user-supplied reference is stored as\n`externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and\n`tax` (rate). `pu` defaults to `1`; `rebate` defaults to `0`.\n", + "description": "Creates a goods receipt with header and lines in one transaction.\nThe manifest `number` is allocated automatically via\n`nextNumber('delivery')`; the user-supplied reference is stored as\n`externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and\n`taxrate` (rate). `pu` defaults to `1`; `rebate` defaults to `0`.\n", "security": [ { "cookieAuth": [] @@ -4276,7 +4276,7 @@ "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -6735,6 +6735,460 @@ } } }, + "/article/{sku}/supplier": { + "get": { + "tags": [ + "Article" + ], + "summary": "List article suppliers", + "description": "Fetches all suppliers linked to the specified article SKU.", + "security": [ + { + "cookieAuth": [] + } + ], + "x-markdown": "article/supplier/list.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "responses": { + "200": { + "description": "List of suppliers linked to the article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "suppliers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ArticleSupplier" + } + } + } + } + } + } + }, + "400": { + "description": "No company selected or invalid sku", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Article or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "post": { + "tags": [ + "Article" + ], + "summary": "Assign supplier to article", + "description": "Assigns a supplier to an article with optional purchase price, stock, and supplier SKU.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-signals": [ + "article-" + ], + "x-markdown": "article/supplier/create.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "entityId" + ], + "properties": { + "entityId": { + "type": "integer", + "description": "Supplier entity ID (in Lieferanten group)" + }, + "sku": { + "type": "string", + "nullable": true, + "description": "Supplier's item number" + }, + "pu": { + "type": "number", + "default": 1, + "description": "Packaging unit (VPE)" + }, + "stock": { + "type": "number", + "nullable": true, + "description": "Supplier stock" + }, + "netPrice": { + "type": "number", + "nullable": true, + "description": "Net purchase price" + }, + "taxId": { + "type": "integer", + "nullable": true, + "description": "Tax ID for purchase price" + }, + "taxRate": { + "type": "number", + "nullable": true, + "description": "Tax rate in percent" + } + }, + "example": { + "entityId": 88, + "sku": "MET-44910", + "pu": 1, + "stock": 140, + "netPrice": 4.2, + "taxId": 1 + } + } + } + } + }, + "responses": { + "201": { + "description": "Created supplier assignment", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "supplier": { + "$ref": "#/components/schemas/ArticleSupplier" + } + } + } + } + } + }, + "400": { + "description": "entityId required, invalid pu, invalid stock, invalid price, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Article, supplier, or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "409": { + "description": "Supplier already assigned", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, + "/article/{sku}/supplier/{id}": { + "patch": { + "tags": [ + "Article" + ], + "summary": "Update article supplier assignment", + "description": "Updates an existing article-supplier association (supplier SKU, packaging unit, stock, purchase price).\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-signals": [ + "article-" + ], + "x-markdown": "article/supplier/update.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "articlesupplier assignment ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sku": { + "type": "string", + "nullable": true + }, + "pu": { + "type": "number" + }, + "stock": { + "type": "number", + "nullable": true + }, + "netPrice": { + "type": "number", + "nullable": true + }, + "taxId": { + "type": "integer", + "nullable": true + }, + "taxRate": { + "type": "number", + "nullable": true + } + }, + "example": { + "sku": "MET-44910-B", + "pu": 6, + "stock": 95, + "netPrice": 22.5 + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated supplier assignment", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "supplier": { + "$ref": "#/components/schemas/ArticleSupplier" + } + } + } + } + } + }, + "400": { + "description": "Invalid pu, invalid stock, invalid price, invalid id, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Supplier assignment, article, or company not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + }, + "delete": { + "tags": [ + "Article" + ], + "summary": "Remove supplier assignment from article", + "description": "Removes a supplier assignment from the article and cleans up its associated purchase price.\n", + "security": [ + { + "cookieAuth": [] + } + ], + "x-signals": [ + "article-" + ], + "x-markdown": "article/supplier/delete.md", + "parameters": [ + { + "in": "path", + "name": "sku", + "required": true, + "schema": { + "type": "string" + }, + "description": "Article SKU" + }, + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "integer" + }, + "description": "articlesupplier assignment ID" + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "400": { + "description": "Invalid id, invalid sku, or no company selected", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Not authorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "404": { + "description": "Supplier assignment or article not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + }, "/article": { "post": { "tags": [ @@ -8680,6 +9134,74 @@ } } }, + "ArticleSupplierPrice": { + "type": "object", + "required": [ + "net", + "taxId" + ], + "properties": { + "net": { + "type": "number", + "description": "Net purchase price" + }, + "taxId": { + "type": "integer", + "description": "Tax ID" + }, + "taxRate": { + "type": "number", + "nullable": true, + "description": "Tax rate in percent" + } + } + }, + "ArticleSupplier": { + "type": "object", + "required": [ + "id", + "articleId", + "entityId", + "pu" + ], + "properties": { + "id": { + "type": "integer" + }, + "articleId": { + "type": "integer" + }, + "entityId": { + "type": "integer" + }, + "supplierName": { + "type": "string", + "nullable": true + }, + "sku": { + "type": "string", + "nullable": true, + "description": "Supplier-specific SKU (Lieferanten-Art.-Nr.)" + }, + "pu": { + "type": "number", + "description": "Packaging unit (Verpackungseinheit / VPE)" + }, + "stock": { + "type": "number", + "nullable": true, + "description": "Supplier stock" + }, + "priceId": { + "type": "integer", + "nullable": true + }, + "price": { + "$ref": "#/components/schemas/ArticleSupplierPrice", + "nullable": true + } + } + }, "Brand": { "type": "object", "required": [ @@ -8952,7 +9474,7 @@ "amount", "net", "rebate", - "tax", + "taxrate", "comment" ], "properties": { @@ -8981,9 +9503,9 @@ "rebate": { "type": "number" }, - "tax": { + "taxrate": { "type": "number", - "description": "tax.rate (manifestarticle.taxrate)" + "description": "Tax rate in percent (e.g. 19)" }, "comment": { "type": "string", @@ -8996,7 +9518,7 @@ "required": [ "amount", "net", - "tax" + "taxrate" ], "description": "Line item input. Requires `sku` or `name`.", "properties": { @@ -9020,9 +9542,9 @@ "net": { "type": "number" }, - "tax": { + "taxrate": { "type": "number", - "description": "Tax rate (resolved via tax.ensure)" + "description": "Tax rate in percent (resolved via tax.ensure)" }, "rebate": { "type": "number", @@ -9065,7 +9587,7 @@ "amount", "net", "rebate", - "tax", + "taxrate", "comment" ], "description": "Line shape for quote and order", @@ -9095,9 +9617,9 @@ "rebate": { "type": "number" }, - "tax": { + "taxrate": { "type": "number", - "description": "tax.rate (manifestarticle.taxrate)" + "description": "Tax rate in percent (e.g. 19)" }, "comment": { "type": "string", @@ -9110,7 +9632,7 @@ "required": [ "amount", "net", - "tax" + "taxrate" ], "description": "Line item input. Requires `sku` or `name`.", "properties": { @@ -9134,9 +9656,9 @@ "net": { "type": "number" }, - "tax": { + "taxrate": { "type": "number", - "description": "Tax rate (resolved via tax.ensure)" + "description": "Tax rate in percent (resolved via tax.ensure)" }, "rebate": { "type": "number", diff --git a/docs/api/routes/article/supplier/create.md b/docs/api/routes/article/supplier/create.md new file mode 100644 index 0000000..e591022 --- /dev/null +++ b/docs/api/routes/article/supplier/create.md @@ -0,0 +1,69 @@ +# `POST /article/{sku}/supplier` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Assign supplier to article + +Assigns a supplier to an article with optional purchase price, stock, and supplier SKU. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Body (JSON) + +```json +{ + "entityId": 88, + "sku": "MET-44910", + "pu": 1, + "stock": 140, + "netPrice": 4.2, + "taxId": 1 +} +``` + +## Signals + +On success, emits SSE signal: `article-` (see [`GET /events`](../events.md)). + +## Response `201` + +Created supplier assignment + +```json +{ + "supplier": { + "id": 1, + "articleId": 1, + "entityId": 1, + "supplierName": "…", + "sku": "…", + "pu": 1, + "stock": 1, + "priceId": 1, + "price": { + "net": 1, + "taxId": 1, + "taxRate": 1 + } + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | entityId required, invalid pu, invalid stock, invalid price, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Article, supplier, or company not found — `{"error":"…"}` | +| 409 | Supplier already assigned — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/supplier/delete.md b/docs/api/routes/article/supplier/delete.md new file mode 100644 index 0000000..98a95a6 --- /dev/null +++ b/docs/api/routes/article/supplier/delete.md @@ -0,0 +1,38 @@ +# `DELETE /article/{sku}/supplier/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Remove supplier assignment from article + +Removes a supplier assignment from the article and cleans up its associated purchase price. + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | +| `id` | yes | integer | articlesupplier assignment ID | + +## Signals + +On success, emits SSE signal: `article-` (see [`GET /events`](../events.md)). + +## Response `204` + +Deleted + +_No content_ + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid id, invalid sku, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Supplier assignment or article not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/supplier/list.md b/docs/api/routes/article/supplier/list.md new file mode 100644 index 0000000..a9f12e1 --- /dev/null +++ b/docs/api/routes/article/supplier/list.md @@ -0,0 +1,52 @@ +# `GET /article/{sku}/supplier` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +List article suppliers + +Fetches all suppliers linked to the specified article SKU. + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | + +## Response `200` + +List of suppliers linked to the article + +```json +{ + "suppliers": [ + { + "id": 1, + "articleId": 1, + "entityId": 1, + "supplierName": "…", + "sku": "…", + "pu": 1, + "stock": 1, + "priceId": 1, + "price": { + "net": 1, + "taxId": 1, + "taxRate": 1 + } + } + ] +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | No company selected or invalid sku — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Article or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/article/supplier/update.md b/docs/api/routes/article/supplier/update.md new file mode 100644 index 0000000..a2e3f0c --- /dev/null +++ b/docs/api/routes/article/supplier/update.md @@ -0,0 +1,67 @@ +# `PATCH /article/{sku}/supplier/{id}` + +Protected. Requires a valid session cookie (`credentials: 'include'`). + +Update article supplier assignment + +Updates an existing article-supplier association (supplier SKU, packaging unit, stock, purchase price). + + +## Path parameters + +| Param | Required | Type | Description | +|-------|----------|------|-------------| +| `sku` | yes | string | Article SKU | +| `id` | yes | integer | articlesupplier assignment ID | + +## Body (JSON) + +```json +{ + "sku": "MET-44910-B", + "pu": 6, + "stock": 95, + "netPrice": 22.5 +} +``` + +## Signals + +On success, emits SSE signal: `article-` (see [`GET /events`](../events.md)). + +## Response `200` + +Updated supplier assignment + +```json +{ + "supplier": { + "id": 1, + "articleId": 1, + "entityId": 1, + "supplierName": "…", + "sku": "…", + "pu": 1, + "stock": 1, + "priceId": 1, + "price": { + "net": 1, + "taxId": 1, + "taxRate": 1 + } + } +} +``` + +## Errors + +| Status | Description | +|--------|-------------| +| 400 | Invalid pu, invalid stock, invalid price, invalid id, or no company selected — `{"error":"…"}` | +| 401 | Unauthorized — `{"error":"…"}` | +| 403 | Not authorized — `{"error":"…"}` | +| 404 | Supplier assignment, article, or company not found — `{"error":"…"}` | + +--- + +_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._ diff --git a/docs/api/routes/delivery/create.md b/docs/api/routes/delivery/create.md index 9052b58..0ce285a 100644 --- a/docs/api/routes/delivery/create.md +++ b/docs/api/routes/delivery/create.md @@ -8,7 +8,7 @@ Creates a goods receipt with header and lines in one transaction. The manifest `number` is allocated automatically via `nextNumber('delivery')`; the user-supplied reference is stored as `externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and -`tax` (rate). `pu` defaults to `1`; `rebate` defaults to `0`. +`taxrate` (rate). `pu` defaults to `1`; `rebate` defaults to `0`. ## Body (JSON) @@ -25,7 +25,7 @@ The manifest `number` is allocated automatically via "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -53,7 +53,7 @@ Created delivery "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/delivery/get.md b/docs/api/routes/delivery/get.md index 089b64e..f09ae75 100644 --- a/docs/api/routes/delivery/get.md +++ b/docs/api/routes/delivery/get.md @@ -34,7 +34,7 @@ Delivery "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/delivery/update.md b/docs/api/routes/delivery/update.md index 50d01a5..4bc0677 100644 --- a/docs/api/routes/delivery/update.md +++ b/docs/api/routes/delivery/update.md @@ -47,7 +47,7 @@ Updated delivery "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/order/create.md b/docs/api/routes/order/create.md index 46ce39a..9a260a5 100644 --- a/docs/api/routes/order/create.md +++ b/docs/api/routes/order/create.md @@ -7,7 +7,7 @@ Create order Creates a sales order with header and lines in one transaction. `date` is the document date (expected delivery). `entityId` must be a company customer (not Lieferanten/POS), or null. -Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. +Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. ## Body (JSON) @@ -24,7 +24,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defa "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -51,7 +51,7 @@ Created order "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/order/get.md b/docs/api/routes/order/get.md index 8884458..aa03d9d 100644 --- a/docs/api/routes/order/get.md +++ b/docs/api/routes/order/get.md @@ -33,7 +33,7 @@ Order "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/order/update.md b/docs/api/routes/order/update.md index a5bf854..a8dafcd 100644 --- a/docs/api/routes/order/update.md +++ b/docs/api/routes/order/update.md @@ -45,7 +45,7 @@ Updated order "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/quote/create.md b/docs/api/routes/quote/create.md index d04b77e..aba0aa4 100644 --- a/docs/api/routes/quote/create.md +++ b/docs/api/routes/quote/create.md @@ -7,7 +7,7 @@ Create quote Creates a quote with header and lines in one transaction. `date` is the document date (valid-until). `entityId` must be a company customer (not Lieferanten/POS), or null. -Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. +Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. ## Body (JSON) @@ -24,7 +24,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defa "pu": 1, "amount": 10, "net": 99.5, - "tax": 19 + "taxrate": 19 } ] } @@ -51,7 +51,7 @@ Created quote "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/quote/get.md b/docs/api/routes/quote/get.md index 9a18c19..dd17cff 100644 --- a/docs/api/routes/quote/get.md +++ b/docs/api/routes/quote/get.md @@ -33,7 +33,7 @@ Quote "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/docs/api/routes/quote/update.md b/docs/api/routes/quote/update.md index 3f5271c..8670553 100644 --- a/docs/api/routes/quote/update.md +++ b/docs/api/routes/quote/update.md @@ -45,7 +45,7 @@ Updated quote "amount": 1, "net": 1, "rebate": 1, - "tax": 1, + "taxrate": 1, "comment": "…" } ] diff --git a/package-lock.json b/package-lock.json index 7b3e8e8..397a9e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,9 +59,9 @@ } }, "node_modules/@alcalzone/ansi-tokenize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", - "integrity": "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz", + "integrity": "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg==", "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", @@ -342,33 +342,33 @@ } }, "node_modules/@azure/msal-browser": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.21.0.tgz", - "integrity": "sha512-80OcuXDErmcEDAIH9pBtSqBsed2sPT/IWmbG3xHLoPMl5zc8TINd6SlJAbVSmN5huGa3xGAg5qR7VnpaIEK0Zw==", + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.22.0.tgz", + "integrity": "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ==", "license": "MIT", "dependencies": { - "@azure/msal-common": "16.14.0" + "@azure/msal-common": "16.14.1" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "16.14.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.0.tgz", - "integrity": "sha512-A4rb55hI86Q9tBl/+jBj7TMz7iX2RFgQs/nExFzcAtoI/BFRVdaH5SL/MivrYD7qvweMpN8AgVvVMHV8UBYxew==", + "version": "16.14.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", + "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.0.tgz", - "integrity": "sha512-PU0l3CsEZVm2K8vZNFxvgmM5d0Yx/sVL/axWW0hJBoeyiB6Q/uv7hXORb7EdzlNxvehsWEF82/o+/sXlIo+UIw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", + "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", "license": "MIT", "dependencies": { - "@azure/msal-common": "16.14.0", + "@azure/msal-common": "16.14.1", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -969,12 +969,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", - "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", + "version": "22.20.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz", + "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==", "license": "MIT", "dependencies": { - "undici-types": "~8.9.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/readable-stream": { @@ -1855,9 +1855,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -2087,9 +2087,9 @@ } }, "node_modules/google-auth-library": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.2.tgz", - "integrity": "sha512-vzpgPutxrghPsnjrjpzLX2bdv8IOL719Rh0oEjGnQu8YCIbnbMuTTQ5zU9LcKvLdOPgCxBwppbvnhgW90Qna5Q==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.1.0.tgz", + "integrity": "sha512-YIBq5KNoHQrZ9PM4PeodDBqqBGlSTIxh1zibN8u3zNIO9UJg8gvKuCpdKV7fWXhsZ2zRkU6FhGrgKNgLJ2IRgw==", "license": "Apache-2.0", "dependencies": { "base64-js": "^1.3.0", @@ -3715,9 +3715,9 @@ } }, "node_modules/undici-types": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", - "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, "node_modules/unpipe": { diff --git a/src/api/openapi.js b/src/api/openapi.js index 25ebbed..656cbab 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -108,6 +108,50 @@ * format: date-time * text: * type: string + * ArticleSupplierPrice: + * type: object + * required: [net, taxId] + * properties: + * net: + * type: number + * description: Net purchase price + * taxId: + * type: integer + * description: Tax ID + * taxRate: + * type: number + * nullable: true + * description: Tax rate in percent + * ArticleSupplier: + * type: object + * required: [id, articleId, entityId, pu] + * properties: + * id: + * type: integer + * articleId: + * type: integer + * entityId: + * type: integer + * supplierName: + * type: string + * nullable: true + * sku: + * type: string + * nullable: true + * description: Supplier-specific SKU (Lieferanten-Art.-Nr.) + * pu: + * type: number + * description: Packaging unit (Verpackungseinheit / VPE) + * stock: + * type: number + * nullable: true + * description: Supplier stock + * priceId: + * type: integer + * nullable: true + * price: + * $ref: '#/components/schemas/ArticleSupplierPrice' + * nullable: true * Brand: * type: object * required: [id, name] @@ -264,7 +308,7 @@ * format: date-time * DeliveryLine: * type: object - * required: [id, sku, name, pu, amount, net, rebate, tax, comment] + * required: [id, sku, name, pu, amount, net, rebate, taxrate, comment] * properties: * id: * type: integer @@ -284,15 +328,15 @@ * type: number * rebate: * type: number - * tax: + * taxrate: * type: number - * description: tax.rate (manifestarticle.taxrate) + * description: Tax rate in percent (e.g. 19) * comment: * type: string * nullable: true * DeliveryLineInput: * type: object - * required: [amount, net, tax] + * required: [amount, net, taxrate] * description: Line item input. Requires `sku` or `name`. * properties: * sku: @@ -310,9 +354,9 @@ * type: number * net: * type: number - * tax: + * taxrate: * type: number - * description: Tax rate (resolved via tax.ensure) + * description: Tax rate in percent (resolved via tax.ensure) * rebate: * type: number * description: Defaults to 0 @@ -331,7 +375,7 @@ * $ref: '#/components/schemas/DeliveryLine' * ManifestLine: * type: object - * required: [id, sku, name, pu, amount, net, rebate, tax, comment] + * required: [id, sku, name, pu, amount, net, rebate, taxrate, comment] * description: Line shape for quote and order * properties: * id: @@ -352,15 +396,15 @@ * type: number * rebate: * type: number - * tax: + * taxrate: * type: number - * description: tax.rate (manifestarticle.taxrate) + * description: Tax rate in percent (e.g. 19) * comment: * type: string * nullable: true * ManifestLineInput: * type: object - * required: [amount, net, tax] + * required: [amount, net, taxrate] * description: Line item input. Requires `sku` or `name`. * properties: * sku: @@ -378,9 +422,9 @@ * type: number * net: * type: number - * tax: + * taxrate: * type: number - * description: Tax rate (resolved via tax.ensure) + * description: Tax rate in percent (resolved via tax.ensure) * rebate: * type: number * description: Defaults to 0 diff --git a/src/api/routes/articlesupplier.js b/src/api/routes/articlesupplier.js new file mode 100644 index 0000000..da6040f --- /dev/null +++ b/src/api/routes/articlesupplier.js @@ -0,0 +1,487 @@ +import * as db from '../../db/articlesupplier.js'; +import { emit } from '../sse.js'; + +function requireSelectedCompany(req) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) { + return { error: 'No company selected' }; + } + return { companyId }; +} + +function parseSku(value) { + const sku = typeof value === 'string' ? value.trim() : ''; + if (!sku) return { error: 'sku is required' }; + return { sku: decodeURIComponent(sku) }; +} + +function parsePositiveInt(value, field) { + const id = Number(value); + if (!Number.isInteger(id) || id <= 0) { + return { error: `${field} is required` }; + } + return { id }; +} + +function emitArticleSignal(req, sku) { + const companyId = req.user?.selectedCompanyId; + if (companyId == null) return; + emit(`article-${sku}`, { companyId }); +} + +function articleSupplierErrorStatus(error) { + if (error.message === 'No company selected') return 400; + if (error.message === 'Not authorized') return 403; + if (error.message === 'Company not found') return 404; + if (error.message === 'Article not found') return 404; + if (error.message === 'Supplier not found') return 404; + if (error.message === 'Supplier assignment not found') return 404; + if (error.message === 'Supplier already assigned') return 409; + if (error.message === 'Supplier required' || error.message === 'entityId is required') return 400; + if (error.message === 'Invalid pu' || error.message === 'pu must be greater than 0') return 400; + if (error.message === 'Invalid stock') return 400; + if (error.message === 'Invalid price') return 400; + if (error.message === 'sku is required') return 400; + if (error.message === 'id is required') return 400; + return 500; +} + +/** + * Registers article supplier routes under `/article/:sku/supplier`. + * @param {import('express').Application} app + * @param {string} [basePath=''] + */ +export function register(app, basePath = '') { + /** + * @openapi + * /article/{sku}/supplier: + * get: + * tags: [Article] + * summary: List article suppliers + * description: Fetches all suppliers linked to the specified article SKU. + * security: + * - cookieAuth: [] + * x-markdown: article/supplier/list.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * responses: + * 200: + * description: List of suppliers linked to the article + * content: + * application/json: + * schema: + * type: object + * properties: + * suppliers: + * type: array + * items: + * $ref: '#/components/schemas/ArticleSupplier' + * 400: + * description: No company selected or invalid sku + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Article or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * post: + * tags: [Article] + * summary: Assign supplier to article + * description: | + * Assigns a supplier to an article with optional purchase price, stock, and supplier SKU. + * security: + * - cookieAuth: [] + * x-signals: [article-] + * x-markdown: article/supplier/create.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: [entityId] + * properties: + * entityId: + * type: integer + * description: Supplier entity ID (in Lieferanten group) + * sku: + * type: string + * nullable: true + * description: Supplier's item number + * pu: + * type: number + * default: 1 + * description: Packaging unit (VPE) + * stock: + * type: number + * nullable: true + * description: Supplier stock + * netPrice: + * type: number + * nullable: true + * description: Net purchase price + * taxId: + * type: integer + * nullable: true + * description: Tax ID for purchase price + * taxRate: + * type: number + * nullable: true + * description: Tax rate in percent + * example: + * entityId: 88 + * sku: MET-44910 + * pu: 1 + * stock: 140 + * netPrice: 4.20 + * taxId: 1 + * responses: + * 201: + * description: Created supplier assignment + * content: + * application/json: + * schema: + * type: object + * properties: + * supplier: + * $ref: '#/components/schemas/ArticleSupplier' + * 400: + * description: entityId required, invalid pu, invalid stock, invalid price, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Article, supplier, or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 409: + * description: Supplier already assigned + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.get(`${basePath}/article/:sku/supplier`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + try { + const suppliers = await db.listByArticleSku(req.user.id, skuParsed.sku); + res.status(200).json({ suppliers }); + } catch (error) { + const status = articleSupplierErrorStatus(error); + if (status === 500) console.error('List article suppliers failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.post(`${basePath}/article/:sku/supplier`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + const { entityId, sku: supplierSku, pu, stock, netPrice, taxId, taxRate } = req.body ?? {}; + if (entityId == null || !Number.isFinite(entityId)) { + return res.status(400).json({ error: 'entityId is required' }); + } + if (pu !== undefined && pu !== null && (!Number.isFinite(pu) || pu <= 0)) { + return res.status(400).json({ error: 'pu must be greater than 0' }); + } + if (stock !== undefined && stock !== null && !Number.isFinite(stock)) { + return res.status(400).json({ error: 'Invalid stock' }); + } + if (netPrice !== undefined && netPrice !== null && !Number.isFinite(netPrice)) { + return res.status(400).json({ error: 'Invalid price' }); + } + + try { + const supplier = await db.create(req.user.id, skuParsed.sku, { + entityId, + sku: supplierSku, + pu, + stock, + netPrice, + taxId, + taxRate, + }); + emitArticleSignal(req, skuParsed.sku); + res.status(201).json({ supplier }); + } catch (error) { + const status = articleSupplierErrorStatus(error); + if (status === 500) console.error('Create article supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + /** + * @openapi + * /article/{sku}/supplier/{id}: + * patch: + * tags: [Article] + * summary: Update article supplier assignment + * description: | + * Updates an existing article-supplier association (supplier SKU, packaging unit, stock, purchase price). + * security: + * - cookieAuth: [] + * x-signals: [article-] + * x-markdown: article/supplier/update.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: articlesupplier assignment ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * sku: + * type: string + * nullable: true + * pu: + * type: number + * stock: + * type: number + * nullable: true + * netPrice: + * type: number + * nullable: true + * taxId: + * type: integer + * nullable: true + * taxRate: + * type: number + * nullable: true + * example: + * sku: MET-44910-B + * pu: 6 + * stock: 95 + * netPrice: 22.50 + * responses: + * 200: + * description: Updated supplier assignment + * content: + * application/json: + * schema: + * type: object + * properties: + * supplier: + * $ref: '#/components/schemas/ArticleSupplier' + * 400: + * description: Invalid pu, invalid stock, invalid price, invalid id, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Supplier assignment, article, or company not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * delete: + * tags: [Article] + * summary: Remove supplier assignment from article + * description: | + * Removes a supplier assignment from the article and cleans up its associated purchase price. + * security: + * - cookieAuth: [] + * x-signals: [article-] + * x-markdown: article/supplier/delete.md + * parameters: + * - in: path + * name: sku + * required: true + * schema: + * type: string + * description: Article SKU + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: articlesupplier assignment ID + * responses: + * 204: + * description: Deleted + * 400: + * description: Invalid id, invalid sku, or no company selected + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 401: + * description: Unauthorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 403: + * description: Not authorized + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + * 404: + * description: Supplier assignment or article not found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/Error' + */ + app.patch(`${basePath}/article/:sku/supplier/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + const idParsed = parsePositiveInt(req.params.id, 'id'); + if (idParsed.error) { + return res.status(400).json({ error: idParsed.error }); + } + + const body = req.body ?? {}; + const patch = {}; + if (body.sku !== undefined) patch.sku = body.sku; + if (body.pu !== undefined) { + if (body.pu === null || !Number.isFinite(body.pu) || body.pu <= 0) { + return res.status(400).json({ error: 'pu must be greater than 0' }); + } + patch.pu = body.pu; + } + if (body.stock !== undefined) { + if (body.stock !== null && !Number.isFinite(body.stock)) { + return res.status(400).json({ error: 'Invalid stock' }); + } + patch.stock = body.stock; + } + if (body.netPrice !== undefined) { + if (body.netPrice !== null && !Number.isFinite(body.netPrice)) { + return res.status(400).json({ error: 'Invalid price' }); + } + patch.netPrice = body.netPrice; + } + if (body.taxId !== undefined) patch.taxId = body.taxId; + if (body.taxRate !== undefined) patch.taxRate = body.taxRate; + + try { + const supplier = await db.update(req.user.id, skuParsed.sku, idParsed.id, patch); + emitArticleSignal(req, skuParsed.sku); + res.status(200).json({ supplier }); + } catch (error) { + const status = articleSupplierErrorStatus(error); + if (status === 500) console.error('Update article supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); + + app.delete(`${basePath}/article/:sku/supplier/:id`, async (req, res) => { + const selected = requireSelectedCompany(req); + if (selected.error) { + return res.status(400).json({ error: selected.error }); + } + + const skuParsed = parseSku(req.params.sku); + if (skuParsed.error) { + return res.status(400).json({ error: skuParsed.error }); + } + + const idParsed = parsePositiveInt(req.params.id, 'id'); + if (idParsed.error) { + return res.status(400).json({ error: idParsed.error }); + } + + try { + await db.remove(req.user.id, skuParsed.sku, idParsed.id); + emitArticleSignal(req, skuParsed.sku); + res.sendStatus(204); + } catch (error) { + const status = articleSupplierErrorStatus(error); + if (status === 500) console.error('Delete article supplier failed:', error); + res.status(status).json({ error: error.message }); + } + }); +} diff --git a/src/api/routes/delivery.js b/src/api/routes/delivery.js index 74d5aa9..1d65ced 100644 --- a/src/api/routes/delivery.js +++ b/src/api/routes/delivery.js @@ -82,7 +82,7 @@ export function register(app, basePath = '') { * The manifest `number` is allocated automatically via * `nextNumber('delivery')`; the user-supplied reference is stored as * `externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and - * `tax` (rate). `pu` defaults to `1`; `rebate` defaults to `0`. + * `taxrate` (rate). `pu` defaults to `1`; `rebate` defaults to `0`. * security: * - cookieAuth: [] * x-markdown: delivery/create.md @@ -117,7 +117,7 @@ export function register(app, basePath = '') { * pu: 1 * amount: 10 * net: 99.5 - * tax: 19 + * taxrate: 19 * responses: * 201: * description: Created delivery diff --git a/src/api/routes/order.js b/src/api/routes/order.js index 7771326..241a9d0 100644 --- a/src/api/routes/order.js +++ b/src/api/routes/order.js @@ -81,7 +81,7 @@ export function register(app, basePath = '') { * Creates a sales order with header and lines in one transaction. * `date` is the document date (expected delivery). * `entityId` must be a company customer (not Lieferanten/POS), or null. - * Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. + * Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. * security: * - cookieAuth: [] * x-markdown: order/create.md @@ -117,7 +117,7 @@ export function register(app, basePath = '') { * pu: 1 * amount: 10 * net: 99.5 - * tax: 19 + * taxrate: 19 * responses: * 201: * description: Created order diff --git a/src/api/routes/quote.js b/src/api/routes/quote.js index 11c8678..b1bc861 100644 --- a/src/api/routes/quote.js +++ b/src/api/routes/quote.js @@ -81,7 +81,7 @@ export function register(app, basePath = '') { * Creates a quote with header and lines in one transaction. * `date` is the document date (valid-until). * `entityId` must be a company customer (not Lieferanten/POS), or null. - * Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. + * Each line requires `sku` or `name`, `amount`, `net`, and `taxrate` (rate); `pu` defaults to `1`; `rebate` defaults to `0`. * security: * - cookieAuth: [] * x-markdown: quote/create.md @@ -117,7 +117,7 @@ export function register(app, basePath = '') { * pu: 1 * amount: 10 * net: 99.5 - * tax: 19 + * taxrate: 19 * responses: * 201: * description: Created quote diff --git a/src/db/articlesupplier.js b/src/db/articlesupplier.js new file mode 100644 index 0000000..6150506 --- /dev/null +++ b/src/db/articlesupplier.js @@ -0,0 +1,21 @@ +import { getStore } from './store.js'; + +export async function listByArticleSku(googleAuthId, sku) { + const store = await getStore(); + return store.articlesupplier.listByArticleSku(googleAuthId, sku); +} + +export async function create(googleAuthId, sku, data) { + const store = await getStore(); + return store.articlesupplier.create(googleAuthId, sku, data); +} + +export async function update(googleAuthId, sku, id, data) { + const store = await getStore(); + return store.articlesupplier.update(googleAuthId, sku, id, data); +} + +export async function remove(googleAuthId, sku, id) { + const store = await getStore(); + return store.articlesupplier.delete(googleAuthId, sku, id); +} diff --git a/src/db/store.js b/src/db/store.js index 3e16611..0ed663b 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -29,6 +29,7 @@ const STORE_NAMESPACES = [ 'order', 'numbers', 'companysetting', + 'articlesupplier', ]; /**