Compare commits

...

11 Commits

Author SHA1 Message Date
seb
bdd7ebf5b8 u 2026-10-15 00:23:55 +02:00
seb
507dffa567 u 2026-10-14 23:26:09 +02:00
seb
0ea7da4f3a u 2026-10-14 22:48:03 +02:00
seb
d880273629 u 2026-10-14 20:50:07 +02:00
seb
9c54a8a1d3 u 2026-10-14 19:43:53 +02:00
seb
04e10cffdb u 2026-10-14 15:32:54 +02:00
seb
90b782e500 u 2026-10-14 14:45:45 +02:00
seb
37c58648db u 2026-10-14 13:44:53 +02:00
seb
5fb8df6f6a u 2026-10-14 13:31:25 +02:00
seb
80450e8748 u 2026-10-14 12:58:18 +02:00
seb
e230c4cf12 u 2026-10-14 12:53:56 +02:00
51 changed files with 6176 additions and 2191 deletions

View File

@@ -240,7 +240,7 @@ export function createBashTool(googleAuthId) {
| Consideration | Ephemeral (`docker run --rm`) | Persistent Container (`docker exec`) |
| :--- | :--- | :--- |
| **Startup Overhead** | ~200–400ms per command | ~20–50ms per command |
| **Startup Overhead** | ~200-400ms per command | ~20-50ms per command |
| **State Cleanliness** | Completely fresh OS environment every call | Processes/background jobs remain running |
| **Lifecycle Complexity** | None. Docker handles lifecycle and cleanup | Requires container pool management and reaper |
| **Failure Recovery** | Automatic | If container crashes, must be recreated |

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 289 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -113,6 +113,8 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/category/_id/property/_propertyId.md`](routes/category/_id/property/_propertyId.md) | `DELETE /category/{id}/property/{propertyId}` |
| [`routes/company/list.md`](routes/company/list.md) | `GET /company` |
| [`routes/company/create.md`](routes/company/create.md) | `POST /company` |
| [`routes/company/export.md`](routes/company/export.md) | `GET /company/export` |
| [`routes/company/export.post.md`](routes/company/export.post.md) | `POST /company/export` |
| [`routes/company/history.md`](routes/company/history.md) | `GET /company/history` |
| [`routes/company/addHistory.md`](routes/company/addHistory.md) | `POST /company/history` |
| [`routes/company/import.md`](routes/company/import.md) | `POST /company/import` |
@@ -132,7 +134,9 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/delivery/delete.md`](routes/delivery/delete.md) | `DELETE /delivery/{id}` |
| [`routes/delivery/get.md`](routes/delivery/get.md) | `GET /delivery/{id}` |
| [`routes/delivery/update.md`](routes/delivery/update.md) | `PATCH /delivery/{id}` |
| [`routes/delivery/_id/file/_fileId.delete.md`](routes/delivery/_id/file/_fileId.delete.md) | `DELETE /delivery/{id}/file/{fileId}` |
| [`routes/delivery/file.md`](routes/delivery/file.md) | `GET /delivery/{id}/file/{fileId}` |
| [`routes/delivery/_id/pdf.md`](routes/delivery/_id/pdf.md) | `POST /delivery/{id}/pdf` |
| [`routes/entity/list.md`](routes/entity/list.md) | `GET /entity` |
| [`routes/entity/create.md`](routes/entity/create.md) | `POST /entity` |
| [`routes/entity/next-number.md`](routes/entity/next-number.md) | `GET /entity/next-number` |
@@ -185,8 +189,11 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/order/delete.md`](routes/order/delete.md) | `DELETE /order/{id}` |
| [`routes/order/get.md`](routes/order/get.md) | `GET /order/{id}` |
| [`routes/order/update.md`](routes/order/update.md) | `PATCH /order/{id}` |
| [`routes/order/_id/file/_fileId.delete.md`](routes/order/_id/file/_fileId.delete.md) | `DELETE /order/{id}/file/{fileId}` |
| [`routes/order/_id/file/_fileId.get.md`](routes/order/_id/file/_fileId.get.md) | `GET /order/{id}/file/{fileId}` |
| [`routes/order/_id/payment.get.md`](routes/order/_id/payment.get.md) | `GET /order/{id}/payment` |
| [`routes/order/_id/payment.post.md`](routes/order/_id/payment.post.md) | `POST /order/{id}/payment` |
| [`routes/order/_id/pdf.md`](routes/order/_id/pdf.md) | `POST /order/{id}/pdf` |
| [`routes/order/_id/shipment.get.md`](routes/order/_id/shipment.get.md) | `GET /order/{id}/shipment` |
| [`routes/order/_id/shipment.post.md`](routes/order/_id/shipment.post.md) | `POST /order/{id}/shipment` |
| [`routes/order/_id/shipping.get.md`](routes/order/_id/shipping.get.md) | `GET /order/{id}/shipping` |
@@ -222,10 +229,16 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
| [`routes/quote/delete.md`](routes/quote/delete.md) | `DELETE /quote/{id}` |
| [`routes/quote/get.md`](routes/quote/get.md) | `GET /quote/{id}` |
| [`routes/quote/update.md`](routes/quote/update.md) | `PATCH /quote/{id}` |
| [`routes/quote/_id/file/_fileId.delete.md`](routes/quote/_id/file/_fileId.delete.md) | `DELETE /quote/{id}/file/{fileId}` |
| [`routes/quote/_id/file/_fileId.get.md`](routes/quote/_id/file/_fileId.get.md) | `GET /quote/{id}/file/{fileId}` |
| [`routes/quote/_id/pdf.md`](routes/quote/_id/pdf.md) | `POST /quote/{id}/pdf` |
| [`routes/serper/image.md`](routes/serper/image.md) | `POST /serper/image` |
| [`routes/serper/images.md`](routes/serper/images.md) | `POST /serper/images` |
| [`routes/shipment/_id.delete.md`](routes/shipment/_id.delete.md) | `DELETE /shipment/{id}` |
| [`routes/shipment/_id.get.md`](routes/shipment/_id.get.md) | `GET /shipment/{id}` |
| [`routes/shipment/_id/file/_fileId.delete.md`](routes/shipment/_id/file/_fileId.delete.md) | `DELETE /shipment/{id}/file/{fileId}` |
| [`routes/shipment/_id/file/_fileId.get.md`](routes/shipment/_id/file/_fileId.get.md) | `GET /shipment/{id}/file/{fileId}` |
| [`routes/shipment/_id/pdf.md`](routes/shipment/_id/pdf.md) | `POST /shipment/{id}/pdf` |
| [`routes/shipment/_id/shipping.get.md`](routes/shipment/_id/shipping.get.md) | `GET /shipment/{id}/shipping` |
| [`routes/shipment/_id/shipping.post.md`](routes/shipment/_id/shipping.post.md) | `POST /shipment/{id}/shipping` |
| [`routes/shipping/_id.delete.md`](routes/shipping/_id.delete.md) | `DELETE /shipping/{id}` |
@@ -279,12 +292,16 @@ Clients subscribe via [`GET /events`](routes/events.md) (`text/event-stream`). A
| `attributename` | `POST /attributename`, `PUT /attributename/order`, `DELETE /attributename/{id}`, `PATCH /attributename/{id}`, `DELETE /attributevalue/{id}/image`, `POST /attributevalue/{id}/image` |
| `brand` | `POST /brand`, `DELETE /brand/{id}`, `PATCH /brand/{id}`, `DELETE /brand/{id}/image`, `POST /brand/{id}/image` |
| `category` | `POST /category`, `PUT /category/order`, `DELETE /category/{id}`, `PATCH /category/{id}`, `DELETE /category/{id}/image`, `POST /category/{id}/image` |
| `delivery` | `DELETE /delivery/{id}/file/{fileId}`, `POST /delivery/{id}/pdf` |
| `labeltemplate` | `POST /labeltemplate`, `DELETE /labeltemplate/{id}`, `PATCH /labeltemplate/{id}`, `PUT /labeltemplate/{id}` |
| `ledger` | `POST /stock/adjust` |
| `modelconversation` | `POST /modelconversation`, `DELETE /modelconversation/{id}` |
| `modelconversation-<id>` | `POST /modelconversation/{id}/message` |
| `order` | `DELETE /order/{id}/file/{fileId}`, `POST /order/{id}/pdf`, `DELETE /shipment/{id}/file/{fileId}` |
| `printer` | `POST /printer`, `PUT /printer/order`, `DELETE /printer/{id}`, `PATCH /printer/{id}`, `PUT /printer/{id}` |
| `propertyname` | `POST /propertyname`, `PUT /propertyname/order`, `DELETE /propertyname/{id}`, `PATCH /propertyname/{id}` |
| `quote` | `DELETE /quote/{id}/file/{fileId}`, `POST /quote/{id}/pdf` |
| `shipment` | `DELETE /shipment/{id}/file/{fileId}`, `POST /shipment/{id}/pdf` |
| `stock` | `POST /stock/adjust` |
| `vfs` | `POST /vfs/mkdir`, `POST /vfs/rename`, `POST /vfs/upload`, `DELETE /vfs/{*path}`, `POST /vfs/{*path}`, `PUT /vfs/{*path}` |
| `warehouse` | `POST /warehouse`, `PUT /warehouse/order`, `DELETE /warehouse/{id}`, `PUT /warehouse/{id}` |

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview (All)
Overview of all 233 API endpoints across all modules.
Overview of all 246 API endpoints across all modules.
### Notation
@@ -10,60 +10,61 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types
- `T1` = `{ order: T2 }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T28[], createdAt?: str, shipping?: T36|null, shippings?: T36[], payment?: T42|null, payments?: T42[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T7[] }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], createdAt?: str, shipping?: T37|null, shippings?: T37[], payment?: T43|null, payments?: T43[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T5[], files?: T15[] }`
- `T3` = `{ article: T4 }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T39, brand: T37|null, categoryIds: int[], photos: T34[], eans: str[], prices: T46[], suppliers: T16[] }`
- `T5` = `{ delivery: T6 }`
- `T6` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T28[], files: T13[], externalNumber?: str|null, createdAt?: str, ledger?: T50 }`
- `T7` = `{ id: int, number: str, type: str, date: str, lines: T28[], shippings: T36[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int }`
- `T8` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T9|null }`
- `T9` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T17, totalSizeBytes: int, durationMs: int }`
- `T10` = `{ labeltemplate: T11 }`
- `T11` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T24, thumb?: str|null }`
- `T12` = `{ quote: T15 }`
- `T13` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, thumbavif?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T14` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
- `T15` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T28[], createdAt?: str }`
- `T16` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }`
- `T17` = `{ vfs: T51, storage: T51, database: T44 }`
- `T18` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T19` = `{ ledger: T20[] }`
- `T20` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T21` = `{ id: int, name: str, sort: int, exclusive: bool, values: T40[], articleCount?: int }`
- `T22` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: integer,null, first_message?: string,null }`
- `T23` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
- `T24` = `{ objects: T29[] }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T40, brand: T38|null, categoryIds: int[], photos: T35[], eans: str[], prices: T47[], suppliers: T17[] }`
- `T5` = `{ id: int, number: str, type: str, date: str, lines: T29[], shippings: T37[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T15[] }`
- `T6` = `{ delivery: T7 }`
- `T7` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T29[], files: T15[], externalNumber?: str|null, createdAt?: str, ledger?: T51 }`
- `T8` = `{ quote: T9 }`
- `T9` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T29[], createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T15[] }`
- `T10` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T11|null }`
- `T11` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T18, totalSizeBytes: int, durationMs: int }`
- `T12` = `{ labeltemplate: T14 }`
- `T13` = `{ attachment: T15, fileUrl: str }`
- `T14` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T25, thumb?: str|null }`
- `T15` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, thumbavif?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T16` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }`
- `T17` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T54|null }`
- `T18` = `{ vfs: T52, storage: T52, database: T45 }`
- `T19` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T20` = `{ ledger: T21[] }`
- `T21` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T22` = `{ id: int, name: str, sort: int, exclusive: bool, values: T41[], articleCount?: int }`
- `T23` = `{ id: int, companyuser_id: int, provider: str, model: str, created_at: str, updated_at: str, state: str, started_by?: integer,null, first_message?: string,null }`
- `T24` = `{ id: int, name: str, rootCategoryId: int|null, languageId: int|null, iso639: str|null, taxrate: num, reducedtaxrate: num, taxfreerate: num }`
- `T25` = `{ objects: T30[] }`
- `T26` = `{ printer: T32 }`
- `T27` = `{ entity: T35 }`
- `T28` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T29` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T26` = `{ objects: T31[] }`
- `T27` = `{ printer: T33 }`
- `T28` = `{ entity: T36 }`
- `T29` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T30` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T31` = `{ shipping: T36 }`
- `T32` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T33` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T34` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T35` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T36` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T37` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T38` = `{ article: T43 }`
- `T39` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T40` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T41` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T42` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T43` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T44` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
- `T45` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T46` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T47` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T48` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T49` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T50` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T51` = `{ name: str, sizeBytes: int, sourceExists: bool }`
- `T52` = `{ warehouse: T54 }`
- `T53` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T54` = `{ id: int, name: str, sort: int }`
- `T55` = `{ id: int, categoryIds: int[] }`
- `T31` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T32` = `{ shipping: T37 }`
- `T33` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T34` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T35` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T36` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T37` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T38` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T39` = `{ article: T44 }`
- `T40` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T41` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T42` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T43` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T44` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T45` = `{ name: str|null, sizeBytes: int, backedUp: bool, reason?: str|null }`
- `T46` = `{ id: int, email: str, selectedCompanyId: int|null, picture?: str }`
- `T47` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T48` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T49` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T50` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T51` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T52` = `{ name: str, sizeBytes: int, sourceExists: bool }`
- `T53` = `{ warehouse: T55 }`
- `T54` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T55` = `{ id: int, name: str, sort: int }`
- `T56` = `{ id: int, categoryIds: int[] }`
### Routes
@@ -93,7 +94,7 @@ _none_
#### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 T38`
- Returns: `201 T39`
#### Export articles as ZIP — `GET /article/export.zip`
- Params: `query: categoryId?: int`
- Returns: `200 zip`
@@ -129,10 +130,10 @@ _none_
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Create and attach a new child article to a parent article — `POST /article/{sku}/children`
- Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 T38`
- Returns: `201 T39`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children`
- Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T43, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
- Returns: `200 { article: T44, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }`
@@ -159,7 +160,7 @@ _none_
- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }`
#### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 T38`
- Returns: `200 T39`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 T3`
@@ -177,10 +178,10 @@ _none_
- Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str`
- Returns: `200 { suppliers: T16[] }`
- Returns: `200 { suppliers: T17[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier`
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `201 { supplier: T16 }`
- Returns: `201 { supplier: T17 }`
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }`
@@ -189,7 +190,7 @@ _none_
- Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `200 { supplier: T16 }`
- Returns: `200 { supplier: T17 }`
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }`
- Returns: `200 T3`
@@ -201,19 +202,19 @@ _none_
#### List attribute names — `GET /attributename`
_none_
- Returns: `200 { attributenames: T21[] }`
- Returns: `200 { attributenames: T22[] }`
#### Create attribute name — `POST /attributename`
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { attributename: T21 }`
- Returns: `201 { attributename: T22 }`
#### Reorder attribute names — `PUT /attributename/order`
- Params: `body: { attributenameIds: int[] }`
- Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int`
- Returns: `200 { values: T40[] }`
- Returns: `200 { values: T41[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T40 }`
- Returns: `201 { attributevalue: T41 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }`
@@ -222,7 +223,7 @@ _none_
- Returns: `200 { ok: bool }`
#### Update attribute name — `PATCH /attributename/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { attributename: T21 }`
- Returns: `200 { attributename: T22 }`
### Section: /attributevalue
@@ -231,34 +232,34 @@ _none_
- Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T40 }`
- Returns: `200 { attributevalue: T41 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T47[] } }`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T48[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T47[] } }`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T48[] } }`
### Section: /auth
#### Login with Google ID token — `POST /auth/login`
- Params: `body: { idToken: str }`
- Returns: `200 { message: str, user: T45 }`
- Returns: `200 { message: str, user: T46 }`
#### Logout — `POST /auth/logout`
_none_
- Returns: `200 { message: str }`
#### Current user — `GET /auth/me`
_none_
- Returns: `200 { user: T45, googleClientId: str }`
- Returns: `200 { user: T46, googleClientId: str }`
#### Set selected company — `PUT /auth/selected-company`
- Params: `body: { companyId: int }`
- Returns: `200 { user: T45 }`
- Returns: `200 { user: T46 }`
### Section: /backup
#### List backups and service status — `GET /backup`
_none_
- Returns: `200 { status: { enabled: bool, isRunning: bool, dbProvider: str, mssqlWarning: bool, backupDir: str, retention: { hourlyMax: int, dailyMax: int, monthly: str }, counts: { hourly: int, daily: int, monthly: int }, latestHourly?: T8|null, latestDaily?: T8|null, latestMonthly?: T8|null }, backups: { counts: { hourly: int, daily: int, monthly: int }, hourly: T8[], daily: T8[], monthly: T8[] } }`
- Returns: `200 { status: { enabled: bool, isRunning: bool, dbProvider: str, mssqlWarning: bool, backupDir: str, retention: { hourlyMax: int, dailyMax: int, monthly: str }, counts: { hourly: int, daily: int, monthly: int }, latestHourly?: T10|null, latestDaily?: T10|null, latestMonthly?: T10|null }, backups: { counts: { hourly: int, daily: int, monthly: int }, hourly: T10[], daily: T10[], monthly: T10[] } }`
#### List VFS files in a backup — `GET /backup/{id}/vfs`
- Params: `id: str, query: companyId?: int`
- Returns: `200 { backupId: str, companyId: int, files: { name: str, size: int, mime: str, mtime: str }[] }`
@@ -273,7 +274,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T47[], gsprpinfo?: str|null } }`
- Returns: `201 { brand: { id: int, name: str, pics: T48[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}`
- Params: `id: int`
- Returns: `204 void`
@@ -282,10 +283,10 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T47[] } }`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T48[] } }`
#### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T47[] } }`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T48[] } }`
### Section: /category
@@ -309,7 +310,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles`
- Params: `id: int`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T37|null }[] }`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T38|null }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -336,10 +337,16 @@ _none_
#### List companies — `GET /company`
_none_
- Returns: `200 { companies: T23[] }`
- Returns: `200 { companies: T24[] }`
#### Create company — `POST /company`
- Params: `body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `201 { company: T23, user: T45 }`
- Returns: `201 { company: T24, user: T46 }`
#### Export company catalog as zip archive (SSE) — `GET /company/export`
- Params: `query: stream?: str`
- Returns: `200 SSE`
#### Export company catalog as zip archive (SSE) — `POST /company/export`
_none_
- Returns: `200 SSE`
#### Get company history — `GET /company/history`
- Params: `query: limit?: int`
- Returns: `200 { history: { id: int, date: str, text: str, companyuserId?: int|null }[], truncated: bool }`
@@ -363,7 +370,7 @@ _none_
- Returns: `204 void`
#### Update company — `PUT /company/{id}`
- Params: `id: int, body: { iso639: str, name: str, taxrate: num, reducedtaxrate: num }`
- Returns: `200 { company: T23 }`
- Returns: `200 { company: T24 }`
### Section: /customergroup
@@ -372,7 +379,7 @@ _none_
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
#### Create customer group — `POST /customergroup`
- Params: `body: { name: str }`
- Returns: `201 { customergroup: T54 }`
- Returns: `201 { customergroup: T55 }`
#### Reorder customer groups — `PUT /customergroup/order`
- Params: `body: { customergroupIds: int[] }`
- Returns: `200 { customergroupIds: int[] }`
@@ -381,16 +388,16 @@ _none_
- Returns: `204 void`
#### Update customer group — `PATCH /customergroup/{id}`
- Params: `id: int, body: { name: str }`
- Returns: `200 { customergroup: T54 }`
- Returns: `200 { customergroup: T55 }`
### Section: /delivery
#### List deliveries — `GET /delivery`
_none_
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T50 }[] }`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T51 }[] }`
#### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T33[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T5`
- Params: `body: { externalNumber: str, date: str, lines: T34[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T6`
#### Parse document (SSE) — `POST /delivery/parse`
- Params: `form: { file: str }`
- Returns: `200 SSE`
@@ -408,58 +415,64 @@ _none_
- Returns: `204 void`
#### Get delivery — `GET /delivery/{id}`
- Params: `id: int`
- Returns: `200 T5`
- Returns: `200 T6`
#### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T33[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T5`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T34[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T6`
#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Get delivery attachment file content — `GET /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
- Params: `id: int`
- Returns: `201 T13`
### Section: /entity
#### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T35[] }`
- Returns: `200 { entities: T36[] }`
#### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 T27`
- Returns: `201 T28`
#### Peek next customer number — `GET /entity/next-number`
_none_
- Returns: `200 { number: str }`
#### List entity addresses — `GET /entity/{entityId}/address`
- Params: `entityId: int`
- Returns: `200 { addresses: T18[] }`
- Returns: `200 { addresses: T19[] }`
#### Create entity address — `POST /entity/{entityId}/address`
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `201 { address: T18 }`
- Returns: `201 { address: T19 }`
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int`
- Returns: `204 void`
#### Update entity address — `PATCH /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `200 { address: T18 }`
- Returns: `200 { address: T19 }`
#### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int`
- Returns: `200 { contacts: T48[] }`
- Returns: `200 { contacts: T49[] }`
#### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T48 }`
- Returns: `201 { contact: T49 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int`
- Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T48 }`
- Returns: `200 { contact: T49 }`
#### Delete entity — `DELETE /entity/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get entity — `GET /entity/{id}`
- Params: `id: int`
- Returns: `200 T27`
- Returns: `200 T28`
#### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 T27`
- Returns: `200 T28`
### Section: /events
@@ -503,35 +516,35 @@ _none_
_none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T25 }`
- Returns: `201 T10`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T26 }`
- Returns: `201 T12`
#### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int`
- Returns: `200 T10`
- Returns: `200 T12`
#### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T25 }`
- Returns: `200 T10`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T26 }`
- Returns: `200 T12`
#### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T25 }`
- Returns: `200 T10`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T26 }`
- Returns: `200 T12`
### Section: /ledger
#### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 T19`
- Returns: `200 T20`
#### Add ledger entry — `POST /ledger`
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
- Returns: `201 { ledger: T20 }`
- Returns: `201 { ledger: T21 }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 T19`
- Returns: `200 T20`
#### Query ledger by article SKU — `GET /ledger/sku/{sku}`
- Params: `sku: str, query: transaction_type?: str`
- Returns: `200 T19`
- Returns: `200 T20`
### Section: /mcp
@@ -561,16 +574,16 @@ _none_
#### List conversations — `GET /modelconversation`
_none_
- Returns: `200 { conversations: T22[] }`
- Returns: `200 { conversations: T23[] }`
#### Create conversation — `POST /modelconversation`
- Params: `body: { provider: str, model: str }`
- Returns: `201 { conversation: T22 }`
- Returns: `201 { conversation: T23 }`
#### Delete conversation — `DELETE /modelconversation/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get conversation — `GET /modelconversation/{id}`
- Params: `id: int`
- Returns: `200 { conversation: T22 }`
- Returns: `200 { conversation: T23 }`
#### List conversation messages — `GET /modelconversation/{id}/message`
- Params: `id: int`
- Returns: `200 { messages: { id: int, conversation_id: int, created_at: str, role: str, message: str }[] }`
@@ -611,7 +624,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T33[], entityId?: int|null }`
- Params: `body: { number: str, date: str, lines: T34[], entityId?: int|null }`
- Returns: `201 T1`
#### Delete order — `DELETE /order/{id}`
- Params: `id: int`
@@ -620,26 +633,35 @@ _none_
- Params: `id: int`
- Returns: `200 T1`
#### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T33[] }`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T34[] }`
- Returns: `200 T1`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Get order attachment file content — `GET /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
#### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int`
- Returns: `200 { payments: T42[] }`
- Returns: `200 { payments: T43[] }`
#### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T42 }`
- Returns: `201 { payment: T43 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int`
- Returns: `201 T13`
#### List shipments of an order — `GET /order/{id}/shipment`
- Params: `id: int`
- Returns: `200 { shipments: T7[] }`
- Returns: `200 { shipments: T5[] }`
#### Create shipment for an order — `POST /order/{id}/shipment`
- Params: `id: int, body: { lines: { sku: str, amount: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[], date?: str, externalNumber?: str }`
- Returns: `201 { shipment: T7 }`
- Returns: `201 { shipment: T5 }`
#### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int`
- Returns: `200 { shippings: T36[] }`
- Returns: `200 { shippings: T37[] }`
#### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, warehouseId?: int }`
- Returns: `201 T31`
- Returns: `201 T32`
### Section: /package
@@ -657,37 +679,37 @@ _none_
#### List POS terminals — `GET /posterminal`
_none_
- Returns: `200 { posterminals: T14[] }`
- Returns: `200 { posterminals: T16[] }`
#### Create POS terminal — `PUT /posterminal`
- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool }`
- Returns: `200 { posterminal: T14 }`
- Returns: `200 { posterminal: T16 }`
#### Delete POS terminal — `DELETE /posterminal/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Update POS terminal — `PATCH /posterminal/{id}`
- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, pairingpin?: str|null }`
- Returns: `200 { posterminal: T14 }`
- Returns: `200 { posterminal: T16 }`
#### Disable all categories on terminal — `DELETE /posterminal/{id}/all`
- Params: `id: int`
- Returns: `200 T55`
- Returns: `200 T56`
#### Enable all categories on terminal — `PUT /posterminal/{id}/all`
- Params: `id: int`
- Returns: `200 T55`
- Returns: `200 T56`
#### Disable category on terminal — `DELETE /posterminal/{id}/category/{categoryId}`
- Params: `id: int, categoryId: int`
- Returns: `200 T55`
- Returns: `200 T56`
#### Enable category on terminal — `PUT /posterminal/{id}/category/{categoryId}`
- Params: `id: int, categoryId: int`
- Returns: `200 T55`
- Returns: `200 T56`
### Section: /printer
#### List printers — `GET /printer`
_none_
- Returns: `200 { printers: T32[] }`
- Returns: `200 { printers: T33[] }`
#### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 T26`
- Returns: `201 T27`
#### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }`
@@ -696,22 +718,22 @@ _none_
- Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}`
- Params: `id: int`
- Returns: `200 T26`
- Returns: `200 T27`
#### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T26`
- Returns: `200 T27`
#### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T26`
- Returns: `200 T27`
### Section: /propertyname
#### List property names — `GET /propertyname`
_none_
- Returns: `200 { propertynames: T41[] }`
- Returns: `200 { propertynames: T42[] }`
#### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T41 }`
- Returns: `201 { propertyname: T42 }`
#### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }`
@@ -720,7 +742,7 @@ _none_
- Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T41 }`
- Returns: `200 { propertyname: T42 }`
### Section: /public
@@ -740,20 +762,29 @@ _none_
### Section: /quote
#### List quotes — `GET /quote`
_none_
- Returns: `200 { quotes: { id: int, number: str, type: str, entityId: int|null, date: str, createdAt?: str }[] }`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T9[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T33[], entityId?: int|null }`
- Returns: `201 T12`
- Params: `body: { number: str, date: str, lines: T34[], entityId?: int|null }`
- Returns: `201 T8`
#### Delete quote — `DELETE /quote/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get quote — `GET /quote/{id}`
- Params: `id: int`
- Returns: `200 T12`
- Returns: `200 T8`
#### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T33[] }`
- Returns: `200 T12`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T34[] }`
- Returns: `200 T8`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Get quote attachment file content — `GET /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int`
- Returns: `201 T13`
### Section: /serper
@@ -777,13 +808,22 @@ _none_
- Returns: `204 void`
#### Get shipment by ID — `GET /shipment/{id}`
- Params: `id: int`
- Returns: `200 { shipment: T7 }`
- Returns: `200 { shipment: T5 }`
#### Delete shipment file attachment — `DELETE /shipment/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Get shipment attachment file content — `GET /shipment/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
- Params: `id: int`
- Returns: `201 T13`
#### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int`
- Returns: `200 { shippings: T36[] }`
- Returns: `200 { shippings: T37[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `201 T31`
- Returns: `201 T32`
### Section: /shipping
@@ -792,7 +832,7 @@ _none_
- Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `200 T31`
- Returns: `200 T32`
#### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -810,28 +850,28 @@ _none_
#### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T20[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T21[] }`
### Section: /supplier
#### List suppliers — `GET /supplier`
_none_
- Returns: `200 { suppliers: T49[] }`
- Returns: `200 { suppliers: T50[] }`
#### Create supplier — `POST /supplier`
- Params: `body: { name: str }`
- Returns: `201 { supplier: T49 }`
- Returns: `201 { supplier: T50 }`
#### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T49 }`
- Returns: `200 { supplier: T50 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: string`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T54|null }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T53|null }[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T54|null }[] }`
### Section: /thumb
@@ -846,10 +886,10 @@ _none_
- Returns: `204 void`
#### List units — `GET /unit`
_none_
- Returns: `200 { units: T39[] }`
- Returns: `200 { units: T40[] }`
#### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T39 }`
- Returns: `200 { unit: T40 }`
### Section: /v1
@@ -903,10 +943,10 @@ _none_
#### List warehouses — `GET /warehouse`
_none_
- Returns: `200 { warehouses: T54[] }`
- Returns: `200 { warehouses: T55[] }`
#### Create warehouse — `POST /warehouse`
- Params: `body: { name: str, sort?: int }`
- Returns: `201 T52`
- Returns: `201 T53`
#### Reorder warehouses — `PUT /warehouse/order`
- Params: `body: { warehouseIds: int[] }`
- Returns: `200 { warehouseIds: int[] }`
@@ -915,10 +955,10 @@ _none_
- Returns: `204 void`
#### Get warehouse by ID — `GET /warehouse/{id}`
- Params: `id: int`
- Returns: `200 T52`
- Returns: `200 T53`
#### Update warehouse (partial) — `PATCH /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T52`
- Returns: `200 T53`
#### Update warehouse — `PUT /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T52`
- Returns: `200 T53`

