This commit is contained in:
seb
2026-09-17 11:46:22 +02:00
parent ab6d018a1f
commit 49efd5f8a1
23 changed files with 1384 additions and 79 deletions

View File

@@ -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/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/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/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/article/set-weight.md`](routes/article/set-weight.md) | `PUT /article/{sku}/weight` |
| [`routes/articleIndex.md`](routes/articleIndex.md) | `GET /articleIndex` | | [`routes/articleIndex.md`](routes/articleIndex.md) | `GET /articleIndex` |
| [`routes/auth/login.md`](routes/auth/login.md) | `POST /auth/login` | | [`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 | | Signal | Emitted by |
|--------|------------| |--------|------------|
| `article-<sku>` | `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-<sku>` | `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}` | | `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` | | `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` |

View File

@@ -762,7 +762,7 @@
"Quote" "Quote"
], ],
"summary": "Create 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": [ "security": [
{ {
"cookieAuth": [] "cookieAuth": []
@@ -812,7 +812,7 @@
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "net": 99.5,
"tax": 19 "taxrate": 19
} }
] ]
} }
@@ -2014,7 +2014,7 @@
"Order" "Order"
], ],
"summary": "Create 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": [ "security": [
{ {
"cookieAuth": [] "cookieAuth": []
@@ -2064,7 +2064,7 @@
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "net": 99.5,
"tax": 19 "taxrate": 19
} }
] ]
} }
@@ -4227,7 +4227,7 @@
"Delivery" "Delivery"
], ],
"summary": "Create 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": [ "security": [
{ {
"cookieAuth": [] "cookieAuth": []
@@ -4276,7 +4276,7 @@
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "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-<sku>"
],
"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-<sku>"
],
"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-<sku>"
],
"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": { "/article": {
"post": { "post": {
"tags": [ "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": { "Brand": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -8952,7 +9474,7 @@
"amount", "amount",
"net", "net",
"rebate", "rebate",
"tax", "taxrate",
"comment" "comment"
], ],
"properties": { "properties": {
@@ -8981,9 +9503,9 @@
"rebate": { "rebate": {
"type": "number" "type": "number"
}, },
"tax": { "taxrate": {
"type": "number", "type": "number",
"description": "tax.rate (manifestarticle.taxrate)" "description": "Tax rate in percent (e.g. 19)"
}, },
"comment": { "comment": {
"type": "string", "type": "string",
@@ -8996,7 +9518,7 @@
"required": [ "required": [
"amount", "amount",
"net", "net",
"tax" "taxrate"
], ],
"description": "Line item input. Requires `sku` or `name`.", "description": "Line item input. Requires `sku` or `name`.",
"properties": { "properties": {
@@ -9020,9 +9542,9 @@
"net": { "net": {
"type": "number" "type": "number"
}, },
"tax": { "taxrate": {
"type": "number", "type": "number",
"description": "Tax rate (resolved via tax.ensure)" "description": "Tax rate in percent (resolved via tax.ensure)"
}, },
"rebate": { "rebate": {
"type": "number", "type": "number",
@@ -9065,7 +9587,7 @@
"amount", "amount",
"net", "net",
"rebate", "rebate",
"tax", "taxrate",
"comment" "comment"
], ],
"description": "Line shape for quote and order", "description": "Line shape for quote and order",
@@ -9095,9 +9617,9 @@
"rebate": { "rebate": {
"type": "number" "type": "number"
}, },
"tax": { "taxrate": {
"type": "number", "type": "number",
"description": "tax.rate (manifestarticle.taxrate)" "description": "Tax rate in percent (e.g. 19)"
}, },
"comment": { "comment": {
"type": "string", "type": "string",
@@ -9110,7 +9632,7 @@
"required": [ "required": [
"amount", "amount",
"net", "net",
"tax" "taxrate"
], ],
"description": "Line item input. Requires `sku` or `name`.", "description": "Line item input. Requires `sku` or `name`.",
"properties": { "properties": {
@@ -9134,9 +9656,9 @@
"net": { "net": {
"type": "number" "type": "number"
}, },
"tax": { "taxrate": {
"type": "number", "type": "number",
"description": "Tax rate (resolved via tax.ensure)" "description": "Tax rate in percent (resolved via tax.ensure)"
}, },
"rebate": { "rebate": {
"type": "number", "type": "number",

View File

@@ -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-<sku>` (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`._

View File

@@ -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-<sku>` (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`._

View File

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

View File

@@ -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-<sku>` (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`._

View File

@@ -8,7 +8,7 @@ Creates a goods receipt with header and lines in one transaction.
The manifest `number` is allocated automatically via The manifest `number` is allocated automatically via
`nextNumber('delivery')`; the user-supplied reference is stored as `nextNumber('delivery')`; the user-supplied reference is stored as
`externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and `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) ## Body (JSON)
@@ -25,7 +25,7 @@ The manifest `number` is allocated automatically via
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "net": 99.5,
"tax": 19 "taxrate": 19
} }
] ]
} }
@@ -53,7 +53,7 @@ Created delivery
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -34,7 +34,7 @@ Delivery
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -47,7 +47,7 @@ Updated delivery
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -7,7 +7,7 @@ Create order
Creates a sales order with header and lines in one transaction. Creates a sales order with header and lines in one transaction.
`date` is the document date (expected delivery). `date` is the document date (expected delivery).
`entityId` must be a company customer (not Lieferanten/POS), or null. `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) ## Body (JSON)
@@ -24,7 +24,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defa
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "net": 99.5,
"tax": 19 "taxrate": 19
} }
] ]
} }
@@ -51,7 +51,7 @@ Created order
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -33,7 +33,7 @@ Order
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -45,7 +45,7 @@ Updated order
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -7,7 +7,7 @@ Create quote
Creates a quote with header and lines in one transaction. Creates a quote with header and lines in one transaction.
`date` is the document date (valid-until). `date` is the document date (valid-until).
`entityId` must be a company customer (not Lieferanten/POS), or null. `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) ## Body (JSON)
@@ -24,7 +24,7 @@ Each line requires `sku` or `name`, `amount`, `net`, and `tax` (rate); `pu` defa
"pu": 1, "pu": 1,
"amount": 10, "amount": 10,
"net": 99.5, "net": 99.5,
"tax": 19 "taxrate": 19
} }
] ]
} }
@@ -51,7 +51,7 @@ Created quote
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -33,7 +33,7 @@ Quote
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

View File

@@ -45,7 +45,7 @@ Updated quote
"amount": 1, "amount": 1,
"net": 1, "net": 1,
"rebate": 1, "rebate": 1,
"tax": 1, "taxrate": 1,
"comment": "…" "comment": "…"
} }
] ]