View File

@@ -1,6 +1,6 @@
# Dash API Routes Overview
Overview of all 164 API endpoints across all modules.
Overview of all 171 API endpoints across all modules.
### Notation
@@ -10,50 +10,51 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
### Reusable types
- `T1` = `{ order: T2 }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T22[], createdAt?: str, shipping?: T30|null, shippings?: T30[], payment?: T36|null, payments?: T36[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T7[] }`
- `T2` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T23[], createdAt?: str, shipping?: T31|null, shippings?: T31[], payment?: T37|null, payments?: T37[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T5[], files?: T13[] }`
- `T3` = `{ article: T4 }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T33, brand: T31|null, categoryIds: int[], photos: T28[], eans: str[], prices: T38[], suppliers: T13[] }`
- `T5` = `{ delivery: T6 }`
- `T6` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T22[], files: T11[], externalNumber?: str|null, createdAt?: str, ledger?: T42 }`
- `T7` = `{ id: int, number: str, type: str, date: str, lines: T22[], shippings: T30[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int }`
- `T8` = `{ labeltemplate: T9 }`
- `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T18, thumb?: str|null }`
- `T10` = `{ quote: T12 }`
- `T11` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, thumbavif?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T12` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T22[], createdAt?: str }`
- `T13` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }`
- `T14` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T15` = `{ ledger: T16[] }`
- `T16` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T17` = `{ id: int, name: str, sort: int, exclusive: bool, values: T34[], articleCount?: int }`
- `T18` = `{ objects: T23[] }`
- `T4` = `{ id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T34, brand: T32|null, categoryIds: int[], photos: T29[], eans: str[], prices: T39[], suppliers: T14[] }`
- `T5` = `{ id: int, number: str, type: str, date: str, lines: T23[], shippings: T31[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T13[] }`
- `T6` = `{ delivery: T7 }`
- `T7` = `{ id: int, number: str, type: str, supplierId: int|null, date: str, lines: T23[], files: T13[], externalNumber?: str|null, createdAt?: str, ledger?: T43 }`
- `T8` = `{ quote: T9 }`
- `T9` = `{ id: int, number: str, type: str, entityId: int|null, date: str, lines: T23[], createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T13[] }`
- `T10` = `{ labeltemplate: T12 }`
- `T11` = `{ attachment: T13, fileUrl: str }`
- `T12` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: T19, thumb?: str|null }`
- `T13` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, thumbavif?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }`
- `T14` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T45|null }`
- `T15` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- `T16` = `{ ledger: T17[] }`
- `T17` = `{ id: int, manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str, hash_version: int, prev_hash?: str, sku?: str|null, created_at?: str }`
- `T18` = `{ id: int, name: str, sort: int, exclusive: bool, values: T35[], articleCount?: int }`
- `T19` = `{ objects: T24[] }`
- `T20` = `{ printer: T26 }`
- `T21` = `{ entity: T29 }`
- `T22` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T23` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T20` = `{ objects: T25[] }`
- `T21` = `{ printer: T27 }`
- `T22` = `{ entity: T30 }`
- `T23` = `{ id: int, sku: str|null, name: str|null, pu: num, amount: num, net: num, rebate: num, taxrate: num, comment: str|null }`
- `T24` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T25` = `{ shipping: T30 }`
- `T26` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T27` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T28` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T29` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T30` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T31` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T32` = `{ article: T37 }`
- `T33` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T34` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T35` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T36` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T37` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T38` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T39` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T40` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T41` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T42` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T43` = `{ warehouse: T45 }`
- `T44` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T45` = `{ id: int, name: str, sort: int }`
- `T25` = `{ id: str, type: str, x: num, y: num, w?: num, h?: num, text?: str, font?: str, fontSize?: num, barcodeFormat?: str }`
- `T26` = `{ shipping: T31 }`
- `T27` = `{ id: int, company_id: int, address: str, type: str, sort: int, model?: str|null, serial?: str|null, desc?: str|null }`
- `T28` = `{ amount: num, net: num, taxrate: num, sku?: str|null, name?: str|null, pu?: num, rebate?: num, comment?: str|null }`
- `T29` = `{ sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }`
- `T30` = `{ id: int, customergroupId: int, firstName: str|null, name: str, number: str|null, createdAt: str, updatedAt: str }`
- `T31` = `{ id: int, manifestId: int, number: str, type: str, state: str, trackingCode?: str|null, carrier?: str|null }`
- `T32` = `{ name: str, id?: int|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null }`
- `T33` = `{ article: T38 }`
- `T34` = `{ si: str, basesi: str, basesifactor: num, symbol?: str|null, name?: str|null }`
- `T35` = `{ id: int, attributeId: int, value: str, sort: int, articleCount?: int }`
- `T36` = `{ id: int, name: str, sort: int, exclusive: bool, articleCount?: int }`
- `T37` = `{ id: int, accountId: int, manifestId: int, type: str, amount: num }`
- `T38` = `{ id: int, categoryId: int, name: str, sku: str, track_stock: bool }`
- `T39` = `{ minmultiplier: num, net: num, taxRate: num, customergroupId: int }`
- `T40` = `{ fullHash: str, thumbW: int, thumbH: int, thumbHash: str }`
- `T41` = `{ id: int, entityId: int, contactType: str, contact: str }`
- `T42` = `{ id: int, name: str, createdAt: str, updatedAt: str }`
- `T43` = `{ totalPositions: int, completelyOnLedgerPositions: int }`
- `T44` = `{ warehouse: T46 }`
- `T45` = `{ net: num, taxId: int, taxRate?: num|null }`
- `T46` = `{ id: int, name: str, sort: int }`
### Routes
@@ -62,7 +63,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### Create article in category — `POST /article`
- Params: `body: { categoryId: int, name: str, desc?: str, seo?: str, track_stock?: bool }`
- Returns: `201 T32`
- Returns: `201 T33`
#### Delete article by SKU — `DELETE /article/{sku}`
- Params: `sku: str, query: cascade?: bool`
- Returns: `204 void`
@@ -95,10 +96,10 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Create and attach a new child article to a parent article — `POST /article/{sku}/children`
- Params: `sku: str, body: { attributeValueId?: int, value?: str, name?: str }`
- Returns: `201 T32`
- Returns: `201 T33`
#### Attach an existing article as a child to a parent article — `PUT /article/{sku}/children`
- Params: `sku: str, body: { sku: str, attributeValueId?: int, value?: str, reparent?: bool }`
- Returns: `200 { article: T37, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
- Returns: `200 { article: T38, children: { id: int, sku: str, name: str, parentsort: int, attributeValueId: int, attributeValue: str }[] }`
#### Remove a child article from a parent article — `DELETE /article/{sku}/children/{childSku}`
- Params: `sku: str, childSku: str, query: deleteArticle?: bool`
- Returns: `200 { success: bool, deleted: bool }`
@@ -122,7 +123,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { options: { id: int, articleId: int, sku: str, name: str, count: int, sort: int, mandated: bool }[] }`
#### Toggle parent mode for an article — `POST /article/{sku}/parent`
- Params: `sku: str, body: { isParent: bool, attributeNameId?: int }`
- Returns: `200 T32`
- Returns: `200 T33`
#### Replace POS prices by SKU (net > gross; price=net, tax=taxRate %) — `PUT /article/{sku}/prices`
- Params: `sku: str, body: { prices: { net?: num, gross?: num, price?: num, taxRate?: num, tax?: num, minmultiplier?: num }[] }`
- Returns: `200 T3`
@@ -140,10 +141,10 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `200 { ok: bool }`
#### List article suppliers — `GET /article/{sku}/supplier`
- Params: `sku: str`
- Returns: `200 { suppliers: T13[] }`
- Returns: `200 { suppliers: T14[] }`
#### Assign supplier to article — `POST /article/{sku}/supplier`
- Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `201 { supplier: T13 }`
- Returns: `201 { supplier: T14 }`
#### Reorder article suppliers — `PUT /article/{sku}/supplier/order`
- Params: `sku: str, body: { ids: int[] }`
- Returns: `200 { ids: int[] }`
@@ -152,7 +153,7 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
- Returns: `204 void`
#### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}`
- Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }`
- Returns: `200 { supplier: T13 }`
- Returns: `200 { supplier: T14 }`
#### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock`
- Params: `sku: str, body: { track_stock: bool }`
- Returns: `200 T3`
@@ -164,19 +165,19 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body).
#### List attribute names — `GET /attributename`
_none_
- Returns: `200 { attributenames: T17[] }`
- Returns: `200 { attributenames: T18[] }`
#### Create attribute name — `POST /attributename`
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { attributename: T17 }`
- Returns: `201 { attributename: T18 }`
#### Reorder attribute names — `PUT /attributename/order`
- Params: `body: { attributenameIds: int[] }`
- Returns: `200 { ok: bool }`
#### List values for an attribute — `GET /attributename/{attributeId}/value`
- Params: `attributeId: int`
- Returns: `200 { values: T34[] }`
- Returns: `200 { values: T35[] }`
#### Create value for an attribute — `POST /attributename/{attributeId}/value`
- Params: `attributeId: int, body: { value: str, sort?: int }`
- Returns: `201 { attributevalue: T34 }`
- Returns: `201 { attributevalue: T35 }`
#### Reorder values of an attribute — `PUT /attributename/{attributeId}/value/order`
- Params: `attributeId: int, body: { attributevalueIds: int[] }`
- Returns: `200 { ok: bool }`
@@ -185,7 +186,7 @@ _none_
- Returns: `200 { ok: bool }`
#### Update attribute name — `PATCH /attributename/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { attributename: T17 }`
- Returns: `200 { attributename: T18 }`
### Section: /attributevalue
@@ -194,13 +195,13 @@ _none_
- Returns: `200 { ok: bool }`
#### Update attribute value — `PATCH /attributevalue/{id}`
- Params: `id: int, body: { value?: str, sort?: int }`
- Returns: `200 { attributevalue: T34 }`
- Returns: `200 { attributevalue: T35 }`
#### Delete attribute value image — `DELETE /attributevalue/{id}/image`
- Params: `id: int`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T39[] } }`
- Returns: `200 { attributevalue: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T40[] } }`
#### Add or replace attribute value image — `POST /attributevalue/{id}/image`
- Params: `id: int, image/avif`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T39[] } }`
- Returns: `201 { attributevalue: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T40[] } }`
### Section: /brand
@@ -209,7 +210,7 @@ _none_
- Returns: `200 { brands: { id: int, name: str, articleCount: int, pics: { fullHash: str, thumbW: int, thumbH: int, thumbHash: str, smallThumbW?: int|null, smallThumbH?: int|null, smallThumbBinary?: str|null }[], gsprpinfo?: str|null }[], total: int, offset: int, limit: int }`
#### Create brand — `POST /brand`
- Params: `body: { name: str, gsprpinfo?: str }`
- Returns: `201 { brand: { id: int, name: str, pics: T39[], gsprpinfo?: str|null } }`
- Returns: `201 { brand: { id: int, name: str, pics: T40[], gsprpinfo?: str|null } }`
#### Delete brand — `DELETE /brand/{id}`
- Params: `id: int`
- Returns: `204 void`
@@ -218,10 +219,10 @@ _none_
- Returns: `200 { brand: { id: int, name: str, touchedArticleCount: int, gsprpinfo?: str|null } }`
#### Delete brand image — `DELETE /brand/{id}/image`
- Params: `id: int`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T39[] } }`
- Returns: `200 { brand: { id: int, thumbW: int|null, thumbH: int|null, thumbHash: str|null, pics: T40[] } }`
#### Add or replace brand image — `POST /brand/{id}/image`
- Params: `id: int, image/avif`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T39[] } }`
- Returns: `201 { brand: { id: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str, pics: T40[] } }`
### Section: /category
@@ -242,7 +243,7 @@ _none_
- Returns: `200 { category: { id: int, name: str, slug: str, parentId: int, sort: int } }`
#### Articles in category — `GET /category/{id}/articles`
- Params: `id: int`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T31|null }[] }`
- Returns: `200 { articles: { id: int, sku: str, name: str, slug: str, track_stock: bool, optionsCount: int, articleListCount: int, childCount: int, net?: num|null, taxRate?: num|null, parentArticleId?: int|null, parentSku?: str|null, fullHash?: str|null, thumbW?: int|null, thumbH?: int|null, thumbHash?: str|null, brand?: T32|null }[] }`
#### List category attribute values — `GET /category/{id}/attributevalue`
- Params: `id: int`
- Returns: `200 { attributevalues: { id: int, attributeId: int, attributeName: str, attributeValueId: int, value: str, sort: int, categoryId?: int, exclusive?: bool }[] }`
@@ -296,7 +297,7 @@ _none_
- Returns: `200 { customergroups: { id: int, name: str, sort: int, editable: bool }[] }`
#### Create customer group — `POST /customergroup`
- Params: `body: { name: str }`
- Returns: `201 { customergroup: T45 }`
- Returns: `201 { customergroup: T46 }`
#### Reorder customer groups — `PUT /customergroup/order`
- Params: `body: { customergroupIds: int[] }`
- Returns: `200 { customergroupIds: int[] }`
@@ -305,73 +306,76 @@ _none_
- Returns: `204 void`
#### Update customer group — `PATCH /customergroup/{id}`
- Params: `id: int, body: { name: str }`
- Returns: `200 { customergroup: T45 }`
- Returns: `200 { customergroup: T46 }`
### Section: /delivery
#### List deliveries — `GET /delivery`
_none_
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T42 }[] }`
- Returns: `200 { deliveries: { id: int, number: str, type: str, supplierId: int|null, date: str, externalNumber?: str|null, createdAt?: str, ledger?: T43 }[] }`
#### Create delivery — `POST /delivery`
- Params: `body: { externalNumber: str, date: str, lines: T27[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T5`
- Params: `body: { externalNumber: str, date: str, lines: T28[], supplierId?: int|null, files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `201 T6`
#### Delete delivery — `DELETE /delivery/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get delivery — `GET /delivery/{id}`
- Params: `id: int`
- Returns: `200 T5`
- Returns: `200 T6`
#### Update delivery — `PATCH /delivery/{id}`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T27[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T5`
#### Get delivery attachment file content — `GET /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int, query: download?: bool`
- Returns: `200 str`
- Params: `id: int, body: { externalNumber?: str|null, date?: str, supplierId?: int|null, lines?: T28[], files?: { mimetype: str, name: str, content: str, sort?: int, size?: int, hash?: str, created?: str, modified?: str, meta?: { [k]: str } }[] }`
- Returns: `200 T6`
#### Delete delivery file attachment — `DELETE /delivery/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf`
- Params: `id: int`
- Returns: `201 T11`
### Section: /entity
#### List entities — `GET /entity`
- Params: `query: customergroupId?: int, role?: str`
- Returns: `200 { entities: T29[] }`
- Returns: `200 { entities: T30[] }`
#### Create entity — `POST /entity`
- Params: `body: { customergroupId: int, name: str, firstName?: str|null, number?: str }`
- Returns: `201 T21`
- Returns: `201 T22`
#### Peek next customer number — `GET /entity/next-number`
_none_
- Returns: `200 { number: str }`
#### List entity addresses — `GET /entity/{entityId}/address`
- Params: `entityId: int`
- Returns: `200 { addresses: T14[] }`
- Returns: `200 { addresses: T15[] }`
#### Create entity address — `POST /entity/{entityId}/address`
- Params: `entityId: int, body: { addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `201 { address: T14 }`
- Returns: `201 { address: T15 }`
#### Delete entity address — `DELETE /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int`
- Returns: `204 void`
#### Update entity address — `PATCH /entity/{entityId}/address/{id}`
- Params: `entityId: int, id: int, body: { addressType?: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }`
- Returns: `200 { address: T14 }`
- Returns: `200 { address: T15 }`
#### List entity contacts — `GET /entity/{entityId}/contact`
- Params: `entityId: int`
- Returns: `200 { contacts: T40[] }`
- Returns: `200 { contacts: T41[] }`
#### Create entity contact — `POST /entity/{entityId}/contact`
- Params: `entityId: int, body: { contactType: str, contact: str }`
- Returns: `201 { contact: T40 }`
- Returns: `201 { contact: T41 }`
#### Delete entity contact — `DELETE /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int`
- Returns: `204 void`
#### Update entity contact — `PATCH /entity/{entityId}/contact/{id}`
- Params: `entityId: int, id: int, body: { contactType?: str, contact?: str }`
- Returns: `200 { contact: T40 }`
- Returns: `200 { contact: T41 }`
#### Delete entity — `DELETE /entity/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get entity — `GET /entity/{id}`
- Params: `id: int`
- Returns: `200 T21`
- Returns: `200 T22`
#### Update entity — `PATCH /entity/{id}`
- Params: `id: int, body: { name?: str, firstName?: str|null, number?: str|null }`
- Returns: `200 T21`
- Returns: `200 T22`
### Section: /labeltemplate
@@ -379,35 +383,35 @@ _none_
_none_
- Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }`
#### Create label template — `POST /labeltemplate`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T19 }`
- Returns: `201 T8`
- Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: T20 }`
- Returns: `201 T10`
#### Delete label template — `DELETE /labeltemplate/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get label template by ID — `GET /labeltemplate/{id}`
- Params: `id: int`
- Returns: `200 T8`
- Returns: `200 T10`
#### Update label template (partial) — `PATCH /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T19 }`
- Returns: `200 T8`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T20 }`
- Returns: `200 T10`
#### Update label template — `PUT /labeltemplate/{id}`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T19 }`
- Returns: `200 T8`
- Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: T20 }`
- Returns: `200 T10`
### Section: /ledger
#### Query ledger by manifest article ID or SKU — `GET /ledger`
- Params: `query: manifestarticle_id?: int, sku?: str, transaction_type?: str`
- Returns: `200 T15`
- Returns: `200 T16`
#### Add ledger entry — `POST /ledger`
- Params: `body: { manifestarticle_id: int, transaction_type: str, amount: num, transaction_text: str }`
- Returns: `201 { ledger: T16 }`
- Returns: `201 { ledger: T17 }`
#### Query ledger by manifest article ID — `GET /ledger/manifestarticle/{manifestarticleId}`
- Params: `manifestarticleId: int, query: transaction_type?: str`
- Returns: `200 T15`
- Returns: `200 T16`
#### Query ledger by article SKU — `GET /ledger/sku/{sku}`
- Params: `sku: str, query: transaction_type?: str`
- Returns: `200 T15`
- Returns: `200 T16`
### Section: /number
@@ -424,7 +428,7 @@ _none_
- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int`
- Returns: `200 { orders: T2[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create order — `POST /order`
- Params: `body: { number: str, date: str, lines: T27[], entityId?: int|null }`
- Params: `body: { number: str, date: str, lines: T28[], entityId?: int|null }`
- Returns: `201 T1`
#### Delete order — `DELETE /order/{id}`
- Params: `id: int`
@@ -433,26 +437,32 @@ _none_
- Params: `id: int`
- Returns: `200 T1`
#### Update order — `PATCH /order/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T27[] }`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T28[] }`
- Returns: `200 T1`
#### Delete order file attachment — `DELETE /order/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### List payments of an order — `GET /order/{id}/payment`
- Params: `id: int`
- Returns: `200 { payments: T36[] }`
- Returns: `200 { payments: T37[] }`
#### Add payment to an order — `POST /order/{id}/payment`
- Params: `id: int, body: { accountId: int, type: str, amount: num }`
- Returns: `201 { payment: T36 }`
- Returns: `201 { payment: T37 }`
#### Generate and attach order PDF — `POST /order/{id}/pdf`
- Params: `id: int`
- Returns: `201 T11`
#### List shipments of an order — `GET /order/{id}/shipment`
- Params: `id: int`
- Returns: `200 { shipments: T7[] }`
- Returns: `200 { shipments: T5[] }`
#### Create shipment for an order — `POST /order/{id}/shipment`
- Params: `id: int, body: { lines: { sku: str, amount: num, name?: str, pu?: num, net?: num, rebate?: num, taxrate?: num, comment?: str }[], date?: str, externalNumber?: str }`
- Returns: `201 { shipment: T7 }`
- Returns: `201 { shipment: T5 }`
#### List shippings of an order — `GET /order/{id}/shipping`
- Params: `id: int`
- Returns: `200 { shippings: T30[] }`
- Returns: `200 { shippings: T31[] }`
#### Create shipping for an order — `POST /order/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, warehouseId?: int }`
- Returns: `201 T25`
- Returns: `201 T26`
### Section: /package
@@ -470,10 +480,10 @@ _none_
#### List printers — `GET /printer`
_none_
- Returns: `200 { printers: T26[] }`
- Returns: `200 { printers: T27[] }`
#### Create printer — `POST /printer`
- Params: `body: { address: str, type: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `201 T20`
- Returns: `201 T21`
#### Reorder printers — `PUT /printer/order`
- Params: `body: { printerIds: int[] }`
- Returns: `200 { printerIds: int[] }`
@@ -482,22 +492,22 @@ _none_
- Returns: `204 void`
#### Get printer by ID — `GET /printer/{id}`
- Params: `id: int`
- Returns: `200 T20`
- Returns: `200 T21`
#### Update printer (partial) — `PATCH /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T20`
- Returns: `200 T21`
#### Update printer — `PUT /printer/{id}`
- Params: `id: int, body: { address?: str, type?: str, model?: str|null, serial?: str|null, sort?: int, desc?: str|null }`
- Returns: `200 T20`
- Returns: `200 T21`
### Section: /propertyname
#### List property names — `GET /propertyname`
_none_
- Returns: `200 { propertynames: T35[] }`
- Returns: `200 { propertynames: T36[] }`
#### Create property name — `POST /propertyname`
- Params: `body: { name: str, sort?: int, exclusive?: bool }`
- Returns: `201 { propertyname: T35 }`
- Returns: `201 { propertyname: T36 }`
#### Reorder property names — `PUT /propertyname/order`
- Params: `body: { propertynameIds: int[] }`
- Returns: `200 { ok: bool }`
@@ -506,25 +516,31 @@ _none_
- Returns: `200 { ok: bool }`
#### Update property name — `PATCH /propertyname/{id}`
- Params: `id: int, body: { name?: str, sort?: int, exclusive?: bool }`
- Returns: `200 { propertyname: T35 }`
- Returns: `200 { propertyname: T36 }`
### Section: /quote
#### List quotes — `GET /quote`
_none_
- Returns: `200 { quotes: { id: int, number: str, type: str, entityId: int|null, date: str, createdAt?: str }[] }`
- Params: `query: offset?: int, limit?: int, search?: str, seekMonths?: int`
- Returns: `200 { quotes: T9[], total: int, unfilteredTotal: int, offset: int, limit: int }`
#### Create quote — `POST /quote`
- Params: `body: { number: str, date: str, lines: T27[], entityId?: int|null }`
- Returns: `201 T10`
- Params: `body: { number: str, date: str, lines: T28[], entityId?: int|null }`
- Returns: `201 T8`
#### Delete quote — `DELETE /quote/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Get quote — `GET /quote/{id}`
- Params: `id: int`
- Returns: `200 T10`
- Returns: `200 T8`
#### Update quote — `PATCH /quote/{id}`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T27[] }`
- Returns: `200 T10`
- Params: `id: int, body: { number?: str, date?: str, entityId?: int|null, lines?: T28[] }`
- Returns: `200 T8`
#### Delete quote file attachment — `DELETE /quote/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Generate and attach quote PDF — `POST /quote/{id}/pdf`
- Params: `id: int`
- Returns: `201 T11`
### Section: /shipment
@@ -533,13 +549,19 @@ _none_
- Returns: `204 void`
#### Get shipment by ID — `GET /shipment/{id}`
- Params: `id: int`
- Returns: `200 { shipment: T7 }`
- Returns: `200 { shipment: T5 }`
#### Delete shipment file attachment — `DELETE /shipment/{id}/file/{fileId}`
- Params: `id: int, fileId: int`
- Returns: `204 void`
#### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf`
- Params: `id: int`
- Returns: `201 T11`
#### List packages of a shipment — `GET /shipment/{id}/shipping`
- Params: `id: int`
- Returns: `200 { shippings: T30[] }`
- Returns: `200 { shippings: T31[] }`
#### Create package for a shipment — `POST /shipment/{id}/shipping`
- Params: `id: int, body: { type: str, state: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `201 T25`
- Returns: `201 T26`
### Section: /shipping
@@ -548,7 +570,7 @@ _none_
- Returns: `204 void`
#### Update shipping — `PATCH /shipping/{id}`
- Params: `id: int, body: { type?: str, state?: str, trackingCode?: str, carrier?: str, warehouseId?: int }`
- Returns: `200 T25`
- Returns: `200 T26`
#### List packages of a shipping — `GET /shipping/{id}/package`
- Params: `id: int`
- Returns: `200 { packages: { id: int, shippingId: int, entityaddressId: int, number: str, type: str, state: str, details?: str|null }[] }`
@@ -566,28 +588,28 @@ _none_
#### Adjust stock manually — `POST /stock/adjust`
- Params: `body: { sku?: str, warehouseId?: int, amount?: num, text?: str|null, lines?: { sku: str, amount: num, warehouseId?: int, text?: str }[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T16[] }`
- Returns: `201 { manifest: { id: int, number: str, type: str, date: str, createdAt: str }, lines: { manifestarticle_id: int, sku: str, name: str, warehouse_id: int, amount: num, text: str }[], ledger: T17[] }`
### Section: /supplier
#### List suppliers — `GET /supplier`
_none_
- Returns: `200 { suppliers: T41[] }`
- Returns: `200 { suppliers: T42[] }`
#### Create supplier — `POST /supplier`
- Params: `body: { name: str }`
- Returns: `201 { supplier: T41 }`
- Returns: `201 { supplier: T42 }`
#### Delete supplier — `DELETE /supplier/{id}`
- Params: `id: int`
- Returns: `204 void`
#### Update supplier — `PATCH /supplier/{id}`
- Params: `id: int, body: { name: str }`
- Returns: `200 { supplier: T41 }`
- Returns: `200 { supplier: T42 }`
#### List supplier articles by SKUs — `GET /supplier/{id}/articles`
- Params: `id: int, query: skus?: string`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T45|null }[] }`
#### Query supplier articles by SKUs (bulk) — `POST /supplier/{id}/articles`
- Params: `id: int, body?: { skus?: str[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T44|null }[] }`
- Returns: `200 { articles: { id: int, articleId: int, entityId: int, pu: num, sort: int, article: { id: int, sku: str, name?: str|null, slug?: str|null, photos?: { sort: int, fullHash: str, thumbW: int, thumbH: int, thumbHash: str }[], eans?: str[] }, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T45|null }[] }`
### Section: /unit
@@ -596,19 +618,19 @@ _none_
- Returns: `204 void`
#### List units — `GET /unit`
_none_
- Returns: `200 { units: T33[] }`
- Returns: `200 { units: T34[] }`
#### Upsert unit — `PUT /unit`
- Params: `body: { si: str, basesi: str, basesifactor: num, symbol: str, name: str }`
- Returns: `200 { unit: T33 }`
- Returns: `200 { unit: T34 }`
### Section: /warehouse
#### List warehouses — `GET /warehouse`
_none_
- Returns: `200 { warehouses: T45[] }`
- Returns: `200 { warehouses: T46[] }`
#### Create warehouse — `POST /warehouse`
- Params: `body: { name: str, sort?: int }`
- Returns: `201 T43`
- Returns: `201 T44`
#### Reorder warehouses — `PUT /warehouse/order`
- Params: `body: { warehouseIds: int[] }`
- Returns: `200 { warehouseIds: int[] }`
@@ -617,13 +639,13 @@ _none_
- Returns: `204 void`
#### Get warehouse by ID — `GET /warehouse/{id}`
- Params: `id: int`
- Returns: `200 T43`
- Returns: `200 T44`
#### Update warehouse (partial) — `PATCH /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T43`
- Returns: `200 T44`
#### Update warehouse — `PUT /warehouse/{id}`
- Params: `id: int, body: { name?: str, sort?: int }`
- Returns: `200 T43`
- Returns: `200 T44`
### Binary & streaming routes (reachable only via `fetch()` in javascript_execute)
These routes are NOT in the table above because their responses are binary or long-lived streams, which the `dashback_request` tool cannot return usefully. They ARE reachable from inside `javascript_execute` with `fetch("/route")`, where you can decode the bytes, write them to the VFS and embed them in your reply:

View File

@@ -0,0 +1,33 @@
# `GET /company/export`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Export company catalog as zip archive (SSE)
Exports full company catalog data (categories hierarchy, articles, variants, brands, pictures)
into a round-trip compatible .zip archive matching the format of `/company/import`.
Streams progress events via Server-Sent Events (`text/event-stream`), followed by base64-encoded
binary chunks in `chunk` events and a final `done` event.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `stream` | no | string | Force SSE stream mode (`1`) or direct binary download (`0`) |
## Response `200`
Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
## Errors
| Status | Description |
|--------|-------------|
| 400 | Bad request (no company selected) — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,23 @@
# `POST /company/export`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Export company catalog as zip archive (SSE)
POST variant of `/company/export` with identical SSE streaming semantics.
## Response `200`
Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
## Errors
| Status | Description |
|--------|-------------|
| 400 | Bad request (no company selected) — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 500 | Server error — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,38 @@
# `DELETE /delivery/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete delivery file attachment
Removes a file attachment from the delivery and deletes the underlying file.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Delivery ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Signals
On success, emits SSE signal: `delivery` (see [`GET /events`](../events.md)).
## Response `204`
Attachment deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid parameters or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Delivery or attachment not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,58 @@
# `POST /delivery/{id}/pdf`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Generate and attach delivery note PDF (goods receipt)
Generates an A4 PDF document for the goods receipt / delivery note with line items and quantities.
Attaches the PDF as a `manifestattachment` to the delivery.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Delivery ID |
## Signals
On success, emits SSE signal: `delivery` (see [`GET /events`](../events.md)).
## Response `201`
PDF generated and attached to delivery
```json
{
"attachment": {
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
},
"fileUrl": "/delivery/1/file/5"
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Delivery not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,38 @@
# `DELETE /order/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete order file attachment
Removes a file attachment from the order and deletes the underlying file.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Order ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Signals
On success, emits SSE signal: `order` (see [`GET /events`](../events.md)).
## Response `204`
Attachment deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid parameters or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Order or attachment not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,38 @@
# `GET /order/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get order attachment file content
Returns the full binary content of an attached file (e.g. order PDF)
for the specified order. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Order ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `download` | no | boolean | When true, sets Content-Disposition to attachment instead of inline |
## Response `200`
Binary file content
## Errors
| Status | Description |
|--------|-------------|
| 304 | Not modified |
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 404 | Order or file not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,58 @@
# `POST /order/{id}/pdf`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Generate and attach order PDF
Generates an A4 PDF document for the sales order with line items, prices, and tax breakdown.
Attaches the PDF as a `manifestattachment` to the order so it can be printed or archived.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Order ID |
## Signals
On success, emits SSE signal: `order` (see [`GET /events`](../events.md)).
## Response `201`
PDF generated and attached to order
```json
{
"attachment": {
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
},
"fileUrl": "/order/1/file/5"
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Order not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -52,6 +52,24 @@ List of shipments
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
]

View File

@@ -89,6 +89,24 @@ Created shipment
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -140,8 +140,44 @@ Created order
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -115,8 +115,44 @@ Order
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -124,8 +124,44 @@ Order list
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
],

View File

@@ -134,8 +134,44 @@ Updated order
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -5,7 +5,7 @@ Public. No cookie required.
POS discovery and pairing
Special API for POS bind and sync.
- Step 1 (Discovery): `authCode` length 1–4. Returns certificate fingerprints, serial number, and mandantId.
- Step 1 (Discovery): `authCode` length 1-4. Returns certificate fingerprints, serial number, and mandantId.
- Step 2 (Pairing): `authCode` length 6 (pairingpin from posterminal). Verifies the pairingpin, sets token, paired_at, and clientserial, revokes the pairingpin, and returns device credentials and company metadata.

View File

@@ -0,0 +1,38 @@
# `DELETE /quote/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete quote file attachment
Removes a file attachment from the quote and deletes the underlying file.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Quote ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Signals
On success, emits SSE signal: `quote` (see [`GET /events`](../events.md)).
## Response `204`
Attachment deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid parameters or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Quote or attachment not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,38 @@
# `GET /quote/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get quote attachment file content
Returns the full binary content of an attached file (e.g. quote PDF)
for the specified quote. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Quote ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `download` | no | boolean | When true, sets Content-Disposition to attachment instead of inline |
## Response `200`
Binary file content
## Errors
| Status | Description |
|--------|-------------|
| 304 | Not modified |
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 404 | Quote or file not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,58 @@
# `POST /quote/{id}/pdf`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Generate and attach quote PDF
Generates an A4 PDF document for the quote with prices, net sum, and tax breakdown.
Stores the PDF as a `manifestattachment` for printing or downloading.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Quote ID |
## Signals
On success, emits SSE signal: `quote` (see [`GET /events`](../events.md)).
## Response `201`
PDF generated and attached to quote
```json
{
"attachment": {
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
},
"fileUrl": "/quote/1/file/5"
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Quote not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -50,6 +50,10 @@ Created quote
"entityId": 1,
"date": "2026-08-10T00:00:00.000Z",
"createdAt": "2026-08-10T00:00:00.000Z",
"positions": 1,
"grossTotal": 1,
"totalQuantity": 1,
"customerName": "…",
"lines": [
{
"id": 1,
@@ -62,6 +66,24 @@ Created quote
"taxrate": 1,
"comment": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -25,6 +25,10 @@ Quote
"entityId": 1,
"date": "2026-08-10T00:00:00.000Z",
"createdAt": "2026-08-10T00:00:00.000Z",
"positions": 1,
"grossTotal": 1,
"totalQuantity": 1,
"customerName": "…",
"lines": [
{
"id": 1,
@@ -37,6 +41,24 @@ Quote
"taxrate": 1,
"comment": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -8,6 +8,15 @@ Lists quotes (`manifest` rows with `type = 'quote'`)
for the authenticated user's selected company.
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `offset` | no | integer | Number of quotes to skip |
| `limit` | no | integer | Number of quotes to return |
| `search` | no | string | Free-text search matching quote number, external number, or customer name |
| `seekMonths` | no | integer | Negative integer for cutoff months ago (e.g. -1 for quotes older than 1 month) |
## Response `200`
Quote list
@@ -21,9 +30,48 @@ Quote list
"type": "quote",
"entityId": 1,
"date": "2026-08-10T00:00:00.000Z",
"createdAt": "2026-08-10T00:00:00.000Z"
"createdAt": "2026-08-10T00:00:00.000Z",
"positions": 1,
"grossTotal": 1,
"totalQuantity": 1,
"customerName": "…",
"lines": [
{
"id": 1,
"sku": "…",
"name": "…",
"pu": 1,
"amount": 1,
"net": 1,
"rebate": 1,
"taxrate": 1,
"comment": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
]
],
"total": 1,
"unfilteredTotal": 1,
"offset": 1,
"limit": 1
}
```
@@ -31,7 +79,7 @@ Quote list
| Status | Description |
|--------|-------------|
| 400 | No company selected — `{"error":"…"}` |
| 400 | Invalid query parameters, invalid offset, invalid limit, or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Company not found — `{"error":"…"}` |

View File

@@ -44,6 +44,10 @@ Updated quote
"entityId": 1,
"date": "2026-08-10T00:00:00.000Z",
"createdAt": "2026-08-10T00:00:00.000Z",
"positions": 1,
"grossTotal": 1,
"totalQuantity": 1,
"customerName": "…",
"lines": [
{
"id": 1,
@@ -56,6 +60,24 @@ Updated quote
"taxrate": 1,
"comment": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -51,6 +51,24 @@ Shipment details
"type": "…",
"state": "…"
}
],
"files": [
{
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
}
]
}
}

View File

@@ -0,0 +1,38 @@
# `DELETE /shipment/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Delete shipment file attachment
Removes a file attachment from the shipment and deletes the underlying file.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Shipment ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Signals
On success, emits SSE signals: `shipment`, `order` (see [`GET /events`](../events.md)).
## Response `204`
Attachment deleted
_No content_
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid parameters or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Shipment or attachment not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,38 @@
# `GET /shipment/{id}/file/{fileId}`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Get shipment attachment file content
Returns the full binary content of an attached file (e.g. shipment delivery note PDF)
for the specified shipment. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Shipment ID |
| `fileId` | yes | integer | File ID or attachment ID |
## Query parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `download` | no | boolean | When true, sets Content-Disposition to attachment instead of inline |
## Response `200`
Binary file content
## Errors
| Status | Description |
|--------|-------------|
| 304 | Not modified |
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 404 | Shipment or file not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

View File

@@ -0,0 +1,59 @@
# `POST /shipment/{id}/pdf`
Protected. Requires a valid session cookie (`credentials: 'include'`).
Generate and attach shipment PDF (delivery note)
Generates an A4 delivery note PDF document for the shipment with line items and package tracking.
Shipping slips contain no price information.
Attaches the PDF as a `manifestattachment` to the shipment so it can be printed or attached to packages.
## Path parameters
| Param | Required | Type | Description |
|-------|----------|------|-------------|
| `id` | yes | integer | Shipment ID |
## Signals
On success, emits SSE signal: `shipment` (see [`GET /events`](../events.md)).
## Response `201`
PDF generated and attached to shipment
```json
{
"attachment": {
"id": 1,
"manifestId": 1,
"fileId": 1,
"sort": 1,
"hash": "…",
"mimetype": "…",
"name": "…",
"size": 1,
"created": "2026-08-10T00:00:00.000Z",
"modified": "2026-08-10T00:00:00.000Z",
"thumbavif": "…",
"thumbx": 1,
"thumby": 1,
"meta": {}
},
"fileUrl": "/shipment/1/file/5"
}
```
## Errors
| Status | Description |
|--------|-------------|
| 400 | Invalid ID or no company selected — `{"error":"…"}` |
| 401 | Unauthorized — `{"error":"…"}` |
| 403 | Not authorized — `{"error":"…"}` |
| 404 | Shipment not found — `{"error":"…"}` |
---
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._