54
package-lock.json generated
View File

@@ -59,9 +59,9 @@
} }
}, },
"node_modules/@alcalzone/ansi-tokenize": { "node_modules/@alcalzone/ansi-tokenize": {
"version": "0.3.0", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.0.tgz", "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.3.1.tgz",
"integrity": "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA==", "integrity": "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.2.1", "ansi-styles": "^6.2.1",
@@ -342,33 +342,33 @@
} }
}, },
"node_modules/@azure/msal-browser": { "node_modules/@azure/msal-browser": {
"version": "5.21.0", "version": "5.22.0",
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.21.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-5.22.0.tgz",
"integrity": "sha512-80OcuXDErmcEDAIH9pBtSqBsed2sPT/IWmbG3xHLoPMl5zc8TINd6SlJAbVSmN5huGa3xGAg5qR7VnpaIEK0Zw==", "integrity": "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@azure/msal-common": "16.14.0" "@azure/msal-common": "16.14.1"
}, },
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@azure/msal-common": { "node_modules/@azure/msal-common": {
"version": "16.14.0", "version": "16.14.1",
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz",
"integrity": "sha512-A4rb55hI86Q9tBl/+jBj7TMz7iX2RFgQs/nExFzcAtoI/BFRVdaH5SL/MivrYD7qvweMpN8AgVvVMHV8UBYxew==", "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/@azure/msal-node": { "node_modules/@azure/msal-node": {
"version": "6.0.0", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz",
"integrity": "sha512-PU0l3CsEZVm2K8vZNFxvgmM5d0Yx/sVL/axWW0hJBoeyiB6Q/uv7hXORb7EdzlNxvehsWEF82/o+/sXlIo+UIw==", "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@azure/msal-common": "16.14.0", "@azure/msal-common": "16.14.1",
"jsonwebtoken": "^9.0.0" "jsonwebtoken": "^9.0.0"
}, },
"engines": { "engines": {
@@ -969,12 +969,12 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "26.5.1", "version": "22.20.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.3.tgz",
"integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", "integrity": "sha512-DZmzkmwHzXrLPAXPyKNDzlIwMMUZCVacoD25ywdy5YTKGbOx/2ld+Q38Im2zJ0vBuZP5Prd3VZutKZyXwkOS8A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~8.9.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@types/readable-stream": { "node_modules/@types/readable-stream": {
@@ -1855,9 +1855,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.7", "version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -2087,9 +2087,9 @@
} }
}, },
"node_modules/google-auth-library": { "node_modules/google-auth-library": {
"version": "11.0.2", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.0.2.tgz", "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-11.1.0.tgz",
"integrity": "sha512-vzpgPutxrghPsnjrjpzLX2bdv8IOL719Rh0oEjGnQu8YCIbnbMuTTQ5zU9LcKvLdOPgCxBwppbvnhgW90Qna5Q==", "integrity": "sha512-YIBq5KNoHQrZ9PM4PeodDBqqBGlSTIxh1zibN8u3zNIO9UJg8gvKuCpdKV7fWXhsZ2zRkU6FhGrgKNgLJ2IRgw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"base64-js": "^1.3.0", "base64-js": "^1.3.0",
@@ -3715,9 +3715,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "8.9.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/unpipe": { "node_modules/unpipe": {

View File

@@ -108,6 +108,50 @@
* format: date-time * format: date-time
* text: * text:
* type: string * 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: * Brand:
* type: object * type: object
* required: [id, name] * required: [id, name]
@@ -264,7 +308,7 @@
* format: date-time * format: date-time
* DeliveryLine: * DeliveryLine:
* type: object * type: object
* required: [id, sku, name, pu, amount, net, rebate, tax, comment] * required: [id, sku, name, pu, amount, net, rebate, taxrate, comment]
* properties: * properties:
* id: * id:
* type: integer * type: integer
@@ -284,15 +328,15 @@
* type: number * type: number
* rebate: * rebate:
* type: number * type: number
* tax: * taxrate:
* type: number * type: number
* description: tax.rate (manifestarticle.taxrate) * description: Tax rate in percent (e.g. 19)
* comment: * comment:
* type: string * type: string
* nullable: true * nullable: true
* DeliveryLineInput: * DeliveryLineInput:
* type: object * type: object
* required: [amount, net, tax] * required: [amount, net, taxrate]
* description: Line item input. Requires `sku` or `name`. * description: Line item input. Requires `sku` or `name`.
* properties: * properties:
* sku: * sku:
@@ -310,9 +354,9 @@
* type: number * type: number
* net: * net:
* type: number * type: number
* tax: * taxrate:
* type: number * type: number
* description: Tax rate (resolved via tax.ensure) * description: Tax rate in percent (resolved via tax.ensure)
* rebate: * rebate:
* type: number * type: number
* description: Defaults to 0 * description: Defaults to 0
@@ -331,7 +375,7 @@
* $ref: '#/components/schemas/DeliveryLine' * $ref: '#/components/schemas/DeliveryLine'
* ManifestLine: * ManifestLine:
* type: object * 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 * description: Line shape for quote and order
* properties: * properties:
* id: * id:
@@ -352,15 +396,15 @@
* type: number * type: number
* rebate: * rebate:
* type: number * type: number
* tax: * taxrate:
* type: number * type: number
* description: tax.rate (manifestarticle.taxrate) * description: Tax rate in percent (e.g. 19)
* comment: * comment:
* type: string * type: string
* nullable: true * nullable: true
* ManifestLineInput: * ManifestLineInput:
* type: object * type: object
* required: [amount, net, tax] * required: [amount, net, taxrate]
* description: Line item input. Requires `sku` or `name`. * description: Line item input. Requires `sku` or `name`.
* properties: * properties:
* sku: * sku:
@@ -378,9 +422,9 @@
* type: number * type: number
* net: * net:
* type: number * type: number
* tax: * taxrate:
* type: number * type: number
* description: Tax rate (resolved via tax.ensure) * description: Tax rate in percent (resolved via tax.ensure)
* rebate: * rebate:
* type: number * type: number
* description: Defaults to 0 * description: Defaults to 0

View File

@@ -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-<sku>]
* 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-<sku>]
* 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-<sku>]
* 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 });
}
});
}