2
mcp.md
View File

@@ -226,7 +226,7 @@ server.registerTool('search-articles', {
searchquery: z.string().nullable().optional().describe('Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).'),
excludewords: z.string().nullable().optional().describe('Keywords to exclude (AND logic).'),
offset: z.number().int().min(0).nullable().optional().describe('Pagination offset (default 0).'),
limit: z.number().int().min(1).max(20).nullable().optional().describe('Window size 1–20 (default 3).'),
limit: z.number().int().min(1).max(20).nullable().optional().describe('Window size 1-20 (default 3).'),
},
}, async ({ searchquery, excludewords, offset, limit }) => { ... });
```

View File

@@ -25,13 +25,19 @@ const OMITTED_ROUTES = new Set([
'GET /category/export.zip',
'DELETE /company/{id}',
'POST /company',
'GET /company/export',
'POST /company/export',
'POST /category/{id}/image',
'GET /delivery/{id}/file/{fileId}',
'POST /delivery/parse',
'GET /events',
'GET /health',
'GET /image/{fullHash}',
'GET /order/{id}/file/{fileId}',
'GET /quote/{id}/file/{fileId}',
'POST /serper/image',
'POST /serper/images',
'GET /shipment/{id}/file/{fileId}',
'GET /thumb/{fullHash}',
'GET /v1/category',
'GET /v1/client',

View File

@@ -965,6 +965,18 @@
* createdAt:
* type: string
* format: date-time
* positions:
* type: integer
* description: Number of line items
* grossTotal:
* type: number
* description: Gross sum across quote positions
* totalQuantity:
* type: number
* description: Total quantity across quote positions
* customerName:
* type: string
* nullable: true
* Quote:
* allOf:
* - $ref: '#/components/schemas/QuoteSummary'
@@ -975,6 +987,11 @@
* type: array
* items:
* $ref: '#/components/schemas/ManifestLine'
* files:
* type: array
* description: Attached documents (e.g. quote PDF) for the quote
* items:
* $ref: '#/components/schemas/DeliveryFile'
* OrderSummary:
* type: object
* required: [id, number, type, entityId, date]
@@ -1045,6 +1062,11 @@
* type: array
* items:
* $ref: '#/components/schemas/Shipment'
* files:
* type: array
* description: Attached documents (e.g. order PDF) for the order
* items:
* $ref: '#/components/schemas/DeliveryFile'
* ArticleImage:
* type: object
* required: [picId, sort, full, thumb, hash, thumbHash]
@@ -1276,6 +1298,11 @@
* type: array
* items:
* $ref: '#/components/schemas/Shipping'
* files:
* type: array
* description: Attached documents (e.g. shipment label PDF) for the shipment
* items:
* $ref: '#/components/schemas/DeliveryFile'
* Package:
* type: object
* required: [id, shippingId, entityaddressId, number, type, state]

View File

@@ -7,6 +7,7 @@ import * as googleAuth from '../../db/googleAuth.js';
import { clearCachedUser } from '../middleware/auth.js';
import { initCompanyVfs } from '../../tools/vfs.js';
import { executeCompanyImport } from '../../services/companyImport.js';
import { executeCompanyExport } from '../../services/companyExport.js';
// #region Helpers
@@ -605,6 +606,189 @@ export function register(app, basePath = '') {
// #endregion
/**
* @openapi
* /company/export:
* get:
* tags: [Company]
* summary: Export company catalog as zip archive (SSE)
* description: |
* Exports full company catalog data (categories hierarchy, articles, variants, brands, pictures)
* into a round-trip compatible .zip archive matching the format of `/company/import`.
* Streams progress events via Server-Sent Events (`text/event-stream`), followed by base64-encoded
* binary chunks in `chunk` events and a final `done` event.
* security:
* - cookieAuth: []
* x-markdown: company/export.md
* parameters:
* - in: query
* name: stream
* schema:
* type: string
* enum: ['0', '1']
* description: Force SSE stream mode (`1`) or direct binary download (`0`)
* responses:
* 200:
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
* content:
* text/event-stream:
* schema:
* type: string
* application/zip:
* schema:
* type: string
* format: binary
* 400:
* description: Bad request (no company selected)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* post:
* tags: [Company]
* summary: Export company catalog as zip archive (SSE)
* description: POST variant of `/company/export` with identical SSE streaming semantics.
* security:
* - cookieAuth: []
* responses:
* 200:
* description: Export progress stream (`text/event-stream`) with binary chunks, or raw ZIP file
* content:
* text/event-stream:
* schema:
* type: string
* application/zip:
* schema:
* type: string
* format: binary
* 400:
* description: Bad request (no company selected)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 500:
* description: Server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET/POST /company/export
const handleCompanyExport = async (req, res) => {
let companyId;
try {
companyId = requireSelectedCompany(req);
} catch (error) {
return res.status(error.statusCode || 400).json({ error: error.message });
}
const wantsSse =
req.headers.accept?.includes('text/event-stream') ||
req.query.stream === '1' ||
req.query.sse === '1' ||
(!req.headers.accept?.includes('application/zip') && !req.path.endsWith('.zip') && req.query.download !== '1');
if (wantsSse) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
if (typeof res.flushHeaders === 'function') {
res.flushHeaders();
}
const sseWrite = (event, data) => {
if (res.writableEnded || res.destroyed) return;
const payload = typeof data === 'string' ? data : JSON.stringify(data);
res.write(`event: ${event}\ndata: ${payload}\n\n`);
if (typeof res.flush === 'function') res.flush();
};
sseWrite('status', { message: 'Export gestartet. Daten werden verarbeitet...' });
try {
const outcome = await executeCompanyExport({
googleAuthId: req.user.id,
companyId,
onStatus: (status) => sseWrite('status', status),
onProgress: (counts) => sseWrite('progress', counts),
});
// Stream binary chunks as base64 in SSE 'chunk' events
const CHUNK_SIZE = 64 * 1024; // 64 KB binary slice per SSE event
const { zipBuffer, filename, stats } = outcome;
const totalChunks = Math.ceil(zipBuffer.length / CHUNK_SIZE);
sseWrite('status', {
message: `Sende Archiv (${(zipBuffer.length / (1024 * 1024)).toFixed(2)} MB in ${totalChunks} Chunks)...`,
});
for (let i = 0; i < totalChunks; i++) {
if (res.writableEnded || res.destroyed) break;
const slice = zipBuffer.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
const base64 = slice.toString('base64');
res.write(`event: chunk\ndata: ${base64}\n\n`);
if (typeof res.flush === 'function') res.flush();
}
sseWrite('done', {
ok: true,
filename,
size: zipBuffer.length,
totalChunks,
stats,
});
res.end();
} catch (err) {
console.error('[Company Export] Export execution failed:', err);
sseWrite('error', { message: err.message || 'Export fehlgeschlagen' });
res.end();
}
} else {
// Direct binary download
try {
const outcome = await executeCompanyExport({
googleAuthId: req.user.id,
companyId,
});
res.setHeader('Content-Type', 'application/zip');
res.setHeader('Content-Disposition', `attachment; filename="${outcome.filename}"`);
res.status(200).send(outcome.zipBuffer);
} catch (err) {
console.error('[Company Export] Export failed:', err);
const status = companyErrorStatus(err);
res.status(status).json({ error: err.message || 'Export fehlgeschlagen' });
}
}
};
app.get(`${basePath}/company/export`, handleCompanyExport);
app.get(`${basePath}/company/export.zip`, handleCompanyExport);
app.post(`${basePath}/company/export`, handleCompanyExport);
// #endregion
/**
* @openapi
* /company/{id}:

View File

@@ -1,6 +1,8 @@
import multer from 'multer';
import * as db from '../../db/delivery.js';
import { createAndAttachManifestPdf } from '../../services/manifestPdf.js';
import { writeErrorLog } from '../../util/errorLog.js';
import { emit } from '../sse.js';
// #region Helpers
@@ -902,4 +904,181 @@ export function register(app, basePath = '') {
app.get(`${basePath}/delivery/:id/files/:fileId`, handleGetDeliveryFile);
// #endregion
/**
* @openapi
* /delivery/{id}/pdf:
* post:
* tags: [Delivery]
* summary: Generate and attach delivery note PDF (goods receipt)
* description: |
* Generates an A4 PDF document for the goods receipt / delivery note with line items and quantities.
* Attaches the PDF as a `manifestattachment` to the delivery.
* security:
* - cookieAuth: []
* x-signals: [delivery]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Delivery ID
* responses:
* 201:
* description: PDF generated and attached to delivery
* content:
* application/json:
* schema:
* type: object
* required: [attachment, fileUrl]
* properties:
* attachment:
* $ref: '#/components/schemas/DeliveryFile'
* fileUrl:
* type: string
* example: /delivery/1/file/5
* 400:
* description: 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: Delivery not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /delivery/:id/pdf
app.post(`${basePath}/delivery/:id/pdf`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
try {
const { attachment } = await createAndAttachManifestPdf(
req.user.id,
selected.companyId,
id,
'delivery'
);
res.status(201).json({
attachment,
fileUrl: `/delivery/${id}/file/${attachment.fileId}`,
});
} catch (error) {
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Create delivery PDF failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /delivery/{id}/file/{fileId}:
* delete:
* tags: [Delivery]
* summary: Delete delivery file attachment
* description: |
* Removes a file attachment from the delivery and deletes the underlying file.
* security:
* - cookieAuth: []
* x-signals: [delivery]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Delivery ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* responses:
* 204:
* description: Attachment deleted
* 400:
* description: Invalid parameters 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: Delivery or attachment not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /delivery/:id/file/:fileId
app.delete(`${basePath}/delivery/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
await db.removeAttachment(req.user.id, id, fileId);
emit('delivery', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = deliveryErrorStatus(error);
if (status === 500) console.error('Delete delivery attachment failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -115,8 +115,8 @@ const tokenVerifier = {
const scopes = typeof decoded.scope === 'string'
? decoded.scope.split(' ')
: Array.isArray(decoded.scope)
? decoded.scope
: ['mcp:tools'];
? decoded.scope
: ['mcp:tools'];
return {
token,
@@ -237,7 +237,7 @@ function createDashbackMcpServer({ user }) {
searchquery: z.string().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.searchquery || 'Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).'),
excludewords: z.string().nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.excludewords || 'Keywords to exclude (AND logic).'),
offset: z.number().int().min(0).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.offset || 'Pagination offset (default 0).'),
limit: z.number().int().min(1).max(20).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.limit || 'Window size 1–20 (default 3).'),
limit: z.number().int().min(1).max(20).nullable().optional().describe(TOOL_PARAMETER_DESCRIPTIONS.search_articles?.limit || 'Window size 1-20 (default 3).'),
},
},
async ({ searchquery = '', excludewords = '', offset = 0, limit = 3 } = {}) => {

View File

@@ -1,4 +1,6 @@
import * as db from '../../db/order.js';
import { createAndAttachManifestPdf } from '../../services/manifestPdf.js';
import { emit } from '../sse.js';
// #region Helpers
@@ -15,6 +17,8 @@ function orderErrorStatus(error) {
if (error.message === 'Not authorized') return 403;
if (error.message === 'Company not found') return 404;
if (error.message === 'Order not found') return 404;
if (error.message === 'Manifest not found') return 404;
if (error.message === 'Attachment not found') return 404;
if (error.message === 'Entity not found') return 404;
if (error.message === 'Article not found') return 404;
if (error.message === 'Invalid entityId') return 400;
@@ -23,6 +27,9 @@ function orderErrorStatus(error) {
if (error.message === 'Invalid offset') return 400;
if (error.message === 'Invalid limit') return 400;
if (error.message === 'Invalid seekMonths') return 400;
if (error.message === 'File name required') return 400;
if (error.message === 'Mimetype required') return 400;
if (error.message === 'Content required') return 400;
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
return 500;
}
@@ -567,4 +574,294 @@ export function register(app, basePath = '') {
});
// #endregion
/**
* @openapi
* /order/{id}/pdf:
* post:
* tags: [Order]
* summary: Generate and attach order PDF
* description: |
* Generates an A4 PDF document for the sales order with line items, prices, and tax breakdown.
* Attaches the PDF as a `manifestattachment` to the order so it can be printed or archived.
* security:
* - cookieAuth: []
* x-signals: [order]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Order ID
* responses:
* 201:
* description: PDF generated and attached to order
* content:
* application/json:
* schema:
* type: object
* required: [attachment, fileUrl]
* properties:
* attachment:
* $ref: '#/components/schemas/DeliveryFile'
* fileUrl:
* type: string
* example: /order/1/file/5
* 400:
* description: 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: Order not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /order/:id/pdf
app.post(`${basePath}/order/:id/pdf`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
try {
const { attachment } = await createAndAttachManifestPdf(
req.user.id,
selected.companyId,
id,
'order'
);
res.status(201).json({
attachment,
fileUrl: `/order/${id}/file/${attachment.fileId}`,
});
} catch (error) {
const status = orderErrorStatus(error);
if (status === 500) console.error('Create order PDF failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /order/{id}/file/{fileId}:
* get:
* tags: [Order]
* summary: Get order attachment file content
* description: |
* Returns the full binary content of an attached file (e.g. order PDF)
* for the specified order. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Order ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* - in: query
* name: download
* required: false
* schema:
* type: boolean
* description: When true, sets Content-Disposition to attachment instead of inline
* responses:
* 200:
* description: Binary file content
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/octet-stream:
* schema:
* type: string
* format: binary
* 304:
* description: Not modified
* 400:
* description: Invalid ID or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Order or file not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /order/:id/file/:fileId
app.get(`${basePath}/order/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
const attachment = await db.getFile(req.user.id, id, fileId);
if (attachment.hash) {
const etag = `"${attachment.hash}"`;
res.setHeader('ETag', etag);
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch === etag || ifNoneMatch === attachment.hash) {
return res.status(304).end();
}
}
const isDownload = req.query.download === '1' || req.query.download === 'true';
const dispositionType = isDownload ? 'attachment' : 'inline';
const filename = attachment.name || `file-${attachment.fileId}`;
const asciiName = filename.replace(/["\r\n\\]/g, '_');
const encodedName = encodeURIComponent(filename).replace(/['()]/g, escape);
res.setHeader('Content-Type', attachment.mimetype || 'application/octet-stream');
res.setHeader('Content-Disposition', `${dispositionType}; filename="${asciiName}"; filename*=UTF-8''${encodedName}`);
res.setHeader('Cache-Control', 'private, max-age=86400');
if (attachment.content && Buffer.isBuffer(attachment.content)) {
res.setHeader('Content-Length', String(attachment.content.length));
return res.status(200).send(attachment.content);
} else {
const buffer = Buffer.from(attachment.content || '');
res.setHeader('Content-Length', String(buffer.length));
return res.status(200).send(buffer);
}
} catch (error) {
const status = orderErrorStatus(error);
if (status === 500) console.error('Get order file failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /order/{id}/file/{fileId}:
* delete:
* tags: [Order]
* summary: Delete order file attachment
* description: |
* Removes a file attachment from the order and deletes the underlying file.
* security:
* - cookieAuth: []
* x-signals: [order]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Order ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* responses:
* 204:
* description: Attachment deleted
* 400:
* description: Invalid parameters 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: Order or attachment not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /order/:id/file/:fileId
app.delete(`${basePath}/order/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
await db.removeAttachment(req.user.id, id, fileId);
emit('order', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = orderErrorStatus(error);
if (status === 500) console.error('Delete order attachment failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -1,4 +1,6 @@
import * as db from '../../db/quote.js';
import { createAndAttachManifestPdf } from '../../services/manifestPdf.js';
import { emit } from '../sse.js';
// #region Helpers
@@ -15,11 +17,18 @@ function quoteErrorStatus(error) {
if (error.message === 'Not authorized') return 403;
if (error.message === 'Company not found') return 404;
if (error.message === 'Quote not found') return 404;
if (error.message === 'Manifest not found') return 404;
if (error.message === 'Attachment not found') return 404;
if (error.message === 'Entity not found') return 404;
if (error.message === 'Article not found') return 404;
if (error.message === 'Invalid entityId') return 400;
if (error.message === 'Invalid quote') return 400;
if (error.message === 'Invalid lines') return 400;
if (error.message === 'File name required') return 400;
if (error.message === 'Mimetype required') return 400;
if (error.message === 'Content required') return 400;
if (error.message === 'Invalid attachment order') return 400;
if (error.message?.includes('Invalid target id')) return 400;
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
return 500;
}
@@ -43,6 +52,32 @@ export function register(app, basePath = '') {
* for the authenticated user's selected company.
* security:
* - cookieAuth: []
* parameters:
* - in: query
* name: offset
* schema:
* type: integer
* default: 0
* minimum: 0
* description: Number of quotes to skip
* - in: query
* name: limit
* schema:
* type: integer
* default: 50
* minimum: 1
* maximum: 100
* description: Number of quotes to return
* - in: query
* name: search
* schema:
* type: string
* description: Free-text search matching quote number, external number, or customer name
* - in: query
* name: seekMonths
* schema:
* type: integer
* description: Negative integer for cutoff months ago (e.g. -1 for quotes older than 1 month)
* x-markdown: quote/list.md
* responses:
* 200:
@@ -51,13 +86,26 @@ export function register(app, basePath = '') {
* application/json:
* schema:
* type: object
* required: [quotes, total, unfilteredTotal, offset, limit]
* properties:
* quotes:
* type: array
* items:
* $ref: '#/components/schemas/QuoteSummary'
* $ref: '#/components/schemas/Quote'
* total:
* type: integer
* description: Total number of filtered quotes
* unfilteredTotal:
* type: integer
* description: Total number of quotes without any filters applied
* offset:
* type: integer
* description: Number of quotes skipped
* limit:
* type: integer
* description: Number of quotes returned
* 400:
* description: No company selected
* description: Invalid query parameters, invalid offset, invalid limit, or no company selected
* content:
* application/json:
* schema:
@@ -167,9 +215,47 @@ export function register(app, basePath = '') {
return res.status(400).json({ error: selected.error });
}
const rawOffset = req.query.offset;
const rawLimit = req.query.limit;
let offset = 0;
let limit = 50;
if (rawOffset !== undefined) {
offset = parseInt(rawOffset, 10);
if (!Number.isInteger(offset) || offset < 0) {
return res.status(400).json({ error: 'offset must be an integer >= 0' });
}
}
if (rawLimit !== undefined) {
limit = parseInt(rawLimit, 10);
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
return res.status(400).json({ error: 'limit must be an integer between 1 and 100' });
}
}
const search = typeof req.query.search === 'string' && req.query.search.trim() !== ''
? req.query.search.trim()
: undefined;
let seekMonths = undefined;
if (req.query.seekMonths !== undefined && req.query.seekMonths !== '') {
const parsedSeek = parseInt(req.query.seekMonths, 10);
if (!Number.isInteger(parsedSeek)) {
return res.status(400).json({ error: 'seekMonths must be an integer' });
}
seekMonths = parsedSeek;
}
try {
const quotes = await db.list(req.user.id);
res.status(200).json({ quotes });
const result = await db.list(req.user.id, {
offset,
limit,
search,
seekMonths,
});
res.status(200).json(result);
} catch (error) {
const status = quoteErrorStatus(error);
if (status === 500) console.error('List quotes failed:', error);
@@ -469,4 +555,294 @@ export function register(app, basePath = '') {
});
// #endregion
/**
* @openapi
* /quote/{id}/pdf:
* post:
* tags: [Quote]
* summary: Generate and attach quote PDF
* description: |
* Generates an A4 PDF document for the quote with prices, net sum, and tax breakdown.
* Stores the PDF as a `manifestattachment` for printing or downloading.
* security:
* - cookieAuth: []
* x-signals: [quote]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Quote ID
* responses:
* 201:
* description: PDF generated and attached to quote
* content:
* application/json:
* schema:
* type: object
* required: [attachment, fileUrl]
* properties:
* attachment:
* $ref: '#/components/schemas/DeliveryFile'
* fileUrl:
* type: string
* example: /quote/1/file/5
* 400:
* description: 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: Quote not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /quote/:id/pdf
app.post(`${basePath}/quote/:id/pdf`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
try {
const { attachment } = await createAndAttachManifestPdf(
req.user.id,
selected.companyId,
id,
'quote'
);
res.status(201).json({
attachment,
fileUrl: `/quote/${id}/file/${attachment.fileId}`,
});
} catch (error) {
const status = quoteErrorStatus(error);
if (status === 500) console.error('Create quote PDF failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /quote/{id}/file/{fileId}:
* get:
* tags: [Quote]
* summary: Get quote attachment file content
* description: |
* Returns the full binary content of an attached file (e.g. quote PDF)
* for the specified quote. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Quote ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* - in: query
* name: download
* required: false
* schema:
* type: boolean
* description: When true, sets Content-Disposition to attachment instead of inline
* responses:
* 200:
* description: Binary file content
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/octet-stream:
* schema:
* type: string
* format: binary
* 304:
* description: Not modified
* 400:
* description: Invalid ID or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Quote or file not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /quote/:id/file/:fileId
app.get(`${basePath}/quote/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
const attachment = await db.getFile(req.user.id, id, fileId);
if (attachment.hash) {
const etag = `"${attachment.hash}"`;
res.setHeader('ETag', etag);
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch === etag || ifNoneMatch === attachment.hash) {
return res.status(304).end();
}
}
const isDownload = req.query.download === '1' || req.query.download === 'true';
const dispositionType = isDownload ? 'attachment' : 'inline';
const filename = attachment.name || `file-${attachment.fileId}`;
const asciiName = filename.replace(/["\r\n\\]/g, '_');
const encodedName = encodeURIComponent(filename).replace(/['()]/g, escape);
res.setHeader('Content-Type', attachment.mimetype || 'application/octet-stream');
res.setHeader('Content-Disposition', `${dispositionType}; filename="${asciiName}"; filename*=UTF-8''${encodedName}`);
res.setHeader('Cache-Control', 'private, max-age=86400');
if (attachment.content && Buffer.isBuffer(attachment.content)) {
res.setHeader('Content-Length', String(attachment.content.length));
return res.status(200).send(attachment.content);
} else {
const buffer = Buffer.from(attachment.content || '');
res.setHeader('Content-Length', String(buffer.length));
return res.status(200).send(buffer);
}
} catch (error) {
const status = quoteErrorStatus(error);
if (status === 500) console.error('Get quote file failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /quote/{id}/file/{fileId}:
* delete:
* tags: [Quote]
* summary: Delete quote file attachment
* description: |
* Removes a file attachment from the quote and deletes the underlying file.
* security:
* - cookieAuth: []
* x-signals: [quote]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Quote ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* responses:
* 204:
* description: Attachment deleted
* 400:
* description: Invalid parameters 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: Quote or attachment not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /quote/:id/file/:fileId
app.delete(`${basePath}/quote/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
await db.removeAttachment(req.user.id, id, fileId);
emit('quote', { companyId: selected.companyId });
res.sendStatus(204);
} catch (error) {
const status = quoteErrorStatus(error);
if (status === 500) console.error('Delete quote attachment failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -1,4 +1,5 @@
import * as db from '../../db/shipment.js';
import { createAndAttachManifestPdf } from '../../services/manifestPdf.js';
import { emit } from '../sse.js';
// #region Helpers
@@ -383,4 +384,296 @@ export function register(app, basePath = '') {
});
// #endregion
/**
* @openapi
* /shipment/{id}/pdf:
* post:
* tags: [Shipment]
* summary: Generate and attach shipment PDF (delivery note)
* description: |
* Generates an A4 delivery note PDF document for the shipment with line items and package tracking.
* Shipping slips contain no price information.
* Attaches the PDF as a `manifestattachment` to the shipment so it can be printed or attached to packages.
* security:
* - cookieAuth: []
* x-signals: [shipment]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Shipment ID
* responses:
* 201:
* description: PDF generated and attached to shipment
* content:
* application/json:
* schema:
* type: object
* required: [attachment, fileUrl]
* properties:
* attachment:
* $ref: '#/components/schemas/DeliveryFile'
* fileUrl:
* type: string
* example: /shipment/1/file/5
* 400:
* description: 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: Shipment not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region POST /shipment/:id/pdf
app.post(`${basePath}/shipment/:id/pdf`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsedId = parsePositiveInt(req.params.id, 'id');
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
try {
const { attachment } = await createAndAttachManifestPdf(
req.user.id,
selected.companyId,
parsedId.id,
'shipment'
);
res.status(201).json({
attachment,
fileUrl: `/shipment/${parsedId.id}/file/${attachment.fileId}`,
});
} catch (error) {
const status = shipmentErrorStatus(error);
if (status === 500) console.error('Create shipment PDF failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /shipment/{id}/file/{fileId}:
* get:
* tags: [Shipment]
* summary: Get shipment attachment file content
* description: |
* Returns the full binary content of an attached file (e.g. shipment delivery note PDF)
* for the specified shipment. Supports HTTP caching via ETag (SHA-256 hash) and 304 Not Modified.
* security:
* - cookieAuth: []
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Shipment ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* - in: query
* name: download
* required: false
* schema:
* type: boolean
* description: When true, sets Content-Disposition to attachment instead of inline
* responses:
* 200:
* description: Binary file content
* content:
* application/pdf:
* schema:
* type: string
* format: binary
* application/octet-stream:
* schema:
* type: string
* format: binary
* 304:
* description: Not modified
* 400:
* description: Invalid ID or no company selected
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 404:
* description: Shipment or file not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region GET /shipment/:id/file/:fileId
app.get(`${basePath}/shipment/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const parsedId = parsePositiveInt(req.params.id, 'id');
if (parsedId.error) {
return res.status(400).json({ error: parsedId.error });
}
const parsedFileId = parsePositiveInt(req.params.fileId, 'fileId');
if (parsedFileId.error) {
return res.status(400).json({ error: parsedFileId.error });
}
try {
const attachment = await db.getFile(req.user.id, parsedId.id, parsedFileId.id);
if (attachment.hash) {
const etag = `"${attachment.hash}"`;
res.setHeader('ETag', etag);
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch === etag || ifNoneMatch === attachment.hash) {
return res.status(304).end();
}
}
const isDownload = req.query.download === '1' || req.query.download === 'true';
const dispositionType = isDownload ? 'attachment' : 'inline';
const filename = attachment.name || `file-${attachment.fileId}`;
const asciiName = filename.replace(/["\r\n\\]/g, '_');
const encodedName = encodeURIComponent(filename).replace(/['()]/g, escape);
res.setHeader('Content-Type', attachment.mimetype || 'application/octet-stream');
res.setHeader('Content-Disposition', `${dispositionType}; filename="${asciiName}"; filename*=UTF-8''${encodedName}`);
res.setHeader('Cache-Control', 'private, max-age=86400');
if (attachment.content && Buffer.isBuffer(attachment.content)) {
res.setHeader('Content-Length', String(attachment.content.length));
return res.status(200).send(attachment.content);
} else {
const buffer = Buffer.from(attachment.content || '');
res.setHeader('Content-Length', String(buffer.length));
return res.status(200).send(buffer);
}
} catch (error) {
const status = shipmentErrorStatus(error);
if (status === 500) console.error('Get shipment file failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
/**
* @openapi
* /shipment/{id}/file/{fileId}:
* delete:
* tags: [Shipment]
* summary: Delete shipment file attachment
* description: |
* Removes a file attachment from the shipment and deletes the underlying file.
* security:
* - cookieAuth: []
* x-signals: [shipment, order]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* description: Shipment ID
* - in: path
* name: fileId
* required: true
* schema:
* type: integer
* description: File ID or attachment ID
* responses:
* 204:
* description: Attachment deleted
* 400:
* description: Invalid parameters 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: Shipment or attachment not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
// #region DELETE /shipment/:id/file/:fileId
app.delete(`${basePath}/shipment/:id/file/:fileId`, async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'id is required' });
}
const fileId = Number(req.params.fileId);
if (!Number.isInteger(fileId) || fileId <= 0) {
return res.status(400).json({ error: 'fileId is required' });
}
try {
await db.removeAttachment(req.user.id, id, fileId);
emitShipmentEvents(selected.companyId);
res.sendStatus(204);
} catch (error) {
const status = shipmentErrorStatus(error);
if (status === 500) console.error('Delete shipment attachment failed:', error);
res.status(status).json({ error: error.message });
}
});
// #endregion
}

View File

@@ -29,3 +29,20 @@ export async function getFile(googleAuthId, deliveryId, fileId) {
const store = await getStore();
return store.manifestattachment.getById(googleAuthId, deliveryId, fileId);
}
export async function addAttachment(googleAuthId, deliveryId, data) {
const store = await getStore();
return store.manifestattachment.add(googleAuthId, deliveryId, data);
}
export async function listAttachments(googleAuthId, deliveryId) {
const store = await getStore();
return store.manifestattachment.listByManifestId(googleAuthId, deliveryId);
}
export async function removeAttachment(googleAuthId, deliveryId, fileId) {
const store = await getStore();
return store.manifestattachment.remove(googleAuthId, deliveryId, fileId);
}

View File

@@ -46,16 +46,18 @@ function calculateShippedQuantity(shipments = []) {
async function enrichSingleOrder(store, googleAuthId, order) {
if (!order) return order;
const [shippings, payments, shipments] = await Promise.all([
const [shippings, payments, shipments, files] = await Promise.all([
store.shipping.listByManifestId(googleAuthId, order.id).catch(() => []),
store.payment.listByManifestId(googleAuthId, order.id).catch(() => []),
store.shipment.listByOrderId(googleAuthId, order.id).catch(() => []),
store.manifestattachment.listByManifestId(googleAuthId, order.id).catch(() => []),
]);
const orderLines = Array.isArray(order.lines) ? order.lines : [];
const paymentsList = Array.isArray(payments) ? payments : [];
const shipmentsList = Array.isArray(shipments) ? shipments : [];
const shippingsList = Array.isArray(shippings) ? shippings : [];
const filesList = Array.isArray(files) ? files : [];
const orderTotal = calculateOrderGrossTotal(orderLines);
const paidTotal = calculatePaidTotal(paymentsList);
@@ -74,6 +76,7 @@ async function enrichSingleOrder(store, googleAuthId, order) {
payment: paymentsList[0] ?? null,
payments: paymentsList,
shipments: shipmentsList,
files: filesList,
orderTotal,
paidTotal,
orderedQuantity,
@@ -99,6 +102,7 @@ function formatEnrichedFromSummary(order) {
payment: null,
payments: [],
shipments: [],
files: [],
orderTotal: grossTotal,
paidTotal,
orderedQuantity,
@@ -154,3 +158,25 @@ export async function remove(googleAuthId, id) {
return store.order.delete(googleAuthId, id);
}
export async function getFile(googleAuthId, orderId, fileId) {
const store = await getStore();
return store.manifestattachment.getById(googleAuthId, orderId, fileId);
}
export async function addAttachment(googleAuthId, orderId, data) {
const store = await getStore();
return store.manifestattachment.add(googleAuthId, orderId, data);
}
export async function listAttachments(googleAuthId, orderId) {
const store = await getStore();
return store.manifestattachment.listByManifestId(googleAuthId, orderId);
}
export async function removeAttachment(googleAuthId, orderId, fileId) {
const store = await getStore();
return store.manifestattachment.remove(googleAuthId, orderId, fileId);
}

View File

@@ -1,26 +1,120 @@
import { getStore } from './store.js';
export async function list(googleAuthId) {
function round2(num) {
return Math.round((Number(num) || 0) * 100) / 100;
}
function calculateQuoteGrossTotal(lines = []) {
let total = 0;
for (const line of lines) {
const amount = Number(line.amount || 0);
const net = Number(line.net || 0);
const rebate = Number(line.rebate || 0);
const taxrate = Number(line.taxrate || 0);
const netAfterRebate = net * (1 - rebate / 100);
const gross = netAfterRebate * (1 + taxrate / 100);
total += amount * gross;
}
return round2(total);
}
function calculateTotalQuantity(lines = []) {
let qty = 0;
for (const line of lines) {
qty += Number(line.amount || 0);
}
return Math.round(qty * 1000) / 1000;
}
function enrichQuote(quote, files = []) {
if (!quote) return quote;
const lines = Array.isArray(quote.lines) ? quote.lines : [];
const positions = quote.positions !== undefined ? quote.positions : lines.length;
const grossTotal = quote.grossTotal !== undefined ? quote.grossTotal : calculateQuoteGrossTotal(lines);
const totalQuantity = quote.totalQuantity !== undefined ? quote.totalQuantity : calculateTotalQuantity(lines);
return {
...quote,
positions,
grossTotal,
totalQuantity,
files: Array.isArray(files) ? files : (quote.files || []),
};
}
export async function list(googleAuthId, opts) {
const store = await getStore();
return store.quote.list(googleAuthId);
const result = await store.quote.list(googleAuthId, opts);
if (Array.isArray(result)) {
const filesList = await Promise.all(
result.map((q) => store.manifestattachment.listByManifestId(googleAuthId, q.id).catch(() => []))
);
return result.map((q, i) => enrichQuote(q, filesList[i]));
}
if (result && Array.isArray(result.quotes)) {
const filesList = await Promise.all(
result.quotes.map((q) => store.manifestattachment.listByManifestId(googleAuthId, q.id).catch(() => []))
);
return {
...result,
quotes: result.quotes.map((q, i) => enrichQuote(q, filesList[i])),
};
}
return result;
}
export async function getById(googleAuthId, id) {
const store = await getStore();
return store.quote.getById(googleAuthId, id);
const [quote, files] = await Promise.all([
store.quote.getById(googleAuthId, id),
store.manifestattachment.listByManifestId(googleAuthId, id).catch(() => []),
]);
return enrichQuote(quote, files);
}
export async function create(googleAuthId, data) {
const store = await getStore();
return store.quote.create(googleAuthId, data);
const quote = await store.quote.create(googleAuthId, data);
return enrichQuote(quote, []);
}
export async function update(googleAuthId, id, data) {
const store = await getStore();
return store.quote.update(googleAuthId, id, data);
const [quote, files] = await Promise.all([
store.quote.update(googleAuthId, id, data),
store.manifestattachment.listByManifestId(googleAuthId, id).catch(() => []),
]);
return enrichQuote(quote, files);
}
export async function remove(googleAuthId, id) {
const store = await getStore();
return store.quote.delete(googleAuthId, id);
}
export async function rebuildSummaries(googleAuthId) {
const store = await getStore();
return store.quote.rebuildSummaries(googleAuthId);
}
export async function getFile(googleAuthId, quoteId, fileId) {
const store = await getStore();
return store.manifestattachment.getById(googleAuthId, quoteId, fileId);
}
export async function addAttachment(googleAuthId, quoteId, data) {
const store = await getStore();
return store.manifestattachment.add(googleAuthId, quoteId, data);
}
export async function listAttachments(googleAuthId, quoteId) {
const store = await getStore();
return store.manifestattachment.listByManifestId(googleAuthId, quoteId);
}
export async function removeAttachment(googleAuthId, quoteId, fileId) {
const store = await getStore();
return store.manifestattachment.remove(googleAuthId, quoteId, fileId);
}

View File

@@ -7,7 +7,15 @@ export async function create(googleAuthId, data) {
export async function getById(googleAuthId, id) {
const store = await getStore();
return store.shipment.getById(googleAuthId, id);
const [shipment, files] = await Promise.all([
store.shipment.getById(googleAuthId, id),
store.manifestattachment.listByManifestId(googleAuthId, id).catch(() => []),
]);
if (!shipment) return shipment;
return {
...shipment,
files: Array.isArray(files) ? files : [],
};
}
export async function listByOrderId(googleAuthId, orderId) {
@@ -19,3 +27,25 @@ export async function remove(googleAuthId, id) {
const store = await getStore();
return store.shipment.delete(googleAuthId, id);
}
export async function getFile(googleAuthId, shipmentId, fileId) {
const store = await getStore();
return store.manifestattachment.getById(googleAuthId, shipmentId, fileId);
}
export async function addAttachment(googleAuthId, shipmentId, data) {
const store = await getStore();
return store.manifestattachment.add(googleAuthId, shipmentId, data);
}
export async function listAttachments(googleAuthId, shipmentId) {
const store = await getStore();
return store.manifestattachment.listByManifestId(googleAuthId, shipmentId);
}
export async function removeAttachment(googleAuthId, shipmentId, fileId) {
const store = await getStore();
return store.manifestattachment.remove(googleAuthId, shipmentId, fileId);
}

View File

@@ -1,4 +1,4 @@
// @applet-input-schema {"type":"object","properties":{"query":{"type":"string","title":"Category name contains","default":"","description":"Case-insensitive substring match on the category name. Leave empty to include every category."},"limit":{"type":"integer","title":"Max rows","default":100,"minimum":1,"maximum":500},"sortBy":{"type":"string","title":"Sort by","default":"articles","enum":["articles","name","depth"],"description":"articles = most articles first, name = A–Z, depth = tree depth then name."}}}
// @applet-input-schema {"type":"object","properties":{"query":{"type":"string","title":"Category name contains","default":"","description":"Case-insensitive substring match on the category name. Leave empty to include every category."},"limit":{"type":"integer","title":"Max rows","default":100,"minimum":1,"maximum":500},"sortBy":{"type":"string","title":"Sort by","default":"articles","enum":["articles","name","depth"],"description":"articles = most articles first, name = A-Z, depth = tree depth then name."}}}
// @applet-output-schema id:integer;name:string;depth:integer;articles:integer;children:integer
// @applet-description "Category inventory — every category with its article count and child count."
//

View File

@@ -0,0 +1,609 @@
// MARK: - Imports
// #region Imports
import JSZip from 'jszip';
import { getStore, getProvider } from '../db/store.js';
import { createLogger } from '../util/logger.js';
// #endregion
const log = createLogger('export');
const UTF8_BOM = '\uFEFF';
// MARK: - CSV Helpers
// #region CSV Helpers
/**
* Formats a value as a double-quoted CSV field.
*
* @param {any} val
* @returns {string}
*/
function formatCsvField(val) {
if (val == null) return '""';
const str = String(val);
return `"${str.replace(/"/g, '""')}"`;
}
/**
* Formats a value as an Excel string formula (e.g. ="4270001588593") so that
* spreadsheet tools treat it strictly as text rather than converting to a number / scientific notation.
* Empty or null values return '""'.
*
* @param {any} val
* @returns {string}
*/
function formatCsvStringFormula(val) {
if (val == null) return '""';
const str = String(val).trim();
if (str === '') return '""';
return `="${str.replace(/"/g, '""')}"`;
}
/**
* Formats a number in German decimal format (e.g. 8.36 -> "8,36").
*
* @param {number|string|null} val
* @param {string} [fallback='0']
* @returns {string}
*/
function formatGermanFloat(val, fallback = '0') {
if (val == null || val === '') return fallback;
const num = Number(val);
if (Number.isNaN(num)) return fallback;
return String(num).replace('.', ',');
}
/**
* Extracts Pfand deposit amount from article options if present.
*
* @param {Array<object>} options
* @returns {string} e.g. "pfand:0.25" or ""
*/
function extractPfandAttribute(options) {
if (!Array.isArray(options) || options.length === 0) return '';
for (const opt of options) {
const sku = String(opt.sku || '').toUpperCase();
const match = sku.match(/PFAND[-_]?([0-9]+(?:[.,][0-9]+)?)/i);
if (match) {
const val = parseFloat(match[1].replace(',', '.'));
if (Number.isFinite(val) && val > 0) {
return `pfand:${val.toFixed(2)}`;
}
}
if (opt.name) {
const nameMatch = String(opt.name).match(/pfand\s*([0-9]+(?:[.,][0-9]+)?)/i);
if (nameMatch) {
const val = parseFloat(nameMatch[1].replace(',', '.'));
if (Number.isFinite(val) && val > 0) {
return `pfand:${val.toFixed(2)}`;
}
}
}
}
return '';
}
// #endregion
// MARK: - Concurrency Pool
// #region Concurrency Pool
/**
* Runs tasks with a controlled concurrency limit.
*
* @template T, R
* @param {Array<T>} items
* @param {number} concurrency
* @param {(item: T, index: number) => Promise<R>} fn
* @returns {Promise<Array<R>>}
*/
async function asyncPool(items, concurrency, fn) {
const results = new Array(items.length);
let index = 0;
const workers = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
while (index < items.length) {
const current = index++;
results[current] = await fn(items[current], current);
}
});
await Promise.all(workers);
return results;
}
// #endregion
// MARK: - Company Export Orchestrator
// #region Company Export Orchestrator
/**
* Executes full catalog export from store into a round-trip compatible ZIP buffer.
*
* @param {object} opts
* @param {number} opts.googleAuthId
* @param {number} opts.companyId
* @param {function} [opts.onStatus] - (statusObj) => void
* @param {function} [opts.onProgress] - (counts) => void
* @returns {Promise<{ zipBuffer: Buffer, stats: object, filename: string }>}
*/
export async function executeCompanyExport({
googleAuthId,
companyId,
onStatus = () => {},
onProgress = () => {},
}) {
const store = await getStore();
const zip = new JSZip();
// 1. Categories
onStatus({ message: 'Lade Kategorien...' });
const treeResult = await store.category.getTreeForCompany(googleAuthId);
const categoryTree = treeResult.tree || [];
onProgress({
stage: 'categories',
categoriestotal: categoryTree.length,
categoriesdone: categoryTree.length,
percent: 10,
});
// 2. Brands
onStatus({ message: 'Lade Hersteller / Marken...' });
const brands = [];
let brandOffset = 0;
const brandLimit = 100;
while (true) {
const brandListRes = await store.brand.list(googleAuthId, { offset: brandOffset, limit: brandLimit });
const batch = brandListRes.brands || [];
brands.push(...batch);
brandOffset += batch.length;
if (batch.length < brandLimit || brandOffset >= (brandListRes.total ?? 0)) {
break;
}
}
const brandById = new Map();
const brandByName = new Map();
for (const b of brands) {
brandById.set(b.id, b);
if (b.name) brandByName.set(b.name.trim().toLowerCase(), b);
}
onProgress({
stage: 'brands',
brandstotal: brands.length,
brandsdone: brands.length,
percent: 15,
});
// 3. Articles
onStatus({ message: 'Lade Artikelliste...' });
const exportArticles = await store.article.listExport(googleAuthId, null);
const totalArticles = exportArticles.length;
onProgress({
stage: 'articles',
articlestotal: totalArticles,
articlesdone: 0,
percent: 20,
});
onStatus({ message: `Lade Detaildaten für ${totalArticles} Artikel...` });
let articlesProcessed = 0;
const fullArticles = await asyncPool(exportArticles, 5, async (item) => {
try {
const detail = await store.article.getBySku(googleAuthId, item.sku);
articlesProcessed++;
if (articlesProcessed % 50 === 0 || articlesProcessed === totalArticles) {
onProgress({
stage: 'articles',
articlestotal: totalArticles,
articlesdone: articlesProcessed,
percent: 20 + Math.floor((articlesProcessed / totalArticles) * 40),
});
}
return detail.article;
} catch (err) {
log.warn(`Could not fetch details for article ${item.sku}:`, err.message);
articlesProcessed++;
return {
id: item.id,
sku: item.sku,
name: item.name,
net: item.net,
taxRate: item.taxRate,
track_stock: item.track_stock,
eans: item.eans || [],
categoryIds: item.categoryIds || [],
photos: (item.fullHashes || []).map((h) => ({ fullHash: h })),
brand: item.brandName ? { name: item.brandName } : null,
};
}
});
// Build parent-child lookup for variant linking
const articleBySku = new Map();
const articleById = new Map();
for (const a of fullArticles) {
if (a.sku) articleBySku.set(a.sku, a);
if (a.id) articleById.set(a.id, a);
}
// Populate attributeValue for children from parent's children array if needed
for (const a of fullArticles) {
if (a.isParent && Array.isArray(a.children)) {
for (const childRef of a.children) {
const childObj = articleBySku.get(childRef.sku) || articleById.get(childRef.id);
if (childObj && !childObj.attributeValue && childRef.attributeValue) {
childObj.attributeValue = childRef.attributeValue;
}
}
}
}
// 4. Collect Images
onStatus({ message: 'Sammle Bildreferenzen...' });
const uniqueHashes = new Set();
// Category pictures
for (const c of categoryTree) {
if (c.fullHash) uniqueHashes.add(c.fullHash);
}
// Brand logos
for (const b of brands) {
if (b.pics?.[0]?.fullHash) {
uniqueHashes.add(b.pics[0].fullHash);
}
}
// Article pictures
for (const a of fullArticles) {
for (const p of a.photos || []) {
if (p.fullHash) uniqueHashes.add(p.fullHash);
}
if (a.brand?.fullHash) {
uniqueHashes.add(a.brand.fullHash);
}
}
const hashList = Array.from(uniqueHashes);
const totalImages = hashList.length;
onProgress({
stage: 'images',
imagestotal: totalImages,
imagesdone: 0,
percent: 60,
});
onStatus({ message: `Lade ${totalImages} Bilddateien...` });
const hashToBuffer = new Map();
let imagesProcessed = 0;
await asyncPool(hashList, 5, async (fullHash) => {
try {
const pic = await store.pic.getImageByHash(googleAuthId, fullHash);
if (pic?.buffer) {
hashToBuffer.set(fullHash, pic.buffer);
}
} catch (err) {
log.warn(`Could not load image hash ${fullHash}:`, err.message);
}
imagesProcessed++;
if (imagesProcessed % 25 === 0 || imagesProcessed === totalImages) {
onProgress({
stage: 'images',
imagestotal: totalImages,
imagesdone: imagesProcessed,
percent: 60 + Math.floor((imagesProcessed / Math.max(totalImages, 1)) * 25),
});
}
});
// 5. Build categories.csv & place category images in ZIP
onStatus({ message: 'Erstelle categories.csv...' });
const categoryCsvRows = [
['"id"', '"parentid"', '"sort"', '"name"', '"pictureFiles"'].join(';'),
];
for (const c of categoryTree) {
let picRel = '';
if (c.fullHash && hashToBuffer.has(c.fullHash)) {
picRel = `pictures/categories/${c.id}/${c.fullHash}.avif`;
zip.file(picRel, hashToBuffer.get(c.fullHash));
}
const catName = c.name || (c.parentId == null ? 'Haupt' : `Kategorie ${c.id}`);
const row = [
formatCsvField(c.id),
formatCsvField(c.parentId == null ? '0' : String(c.parentId)),
formatCsvField(c.sort ?? 0),
formatCsvField(catName),
formatCsvField(picRel),
];
categoryCsvRows.push(row.join(';'));
}
zip.file('categories.csv', UTF8_BOM + categoryCsvRows.join('\r\n'));
// 6. Place brand images in ZIP
for (const b of brands) {
const hash = b.pics?.[0]?.fullHash;
if (hash && hashToBuffer.has(hash)) {
const rel = `pictures/brands/${b.id}/${hash}.avif`;
zip.file(rel, hashToBuffer.get(hash));
}
}
// 7. Build articles.csv & place article images in ZIP
onStatus({ message: 'Erstelle articles.csv...' });
const articleCsvHeaders = [
'"id"',
'"sku"',
'"name"',
'"net"',
'"taxrate"',
'"active"',
'"stock"',
'"ean"',
'"weight"',
'"track_strock"',
'"created_at"',
'"sort"',
'"variation"',
'"parentid"',
'"is_parent"',
'"brand"',
'"categories"',
'"attributes"',
'"brandPictureFiles"',
'"pictureFiles"',
'"desc"',
];
const articleCsvRows = [articleCsvHeaders.join(';')];
for (const a of fullArticles) {
// Collect article pictures
const picPaths = [];
for (const p of a.photos || []) {
if (p.fullHash && hashToBuffer.has(p.fullHash)) {
const rel = `pictures/articles/${a.id}/${p.fullHash}.avif`;
zip.file(rel, hashToBuffer.get(p.fullHash));
picPaths.push(rel);
}
}
// Brand image path
let brandPicRel = '';
const brandObj = a.brand?.id
? brandById.get(a.brand.id)
: a.brand?.name
? brandByName.get(a.brand.name.trim().toLowerCase())
: null;
const brandHash = brandObj?.pics?.[0]?.fullHash || a.brand?.fullHash;
if (brandHash && hashToBuffer.has(brandHash) && (brandObj?.id || a.brand?.id)) {
const bId = brandObj?.id || a.brand.id;
brandPicRel = `pictures/brands/${bId}/${brandHash}.avif`;
}
// Price & Tax
const netPrice = a.prices?.[0]?.net ?? a.net ?? 0;
const taxRate = a.prices?.[0]?.taxRate ?? a.taxRate ?? 19;
// Parent / Variant fields
const isParent = Boolean(a.isParent);
let parentId = '0';
let variation = '';
if (isParent) {
variation = a.parentAttributeName || 'Variante';
} else if (a.parentArticleId != null || a.parentSku != null) {
const parentObj = a.parentArticleId
? articleById.get(a.parentArticleId)
: articleBySku.get(a.parentSku);
parentId = String(parentObj?.id ?? a.parentArticleId ?? 0);
variation = a.attributeValue || 'Standard';
}
// Pfand attribute
const pfandAttr = extractPfandAttribute(a.options);
// Barcode / EAN
const ean = Array.isArray(a.eans) && a.eans.length > 0 ? a.eans[0] : '';
// Categories
const catStr = Array.isArray(a.categoryIds) ? a.categoryIds.join(';') : '';
const row = [
formatCsvField(a.id),
formatCsvStringFormula(a.sku),
formatCsvField(a.name ?? ''),
formatCsvField(formatGermanFloat(netPrice)),
formatCsvField(formatGermanFloat(taxRate, '19')),
formatCsvField('1'), // active
formatCsvField(a.stock ?? 0),
formatCsvStringFormula(ean),
formatCsvField(formatGermanFloat(a.weight ?? 0)),
formatCsvField(a.track_stock ? '1' : '0'),
formatCsvField(a.createdAt || new Date().toISOString()),
formatCsvField(a.sort ?? 0),
formatCsvField(variation),
formatCsvField(parentId),
formatCsvField(isParent ? '1' : '0'),
formatCsvField(a.brand?.name || ''),
formatCsvField(catStr),
formatCsvField(pfandAttr),
formatCsvField(brandPicRel),
formatCsvField(picPaths.join(';')),
formatCsvField(a.desc || ''),
];
articleCsvRows.push(row.join(';'));
}
zip.file('articles.csv', UTF8_BOM + articleCsvRows.join('\r\n'));
// 8. Orders / Manifests (if present)
let exportedReceiptsCount = 0;
try {
const orderListRes = await store.order.list(googleAuthId, { offset: 0, limit: 1 });
const totalOrders = orderListRes?.total ?? (Array.isArray(orderListRes) ? orderListRes.length : 0);
if (totalOrders > 0) {
onStatus({ message: `Exportiere ${totalOrders} Aufträge / Belege...` });
// Prefetch payment types and shipping types by manifest ID
const paymentMap = new Map();
const shippingMap = new Map();
try {
const provider = await getProvider();
const isSqlite = provider?.dialect === 'sqlite3';
const typeCol = isSqlite ? '"type"' : '[type]';
const [paymentsRes, shippingsRes] = await Promise.all([
provider.query(`
SELECT p.manifest_id AS manifestId, p.${typeCol} AS type
FROM payment p
INNER JOIN manifest m ON m.id = p.manifest_id
WHERE m.company_id = @companyId
ORDER BY p.id ASC
`, { companyId }),
provider.query(`
SELECT s.manifest_id AS manifestId, s.${typeCol} AS type
FROM shipping s
INNER JOIN manifest m ON m.id = s.manifest_id
WHERE m.company_id = @companyId
ORDER BY s.id ASC
`, { companyId }),
]);
for (const row of paymentsRes.rows || []) {
if (!paymentMap.has(row.manifestId)) {
paymentMap.set(row.manifestId, row.type);
}
}
for (const row of shippingsRes.rows || []) {
if (!shippingMap.has(row.manifestId)) {
shippingMap.set(row.manifestId, row.type);
}
}
} catch (err) {
log.warn('Could not prefetch payment/shipping types:', err.message);
}
const manifestCsvHeaders = [
'"id"',
'"nr"',
'"extnr"',
'"created_at"',
'"paymentType"',
'"shippingType"',
'"payed"',
'"shipped"',
];
const manifestItemCsvHeaders = [
'"id"',
'"manifestid"',
'"articleid"',
'"name"',
'"amount"',
'"amountShipped"',
'"net"',
'"taxrate"',
'"sort"',
];
const manifestCsvRows = [manifestCsvHeaders.join(';')];
const manifestItemCsvRows = [manifestItemCsvHeaders.join(';')];
let orderOffset = 0;
const orderLimit = 200;
let itemSeq = 0;
while (true) {
const batchRes = await store.order.list(googleAuthId, { offset: orderOffset, limit: orderLimit });
const batch = Array.isArray(batchRes) ? batchRes : (batchRes?.orders || []);
if (batch.length === 0) break;
for (const ord of batch) {
const payType = paymentMap.get(ord.id) || ord.payment?.type || ord.payments?.[0]?.type || 'Bar';
const shipType = shippingMap.get(ord.id) || ord.shipping?.type || ord.shippings?.[0]?.type || 'Selbstabholer';
manifestCsvRows.push([
formatCsvField(ord.id),
formatCsvField(ord.number),
formatCsvField(ord.externalNumber || ''),
formatCsvField(ord.createdAt || ord.date || new Date().toISOString()),
formatCsvField(payType),
formatCsvField(shipType),
formatCsvField(ord.isPaid || ord.paid ? '1' : '0'),
formatCsvField(ord.isShipped || ord.shipped ? '1' : '0'),
].join(';'));
for (const line of ord.lines || []) {
const artId = (line.sku && articleBySku.get(line.sku)?.id) || line.articleId || '';
manifestItemCsvRows.push([
formatCsvField(line.id || ++itemSeq),
formatCsvField(ord.id),
formatCsvField(artId),
formatCsvField(line.name || ''),
formatCsvField(line.amount ?? 1),
formatCsvField(ord.isShipped || ord.shipped ? (line.amount ?? 1) : 0),
formatCsvField(formatGermanFloat(line.net ?? 0)),
formatCsvField(formatGermanFloat(line.taxrate ?? 19)),
formatCsvField(line.sort ?? 0),
].join(';'));
}
}
orderOffset += batch.length;
if (batch.length < orderLimit || orderOffset >= totalOrders) {
break;
}
}
if (manifestCsvRows.length > 1) {
zip.file('manifests.csv', UTF8_BOM + manifestCsvRows.join('\r\n'));
zip.file('manifestitems.csv', UTF8_BOM + manifestItemCsvRows.join('\r\n'));
exportedReceiptsCount = manifestCsvRows.length - 1;
}
}
} catch (orderErr) {
log.warn('Could not export orders / manifests:', orderErr.message);
}
// 9. Generate ZIP buffer
onStatus({ message: 'Erstelle ZIP-Archiv...' });
onProgress({
stage: 'packaging',
percent: 92,
});
const zipBuffer = await zip.generateAsync({
type: 'nodebuffer',
compression: 'DEFLATE',
compressionOptions: { level: 6 },
});
onProgress({
stage: 'packaging',
percent: 100,
});
const filename = `company-${companyId}-export.zip`;
const stats = {
categories: categoryTree.length,
brands: brands.length,
articles: fullArticles.length,
images: hashToBuffer.size,
orders: exportedReceiptsCount,
zipSize: zipBuffer.length,
};
log.info(`Company export complete: ${filename} (${zipBuffer.length} bytes, ${stats.articles} articles, ${stats.images} images)`);
return {
zipBuffer,
stats,
filename,
};
}
// #endregion

802
src/services/manifestPdf.js Normal file
View File

@@ -0,0 +1,802 @@
import PDFDocument from 'pdfkit';
import { getStore } from '../db/store.js';
import * as dbOrder from '../db/order.js';
import { emit } from '../api/sse.js';
/**
* Formats a Date or date string to German format DD.MM.YYYY.
* @param {Date|string} dateVal
* @returns {string}
*/
export function formatDate(dateVal) {
if (!dateVal) return '';
const d = new Date(dateVal);
if (Number.isNaN(d.getTime())) return String(dateVal);
const day = String(d.getDate()).padStart(2, '0');
const month = String(d.getMonth() + 1).padStart(2, '0');
const year = d.getFullYear();
return `${day}.${month}.${year}`;
}
/**
* Formats a number as German currency string (e.g. 1.234,50 €).
* @param {number} val
* @returns {string}
*/
export function formatCurrency(val) {
const n = Number(val) || 0;
return (
n.toLocaleString('de-DE', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}) + ' €'
);
}
/**
* Formats a number with up to 3 decimal places (e.g. 1 or 2,5).
* @param {number} val
* @returns {string}
*/
export function formatQuantity(val) {
const n = Number(val) || 0;
return n.toLocaleString('de-DE', {
minimumFractionDigits: 0,
maximumFractionDigits: 3,
});
}
/**
* Configuration per manifest type.
*/
const TYPE_CONFIG = {
quote: {
docTitle: 'ANGEBOT',
numberPrefix: 'Angebot-Nr.',
showPrices: true,
filenamePrefix: 'angebot',
introText: 'Wir freuen uns über Ihr Interesse und bieten Ihnen freibleibend wie folgt an:',
closingText: '',
defaultSubject: 'Angebot',
},
order: {
docTitle: 'AUFTRAG',
numberPrefix: 'Auftrags-Nr.',
showPrices: true,
filenamePrefix: 'auftrag',
introText: 'Wir bestätigen dankend die Annahme Ihres Auftrags zu folgenden Positionen:',
closingText: 'Vielen Dank für Ihren Auftrag!',
defaultSubject: 'Auftragsbestätigung',
},
shipment: {
docTitle: 'LIEFERSCHEIN',
numberPrefix: 'Lieferschein-Nr.',
showPrices: false,
filenamePrefix: 'lieferschein',
introText: 'Mit dieser Sendung liefern wir Ihnen folgende Artikel:',
closingText: 'Die Ware bleibt bis zur vollständigen Bezahlung unser Eigentum. Bitte prüfen Sie die Sendung sofort auf Vollständigkeit und eventuelle Transportschäden.',
defaultSubject: 'Lieferschein',
},
delivery: {
docTitle: 'WARENEINGANG',
numberPrefix: 'Beleg-Nr.',
showPrices: false,
filenamePrefix: 'wareneingang',
introText: 'Wareneingangsdokumentation zu folgenden Positionen:',
closingText: 'Wareneingang ordnungsgemäß im Lager erfasst.',
defaultSubject: 'Wareneingang',
},
};
/**
* Generates an A4 PDF document Buffer for any manifest document type.
*
* @param {object} params
* @param {'quote'|'order'|'shipment'|'delivery'} [params.type='order'] - Manifest document type.
* @param {object} params.manifest - Manifest document object (order, quote, shipment, or delivery).
* @param {object} [params.company] - Company details (name, etc.).
* @param {Array<object>} [params.settings] - Company settings list [{ key, value }].
* @param {object} [params.partner] - Entity (customer or supplier).
* @param {Array<object>} [params.addresses] - Entity addresses.
* @returns {Promise<Buffer>}
*/
export function generateManifestPdf({
type = 'order',
manifest,
company = {},
settings = [],
partner = null,
addresses = [],
}) {
const config = TYPE_CONFIG[type] || TYPE_CONFIG.order;
return new Promise((resolve, reject) => {
try {
const doc = new PDFDocument({
size: 'A4',
margins: {
top: 45,
bottom: 25,
left: 45,
right: 45,
},
bufferPages: true,
info: {
Title: `${config.docTitle} ${manifest.number || manifest.id}`,
Author: company.name || 'Dash',
Subject: `${config.defaultSubject} #${manifest.number || manifest.id}`,
CreationDate: new Date(),
},
});
const chunks = [];
doc.on('data', (chunk) => chunks.push(chunk));
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', reject);
// Palette
const primaryColor = '#0f172a'; // slate-900
const secondaryColor = '#2563eb'; // blue-600
const textColor = '#1e293b'; // slate-800
const mutedColor = '#64748b'; // slate-500
const lightBorder = '#e2e8f0'; // slate-200
const lightBg = '#f8fafc'; // slate-50
// Helper to lookup settings
const settingsMap = new Map();
if (Array.isArray(settings)) {
for (const s of settings) {
if (s?.key) settingsMap.set(s.key, s.value);
}
}
const companyName = company?.name || settingsMap.get('company.name') || 'Firma';
const companyStreet = settingsMap.get('company.street') || '';
const companyZipCity = [settingsMap.get('company.zip'), settingsMap.get('company.city')].filter(Boolean).join(' ');
const companyEmail = settingsMap.get('company.email') || '';
const companyPhone = settingsMap.get('company.phone') || '';
const companyTaxId = settingsMap.get('company.taxId') || settingsMap.get('company.vatId') || '';
// Selected Address (for shipment prefer shipping address, for others prefer billing, fallback to first)
let chosenAddress = null;
if (Array.isArray(addresses) && addresses.length > 0) {
if (type === 'shipment') {
chosenAddress = addresses.find((a) => a.addressType === 'shipping') || addresses[0];
} else {
chosenAddress = addresses.find((a) => a.addressType === 'billing') || addresses[0];
}
}
const partnerName = partner
? [partner.firstName, partner.name].filter(Boolean).join(' ')
: '';
// ----------------------------------------------------
// 1. Header (Top bar)
// ----------------------------------------------------
let y = 45;
// Company Title (Left)
doc.font('Helvetica-Bold')
.fontSize(16)
.fillColor(primaryColor)
.text(companyName, 45, y);
// Document Title (Right)
doc.font('Helvetica-Bold')
.fontSize(20)
.fillColor(secondaryColor)
.text(config.docTitle, 350, y, { align: 'right', width: 200 });
y += 24;
// Company subline
const companyAddressLine = [companyStreet, companyZipCity].filter(Boolean).join(' • ');
if (companyAddressLine) {
doc.font('Helvetica')
.fontSize(8)
.fillColor(mutedColor)
.text(companyAddressLine, 45, y);
}
// Document Number under Document Title
const headerNumStr = `${config.numberPrefix} ${manifest.number || manifest.id}`;
let headerNumFontSize = 11;
const headerNumWidth = 250;
doc.font('Helvetica-Bold');
while (headerNumFontSize > 8 && doc.fontSize(headerNumFontSize).widthOfString(headerNumStr) > headerNumWidth) {
headerNumFontSize -= 0.5;
}
doc.fontSize(headerNumFontSize)
.fillColor(primaryColor)
.text(headerNumStr, 300, y, { align: 'right', width: headerNumWidth, lineBreak: false });
y += 18;
// Separator rule
doc.moveTo(45, y)
.lineTo(550, y)
.strokeColor(lightBorder)
.lineWidth(1)
.stroke();
y += 20;
// ----------------------------------------------------
// 2. Partner Address & Meta Box
// ----------------------------------------------------
const addressBlockTop = y;
// Small return-address header line (DIN 5008 style)
if (companyName || companyZipCity) {
doc.font('Helvetica')
.fontSize(7)
.fillColor(mutedColor)
.text(`${companyName} ${companyAddressLine ? '• ' + companyAddressLine : ''}`, 45, y);
y += 14;
}
// Partner Address
const partnerLabel = type === 'delivery' ? 'Lieferant' : (type === 'shipment' ? 'Lieferadresse' : 'Rechnungsadresse');
if (partnerName || chosenAddress) {
doc.font('Helvetica-Bold')
.fontSize(10)
.fillColor(textColor);
const displayName = chosenAddress?.name
? [chosenAddress.firstName, chosenAddress.name].filter(Boolean).join(' ')
: partnerName;
if (displayName) {
doc.text(displayName, 45, y);
y += 14;
}
doc.font('Helvetica')
.fontSize(9)
.fillColor(textColor);
if (chosenAddress?.street) {
const streetLine = [chosenAddress.street, chosenAddress.streetNumber].filter(Boolean).join(' ');
doc.text(streetLine, 45, y);
y += 13;
}
if (chosenAddress?.zip || chosenAddress?.city) {
const zipCity = [chosenAddress.zip, chosenAddress.city].filter(Boolean).join(' ');
doc.text(zipCity, 45, y);
y += 13;
}
if (chosenAddress?.county) {
doc.text(chosenAddress.county, 45, y);
y += 13;
}
}
// Right side Meta Box
const metaBoxX = 330;
const metaBoxWidth = 220;
const metaBoxY = addressBlockTop;
const metaBoxHeight = 85;
doc.roundedRect(metaBoxX, metaBoxY, metaBoxWidth, metaBoxHeight, 4)
.fillColor(lightBg)
.fill()
.strokeColor(lightBorder)
.lineWidth(0.75)
.stroke();
let metaY = metaBoxY + 10;
const drawMetaRow = (label, val) => {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(mutedColor)
.text(label, metaBoxX + 10, metaY, { width: 72, lineBreak: false });
const valText = val || '–';
doc.font('Helvetica-Bold');
let valFontSize = 8.5;
const maxValWidth = 128;
while (valFontSize > 6.5 && doc.fontSize(valFontSize).widthOfString(valText) > maxValWidth) {
valFontSize -= 0.5;
}
doc.fontSize(valFontSize)
.fillColor(textColor)
.text(valText, metaBoxX + 82, metaY, { width: maxValWidth, align: 'right', lineBreak: false });
metaY += 15;
};
const numLabel = config.numberPrefix ? (config.numberPrefix.endsWith(':') ? config.numberPrefix : `${config.numberPrefix}:`) : 'Beleg-Nr.:';
drawMetaRow(numLabel, String(manifest.number || manifest.id));
const dateLabel = type === 'shipment' ? 'Versanddatum:' : (type === 'delivery' ? 'Eingangsdatum:' : 'Datum:');
drawMetaRow(dateLabel, formatDate(manifest.date || manifest.createdAt));
if (manifest.externalNumber) {
drawMetaRow('Externe Referenz:', String(manifest.externalNumber));
}
if (partner?.number) {
const numLabel = type === 'delivery' ? 'Lieferantennr.:' : 'Kundennummer:';
drawMetaRow(numLabel, String(partner.number));
}
// Ensure y is past the address & meta blocks
y = Math.max(y + 15, metaBoxY + metaBoxHeight + 15);
// If shipment has package tracking info, show a compact card
if (type === 'shipment' && Array.isArray(manifest.shippings) && manifest.shippings.length > 0) {
const pkgBoxHeight = 16 + manifest.shippings.length * 13;
doc.roundedRect(45, y, 505, pkgBoxHeight, 4)
.fillColor(lightBg)
.fill()
.strokeColor(lightBorder)
.lineWidth(0.5)
.stroke();
doc.font('Helvetica-Bold').fontSize(8.5).fillColor(primaryColor).text('Pakete & Sendungsverfolgung:', 55, y + 6);
let pkgY = y + 20;
manifest.shippings.forEach((pkg, pIdx) => {
const pkgText = `Paket ${pIdx + 1}: ${pkg.carrier || 'Paketdienst'} ${pkg.number ? '#' + pkg.number : ''} ${pkg.trackingCode ? '• Tracking: ' + pkg.trackingCode : ''} (${pkg.state || 'gepackt'})`;
doc.font('Helvetica').fontSize(8).fillColor(textColor).text(pkgText, 55, pkgY, { width: 485, ellipsis: true });
pkgY += 13;
});
y += pkgBoxHeight + 15;
}
// Intro text
doc.font('Helvetica')
.fontSize(9)
.fillColor(textColor)
.text(config.introText, 45, y);
y += 18;
// ----------------------------------------------------
// 3. Line Items Table
// ----------------------------------------------------
const showPrices = config.showPrices;
// Column setups
const colXPrices = { pos: 45, sku: 70, name: 150, amount: 335, gross: 385, tax: 450, total: 482 };
const colWidthPrices = { pos: 22, sku: 75, name: 180, amount: 45, gross: 60, tax: 30, total: 65 };
const colXNoPrices = { pos: 45, sku: 80, name: 185, amount: 455 };
const colWidthNoPrices = { pos: 30, sku: 100, name: 265, amount: 92 };
const drawTableHeader = (atY) => {
doc.rect(45, atY, 505, 20)
.fillColor(primaryColor)
.fill();
doc.font('Helvetica-Bold')
.fontSize(8)
.fillColor('#ffffff');
if (showPrices) {
doc.text('Pos', colXPrices.pos + 3, atY + 6, { width: colWidthPrices.pos });
doc.text('Art.-Nr.', colXPrices.sku, atY + 6, { width: colWidthPrices.sku });
doc.text('Bezeichnung', colXPrices.name, atY + 6, { width: colWidthPrices.name });
doc.text('Menge', colXPrices.amount, atY + 6, { width: colWidthPrices.amount, align: 'right' });
doc.text('E-Preis Brutto', colXPrices.gross, atY + 6, { width: colWidthPrices.gross, align: 'right' });
doc.text('MwSt', colXPrices.tax, atY + 6, { width: colWidthPrices.tax, align: 'right' });
doc.text('Gesamt Brutto', colXPrices.total, atY + 6, { width: colWidthPrices.total, align: 'right' });
} else {
doc.text('Pos', colXNoPrices.pos + 3, atY + 6, { width: colWidthNoPrices.pos });
doc.text('Art.-Nr.', colXNoPrices.sku, atY + 6, { width: colWidthNoPrices.sku });
doc.text('Bezeichnung', colXNoPrices.name, atY + 6, { width: colWidthNoPrices.name });
doc.text('Liefermenge', colXNoPrices.amount, atY + 6, { width: colWidthNoPrices.amount, align: 'right' });
}
return atY + 22;
};
y = drawTableHeader(y);
const lines = Array.isArray(manifest.lines) ? manifest.lines : [];
let totalNet = 0;
let totalQty = 0;
const taxBreakdown = new Map();
lines.forEach((line, index) => {
const amount = Number(line.amount || 0);
const net = Number(line.net || 0);
const rebate = Number(line.rebate || 0);
const taxrate = Number(line.taxrate || 0);
const taxFactor = 1 + taxrate / 100;
totalQty += amount;
let lineNet = 0;
let lineGross = 0;
let unitGross = net * taxFactor;
let unitNetAfterRebate = net;
let unitGrossAfterRebate = unitGross;
if (showPrices) {
unitNetAfterRebate = net * (1 - rebate / 100);
unitGrossAfterRebate = unitNetAfterRebate * taxFactor;
lineNet = amount * unitNetAfterRebate;
const lineTax = lineNet * (taxrate / 100);
lineGross = lineNet + lineTax;
totalNet += lineNet;
const currTax = taxBreakdown.get(taxrate) || { net: 0, tax: 0 };
currTax.net += lineNet;
currTax.tax += lineTax;
taxBreakdown.set(taxrate, currTax);
}
const nameWidth = showPrices ? colWidthPrices.name : colWidthNoPrices.name;
const puSuffix = line.pu && line.pu > 1 ? ` (VPE ${line.pu})` : '';
const nameText = (line.name || 'Position') + puSuffix;
// Measure actual rendered text heights to prevent overlaps
doc.font('Helvetica').fontSize(8.5);
const nameHeight = doc.heightOfString(nameText, { width: nameWidth });
const hasRebate = showPrices && rebate > 0;
const rebateText = hasRebate ? `Rabatt: ${rebate}% (effektiver E-Preis: ${formatCurrency(unitGrossAfterRebate)})` : '';
let rebateHeight = 0;
if (hasRebate) {
doc.font('Helvetica').fontSize(7.5);
rebateHeight = doc.heightOfString(rebateText, { width: nameWidth }) + 2;
}
const hasComment = Boolean(line.comment);
let commentHeight = 0;
if (hasComment) {
doc.font('Helvetica-Oblique').fontSize(7.5);
commentHeight = doc.heightOfString(line.comment, { width: nameWidth }) + 2;
}
const contentHeight = nameHeight + rebateHeight + commentHeight;
const rowHeight = Math.max(18, Math.ceil(contentHeight + 6));
// Check page overflow
if (y + rowHeight > 730) {
doc.addPage();
y = drawTableHeader(45);
}
// Zebra striping
if (index % 2 === 1) {
doc.rect(45, y - 2, 505, rowHeight)
.fillColor(lightBg)
.fill();
}
const textY = y + 1;
if (showPrices) {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor);
// Pos
doc.text(String(index + 1), colXPrices.pos + 3, textY, { width: colWidthPrices.pos });
// SKU
doc.text(line.sku || '–', colXPrices.sku, textY, { width: colWidthPrices.sku, ellipsis: true });
// Name (wraps naturally without clipping)
doc.text(nameText, colXPrices.name, textY, { width: nameWidth });
// Amount
doc.text(formatQuantity(amount), colXPrices.amount, textY, {
width: colWidthPrices.amount,
align: 'right',
});
// Unit Price Gross
doc.text(formatCurrency(unitGross), colXPrices.gross, textY, {
width: colWidthPrices.gross,
align: 'right',
});
// Tax Rate
doc.text(`${taxrate}%`, colXPrices.tax, textY, {
width: colWidthPrices.tax,
align: 'right',
});
// Line Total Gross
doc.font('Helvetica-Bold')
.text(formatCurrency(lineGross), colXPrices.total, textY, {
width: colWidthPrices.total,
align: 'right',
});
let subY = textY + nameHeight + 2;
if (hasRebate) {
doc.font('Helvetica')
.fontSize(7.5)
.fillColor(secondaryColor)
.text(rebateText, colXPrices.name, subY, {
width: nameWidth,
});
subY += rebateHeight;
}
if (hasComment) {
doc.font('Helvetica-Oblique')
.fontSize(7.5)
.fillColor(mutedColor)
.text(line.comment, colXPrices.name, subY, {
width: nameWidth,
});
}
} else {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor);
// Without Prices
doc.text(String(index + 1), colXNoPrices.pos + 3, textY, { width: colWidthNoPrices.pos });
doc.text(line.sku || '–', colXNoPrices.sku, textY, { width: colWidthNoPrices.sku, ellipsis: true });
doc.text(nameText, colXNoPrices.name, textY, { width: nameWidth });
doc.font('Helvetica-Bold')
.text(formatQuantity(amount) + ' Stk', colXNoPrices.amount, textY, {
width: colWidthNoPrices.amount,
align: 'right',
});
if (hasComment) {
doc.font('Helvetica-Oblique')
.fontSize(7.5)
.fillColor(mutedColor)
.text(line.comment, colXNoPrices.name, textY + nameHeight + 2, {
width: nameWidth,
});
}
}
// Bottom row separator
doc.moveTo(45, y + rowHeight - 2)
.lineTo(550, y + rowHeight - 2)
.strokeColor(lightBorder)
.lineWidth(0.5)
.stroke();
y += rowHeight;
});
// ----------------------------------------------------
// 4. Summary & Totals Block
// ----------------------------------------------------
if (showPrices) {
let totalTax = 0;
for (const item of taxBreakdown.values()) {
totalTax += item.tax;
}
const grandTotal = totalNet + totalTax;
if (y + 110 > 750) {
doc.addPage();
y = 50;
} else {
y += 15;
}
const summaryX = 330;
const summaryWidth = 220;
// Subtotal Net
doc.font('Helvetica')
.fontSize(9)
.fillColor(mutedColor)
.text('Summe Netto:', summaryX, y, { width: 110 });
doc.font('Helvetica-Bold')
.fontSize(9)
.fillColor(textColor)
.text(formatCurrency(totalNet), summaryX + 110, y, { width: 110, align: 'right' });
y += 15;
// Tax Breakdown
const sortedRates = Array.from(taxBreakdown.entries()).sort((a, b) => a[0] - b[0]);
for (const [rate, val] of sortedRates) {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(mutedColor)
.text(`zzgl. ${rate}% MwSt:`, summaryX, y, { width: 110 });
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor)
.text(formatCurrency(val.tax), summaryX + 110, y, { width: 110, align: 'right' });
y += 14;
}
// Divider before Grand Total
doc.moveTo(summaryX, y + 2)
.lineTo(summaryX + summaryWidth, y + 2)
.strokeColor(lightBorder)
.lineWidth(1)
.stroke();
y += 8;
// Grand Total
doc.font('Helvetica-Bold')
.fontSize(12)
.fillColor(primaryColor)
.text('Gesamtsumme:', summaryX, y, { width: 110 });
doc.font('Helvetica-Bold')
.fontSize(12)
.fillColor(secondaryColor)
.text(formatCurrency(grandTotal), summaryX + 110, y, { width: 110, align: 'right' });
y += 24;
// Payments / open balance note (for orders)
if (manifest.paidTotal !== undefined && manifest.paidTotal > 0) {
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(mutedColor)
.text('Bereits bezahlt:', summaryX, y, { width: 110 });
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(textColor)
.text(formatCurrency(manifest.paidTotal), summaryX + 110, y, { width: 110, align: 'right' });
y += 14;
const openAmount = Math.max(0, grandTotal - Number(manifest.paidTotal || 0));
if (openAmount > 0.01) {
doc.font('Helvetica-Bold')
.fontSize(8.5)
.fillColor(textColor)
.text('Offener Betrag:', summaryX, y, { width: 110 });
doc.font('Helvetica-Bold')
.fontSize(8.5)
.fillColor(textColor)
.text(formatCurrency(openAmount), summaryX + 110, y, { width: 110, align: 'right' });
y += 16;
}
}
} else {
// Non-price summary (total quantity and positions count)
if (y + 50 > 750) {
doc.addPage();
y = 50;
} else {
y += 15;
}
doc.roundedRect(45, y, 505, 30, 4)
.fillColor(lightBg)
.fill()
.strokeColor(lightBorder)
.lineWidth(0.5)
.stroke();
doc.font('Helvetica')
.fontSize(9)
.fillColor(mutedColor)
.text('Gesamtübersicht:', 55, y + 9);
doc.font('Helvetica-Bold')
.fontSize(9)
.fillColor(textColor)
.text(`${lines.length} Positionen • Gesamtmenge: ${formatQuantity(totalQty)} Stück`, 150, y + 9, {
width: 385,
align: 'right',
});
y += 45;
}
// Closing note
if (config.closingText) {
if (y + 40 > 750) {
doc.addPage();
y = 50;
}
doc.font('Helvetica')
.fontSize(8.5)
.fillColor(mutedColor)
.text(config.closingText, 45, y, { width: 505 });
}
// ----------------------------------------------------
// 5. Footers on all pages
// ----------------------------------------------------
const range = doc.bufferedPageRange();
for (let i = 0; i < range.count; i++) {
doc.switchToPage(i);
doc.page.margins.bottom = 0;
// Footer rule
doc.moveTo(45, 785)
.lineTo(550, 785)
.strokeColor(lightBorder)
.lineWidth(0.5)
.stroke();
// Footer text left
const footerInfo = [
companyName,
companyTaxId ? `USt-IdNr.: ${companyTaxId}` : null,
companyEmail,
companyPhone,
].filter(Boolean).join(' • ');
doc.font('Helvetica')
.fontSize(7.5)
.fillColor(mutedColor)
.text(footerInfo || 'Vielen Dank für Ihre Zusammenarbeit!', 45, 793, {
width: 350,
ellipsis: true,
lineBreak: false,
});
// Footer text right: Page X of Y
doc.text(
`Seite ${i + 1} von ${range.count}`,
450,
793,
{ width: 100, align: 'right', lineBreak: false }
);
}
doc.end();
} catch (err) {
reject(err);
}
});
}
/**
* Creates and attaches a PDF to any manifest (quote, order, shipment, delivery)
* and emits SSE updates.
*
* @param {number} googleAuthId - Google auth user ID.
* @param {number} companyId - Active company ID.
* @param {number} manifestId - Manifest ID.
* @param {'quote'|'order'|'shipment'|'delivery'} type - Manifest type.
* @returns {Promise<{manifest: object, attachment: object, pdfBuffer: Buffer, filename: string}>}
*/
export async function createAndAttachManifestPdf(googleAuthId, companyId, manifestId, type = 'order') {
const store = await getStore();
let manifest;
if (type === 'quote') {
manifest = await store.quote.getById(googleAuthId, manifestId);
} else if (type === 'order') {
manifest = await dbOrder.getById(googleAuthId, manifestId);
} else if (type === 'shipment') {
manifest = await store.shipment.getById(googleAuthId, manifestId);
} else if (type === 'delivery') {
manifest = await store.delivery.getById(googleAuthId, manifestId);
} else {
throw new Error(`Unsupported manifest type: ${type}`);
}
if (!manifest) {
throw new Error('Manifest not found');
}
const partnerId = manifest.entityId ?? manifest.supplierId ?? null;
const [company, settings, partner, addresses] = await Promise.all([
store.company.getById(companyId).catch(() => null),
store.companysetting.list(googleAuthId, { frontendOnly: false, maskWriteonly: false }).catch(() => []),
partnerId ? store.entity.getById(googleAuthId, partnerId).catch(() => null) : null,
partnerId ? store.entityaddress.list(googleAuthId, partnerId).catch(() => []) : [],
]);
const pdfBuffer = await generateManifestPdf({
type,
manifest,
company: company || {},
settings: settings || [],
partner: partner || null,
addresses: addresses || [],
});
const config = TYPE_CONFIG[type] || TYPE_CONFIG.order;
const safeNumber = String(manifest.number || manifest.id).replace(/[^a-zA-Z0-9._-]/g, '_');
const filename = `${config.filenamePrefix}-${safeNumber}.pdf`;
const attachment = await store.manifestattachment.add(googleAuthId, manifest.id, {
mimetype: 'application/pdf',
name: filename,
content: pdfBuffer,
meta: { type: `${type}_pdf` },
});
emit(type, { companyId });
return { manifest, attachment, pdfBuffer, filename };
}
// Re-export alias for order backwards compatibility
export { generateManifestPdf as generateOrderPdf };