View File

@@ -82,7 +82,7 @@ export function register(app, basePath = '') {
* The manifest `number` is allocated automatically via * The manifest `number` is allocated automatically via
* `nextNumber('delivery')`; the user-supplied reference is stored as * `nextNumber('delivery')`; the user-supplied reference is stored as
* `externalNumber`. Each line requires `sku` or `name`, `amount`, `net`, and * `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: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: delivery/create.md * x-markdown: delivery/create.md
@@ -117,7 +117,7 @@ export function register(app, basePath = '') {
* pu: 1 * pu: 1
* amount: 10 * amount: 10
* net: 99.5 * net: 99.5
* tax: 19 * taxrate: 19
* responses: * responses:
* 201: * 201:
* description: Created delivery * description: Created delivery

View File

@@ -81,7 +81,7 @@ export function register(app, basePath = '') {
* Creates a sales order with header and lines in one transaction. * Creates a sales order with header and lines in one transaction.
* `date` is the document date (expected delivery). * `date` is the document date (expected delivery).
* `entityId` must be a company customer (not Lieferanten/POS), or null. * `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: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: order/create.md * x-markdown: order/create.md
@@ -117,7 +117,7 @@ export function register(app, basePath = '') {
* pu: 1 * pu: 1
* amount: 10 * amount: 10
* net: 99.5 * net: 99.5
* tax: 19 * taxrate: 19
* responses: * responses:
* 201: * 201:
* description: Created order * description: Created order

View File

@@ -81,7 +81,7 @@ export function register(app, basePath = '') {
* Creates a quote with header and lines in one transaction. * Creates a quote with header and lines in one transaction.
* `date` is the document date (valid-until). * `date` is the document date (valid-until).
* `entityId` must be a company customer (not Lieferanten/POS), or null. * `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: * security:
* - cookieAuth: [] * - cookieAuth: []
* x-markdown: quote/create.md * x-markdown: quote/create.md
@@ -117,7 +117,7 @@ export function register(app, basePath = '') {
* pu: 1 * pu: 1
* amount: 10 * amount: 10
* net: 99.5 * net: 99.5
* tax: 19 * taxrate: 19
* responses: * responses:
* 201: * 201:
* description: Created quote * description: Created quote

21
src/db/articlesupplier.js Normal file
View File

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

View File

@@ -29,6 +29,7 @@ const STORE_NAMESPACES = [
'order', 'order',
'numbers', 'numbers',
'companysetting', 'companysetting',
'articlesupplier',
]; ];
/** /**