8
src/services/orderPdf.js Normal file
View File

@@ -0,0 +1,8 @@
export {
formatDate,
formatCurrency,
formatQuantity,
generateManifestPdf,
generateOrderPdf,
createAndAttachManifestPdf,
} from './manifestPdf.js';

View File

@@ -21,7 +21,7 @@ import {
* call with the image attached. This tool is that bridge:
*
* - 1 image → resized to fit 400×400 and sent as a single WebP.
* - 2–9 → composited into a labelled grid (2x1/3x1/2x2/3x2/3x3) with the file name
* - 2-9 → composited into a labelled grid (2x1/3x1/2x2/3x2/3x3) with the file name
* printed under each cell, so one cheap call can compare many candidates.
* - Database images are addressed as `image/<fullHash>` and fetched from `/image/:hash`.
*
@@ -314,7 +314,7 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
const inputs = extractImageInputs({ paths });
if (inputs.length === 0) {
throw new ToolError(
'analyze_image requires "paths": an array of 1–9 VFS paths (e.g. ["images/a-1.avif"]) ' +
'analyze_image requires "paths": an array of 1-9 VFS paths (e.g. ["images/a-1.avif"]) ' +
'or database paths (e.g. ["image/<fullHash>"]). Get candidates from search_images first.'
);
}

View File

@@ -102,7 +102,9 @@ export const TOOL_DESCRIPTIONS = {
subagent:
'Delegate a task to an isolated subagent. Pre-loads requested SKILL files into its ' +
'system prompt and returns a compact summary of tool executions alongside the final output. ' +
'Subagents can call all tools you can call except subagent, ask_user and set_user_route',
'Subagents can call all tools you can call except subagent, ask_user, and set_user_route. ' +
'To efficiently communicate with subagents, store the input and output data in VFS files ' +
'and provide these paths in the subagent instructions.',
};
// #endregion
@@ -171,7 +173,7 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
analyze_image: {
note: NOTE_DESC,
paths: '1–9 VFS paths or DB paths ("image/<hash>").',
paths: '1-9 VFS paths or DB paths ("image/<hash>").',
prompt: 'Inspection criteria or question.',
grid: 'Grid layout or null for auto.',
},
@@ -214,7 +216,7 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
searchquery: 'Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).',
excludewords: 'Keywords to exclude (AND logic).',
offset: 'Pagination offset (default 0).',
limit: 'Window size 1–20 (default 3).',
limit: 'Window size 1-20 (default 3).',
},
edit_image: {
@@ -241,7 +243,7 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
taxRate: 'VAT percentage (default 19).',
amount: 'Price amount when paired with "from".',
from: '"gross" (default) or "net".',
precision: 'Net price API decimal precision 2–8 (default 6).',
precision: 'Net price API decimal precision 2-8 (default 6).',
items: 'Array of prices or calculation objects for batch processing.',
},
@@ -259,7 +261,6 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
};
export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
export const PATCH_PARAM_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
export const ANALYZE_IMAGE_VISION_COMPARE_PROMPT =
'Compare these images, evaluate visual quality, flag watermarks/overlays/defects, and state the best candidate with rationale.';
export const ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT =
@@ -312,32 +313,38 @@ export function buildSkillIndex(vfsRoot) {
export async function buildSystemPrompt(vfsRoot, googleAuthId) {
let prefix = '';
let suffix = '';
if (googleAuthId) {
try {
const prefixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-prefix', { maskWriteonly: false });
prefix = prefixSetting?.value?.trim() || '';
} catch (err) {
console.error('conversation: failed to read systemprompt-prefix:', err.message);
const [prefixRes, suffixRes] = await Promise.allSettled([
dbCompanySetting.getByKey(googleAuthId, 'systemprompt-prefix', { maskWriteonly: false }),
dbCompanySetting.getByKey(googleAuthId, 'systemprompt-suffix', { maskWriteonly: false }),
]);
if (prefixRes.status === 'fulfilled') {
prefix = prefixRes.value?.value?.trim() || '';
} else {
console.error('conversation: failed to read systemprompt-prefix:', prefixRes.reason?.message);
}
try {
const suffixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-suffix', { maskWriteonly: false });
suffix = suffixSetting?.value?.trim() || '';
} catch (err) {
console.error('conversation: failed to read systemprompt-suffix:', err.message);
if (suffixRes.status === 'fulfilled') {
suffix = suffixRes.value?.value?.trim() || '';
} else {
console.error('conversation: failed to read systemprompt-suffix:', suffixRes.reason?.message);
}
}
const today = new Date().toISOString().split('T')[0];
const basePrompt = [
'QuixPOS Article Management System API Assistant',
`Date: ${today}`,
'[TESTSYSTEM: destructive commands allowed; primary goal is finding bugs]',
'',
SYSTEM_PROMPT_RULES,
'',
buildSkillIndex(vfsRoot),
'',
SYSTEM_PROMPT_ROUTE_MATRIX,
'',
`Date: ${today}`,
'',
buildSkillIndex(vfsRoot),
].join('\n');
const sections = [];

View File

@@ -64,7 +64,7 @@ export function buildClientStep2(authCode, config) {
* summary: POS discovery and pairing
* description: |
* Special API for POS bind and sync.
* - Step 1 (Discovery): `authCode` length 1–4. Returns certificate fingerprints, serial number, and mandantId.
* - Step 1 (Discovery): `authCode` length 1-4. Returns certificate fingerprints, serial number, and mandantId.
* - Step 2 (Pairing): `authCode` length 6 (pairingpin from posterminal). Verifies the pairingpin, sets token, paired_at, and clientserial, revokes the pairingpin, and returns device credentials and company metadata.
* security: []
* x-markdown: pos/client.md