diff --git a/docs/api/routes.md b/docs/api/routes.md index a3fbcb4..2e1c92a 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -18,7 +18,7 @@ Overview of all 162 API endpoints across all modules. | `GET` | `/article/{sku}` | `sku:string` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Get article by SKU | | `PATCH` | `/article/{sku}` | `sku:string, body: { name?, unitamount?, unitSi?, track_stock? }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Update article by SKU | | `GET` | `/article/{sku}/articlelist` | `sku:string` | `200 { items: { id, articleId, sku, name, count, sort }[] }` | Get Stückliste (bundle items) for an article | -| `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article | +| `PUT` | `/article/{sku}/articlelist` | `sku:string, body: { items: { articleId?, sku?: string, count?, sort? }[] }` | `200 { items: object[] }` | Set Stückliste (bundle items) for an article | | `GET` | `/article/{sku}/attributevalue` | `sku:string` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | List article attribute values | | `PUT` | `/article/{sku}/attributevalue` | `sku:string, body: { attributevalues: any[] }` | `200 { attributevalues: { id, articleId, attributeId, attributeName, exclusive, attributeValueId, value, sort }[] }` | Bulk set article attribute values | | `PUT` | `/article/{sku}/brand` | `sku:string, body: { brandId: integer }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Set or clear article brand by SKU | @@ -33,17 +33,17 @@ Overview of all 162 API endpoints across all modules. | `DELETE` | `/article/{sku}/image/{fullHash}` | `sku:string, fullHash:string` | `204 void` | Delete article image | | `PUT` | `/article/{sku}/images/order` | `sku:string, body: { fullHashes: string[] }` | `200 { fullHashes: string[] }` | Reorder article images | | `GET` | `/article/{sku}/optionslist` | `sku:string` | `200 { options: { id, articleId, sku, name, count, sort, mandated }[] }` | Get options list for an article | -| `PUT` | `/article/{sku}/optionslist` | `sku:string, body: { options: { articleId?, sku?, count?, sort?, mandated? }[] }` | `200 { options: object[] }` | Set options list for an article | +| `PUT` | `/article/{sku}/optionslist` | `sku:string, body: { options: { articleId?, sku?: string, count?, sort?, mandated? }[] }` | `200 { options: object[] }` | Set options list for an article | | `PUT` | `/article/{sku}/prices` | `sku:string, body: { prices: { price, tax, minmultiplier? }[] }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Replace POS prices by SKU | | `POST` | `/article/{sku}/print` | `sku:string, body: { templateId, printerId, quantity }` | `200` | Print article label | | `GET` | `/article/{sku}/property` | `sku:string` | `200 { properties: { id, articleId, propertyId, name, exclusive, value, sort }[] }` | List article properties | | `PUT` | `/article/{sku}/property` | `sku:string, body: { properties: { propertyId, value, sort? }[] }` | `200 { properties: { id, articleId, propertyId, name, exclusive, value, sort }[] }` | Bulk set article properties | | `DELETE` | `/article/{sku}/property/{propertyId}` | `sku:string, propertyId:integer` | `200` | Delete article property | | `GET` | `/article/{sku}/supplier` | `sku:string` | `200 { suppliers: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price }[] }` | List article suppliers | -| `POST` | `/article/{sku}/supplier` | `sku:string, body: { entityId, sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }` | `201 { supplier: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } }` | Assign supplier to article | +| `POST` | `/article/{sku}/supplier` | `sku:string, body: { entityId, sku?: string, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }` | `201 { supplier: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } }` | Assign supplier to article | | `PUT` | `/article/{sku}/supplier/order` | `sku:string, body: { ids: integer[] }` | `200 { ids: integer[] }` | Reorder article suppliers | | `DELETE` | `/article/{sku}/supplier/{id}` | `sku:string, id:integer` | `204 void` | Remove supplier assignment from article | -| `PATCH` | `/article/{sku}/supplier/{id}` | `sku:string, id:integer, body: { sku?, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }` | `200 { supplier: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } }` | Update article supplier assignment | +| `PATCH` | `/article/{sku}/supplier/{id}` | `sku:string, id:integer, body: { sku?: string, pu?, stock?, sort?, netPrice?, taxId?, taxRate? }` | `200 { supplier: { id, articleId, entityId, supplierName, sku, pu, stock, sort, priceId, price } }` | Update article supplier assignment | | `PUT` | `/article/{sku}/track_stock` | `sku:string, body: { track_stock: boolean }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Set article track_stock flag by SKU | | `PUT` | `/article/{sku}/weight` | `sku:string, body: { weight: number }` | `200 { article: { id, sku, name, slug, track_stock, unitamount, weight, unit, brand, categoryIds: integer[], photos: object[], eans: string[], prices: object[], suppliers: object[] } }` | Set article weight by SKU | | `GET` | `/articleIndex` | — | `200 { articles: { id, sku, name, eans: string[], categoryIds: integer[] }[] }` | List article search index | @@ -86,11 +86,11 @@ Overview of all 162 API endpoints across all modules. | `DELETE` | `/customergroup/{id}` | `id:integer` | `204 void` | Delete customer group | | `PATCH` | `/customergroup/{id}` | `id:integer, body: { name: string }` | `200 { customergroup: { id, name, sort } }` | Update customer group | | `GET` | `/delivery` | — | `200 { deliveries: { id, number, type, supplierId, externalNumber, date, createdAt, ledger }[] }` | List deliveries | -| `POST` | `/delivery` | `body: { externalNumber, date, lines: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[], supplierId?, files?: { mimetype, name, content, sort?, size?, hash?, created?, modified?, meta? }[] }` | `201 { delivery: { id, number, type, supplierId, externalNumber, date, createdAt, ledger, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[], files: { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby, meta }[] } }` | Create delivery | +| `POST` | `/delivery` | `body: { externalNumber, date, lines: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[], supplierId?, files?: { mimetype, name, content, sort?, size?, hash?, created?, modified?, meta? }[] }` | `201 { delivery: { id, number, type, supplierId, externalNumber, date, createdAt, ledger, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[], files: { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby, meta }[] } }` | Create delivery | | `POST` | `/delivery/parse` | `form: { file: string }` | `200 SSE` | Parse document (SSE) | | `DELETE` | `/delivery/{id}` | `id:integer` | `204 void` | Delete delivery | | `GET` | `/delivery/{id}` | `id:integer` | `200 { delivery: { id, number, type, supplierId, externalNumber, date, createdAt, ledger, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[], files: { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby, meta }[] } }` | Get delivery | -| `PATCH` | `/delivery/{id}` | `id:integer, body: { externalNumber?, date?, supplierId?, lines?: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[], files?: { mimetype, name, content, sort?, size?, hash?, created?, modified?, meta? }[] }` | `200 { delivery: { id, number, type, supplierId, externalNumber, date, createdAt, ledger, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[], files: { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby, meta }[] } }` | Update delivery | +| `PATCH` | `/delivery/{id}` | `id:integer, body: { externalNumber?, date?, supplierId?, lines?: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[], files?: { mimetype, name, content, sort?, size?, hash?, created?, modified?, meta? }[] }` | `200 { delivery: { id, number, type, supplierId, externalNumber, date, createdAt, ledger, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[], files: { id, manifestId, fileId, sort, hash, mimetype, name, size, created, modified, thumbavif, thumbx, thumby, meta }[] } }` | Update delivery | | `GET` | `/delivery/{id}/file/{fileId}` | `id:integer, fileId:integer, query: download?:boolean` | `200 string` | Get delivery attachment file content | | `GET` | `/entity` | `query: customergroupId?:integer, role?:string` | `200 { entities: { id, customergroupId, firstName, name, number, createdAt, updatedAt }[] }` | List entities | | `POST` | `/entity` | `body: { customergroupId, name, firstName?, number? }` | `201 { entity: { id, customergroupId, firstName, name, number, createdAt, updatedAt } }` | Create entity | @@ -122,10 +122,10 @@ Overview of all 162 API endpoints across all modules. | `GET` | `/number` | — | `200 { numbers: { id, type, prefix, padding, next }[] }` | List number sequences | | `PATCH` | `/number/{id}` | `id:integer, body: { prefix?, padding?, next? }` | `200 { number: { id, type, prefix, padding, next } }` | Update number sequence | | `GET` | `/order` | — | `200 { orders: { id, number, type, entityId, date, createdAt }[] }` | List orders | -| `POST` | `/order` | `body: { number, date, lines: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[], entityId? }` | `201 { order: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Create order | +| `POST` | `/order` | `body: { number, date, lines: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[], entityId? }` | `201 { order: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Create order | | `DELETE` | `/order/{id}` | `id:integer` | `204 void` | Delete order | | `GET` | `/order/{id}` | `id:integer` | `200 { order: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Get order | -| `PATCH` | `/order/{id}` | `id:integer, body: { number?, date?, entityId?, lines?: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[] }` | `200 { order: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Update order | +| `PATCH` | `/order/{id}` | `id:integer, body: { number?, date?, entityId?, lines?: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[] }` | `200 { order: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Update order | | `GET` | `/posterminal` | — | `200 { posterminals: { id, name, pairingpin, categoryIds: integer[] }[] }` | List POS terminals | | `PUT` | `/posterminal` | `body: { name: string }` | `200 { posterminal: { id, name, pairingpin, categoryIds: integer[] } }` | Create POS terminal | | `DELETE` | `/posterminal/{id}` | `id:integer` | `204 void` | Delete POS terminal | @@ -146,13 +146,13 @@ Overview of all 162 API endpoints across all modules. | `DELETE` | `/propertyname/{id}` | `id:integer` | `200` | Delete property name | | `PATCH` | `/propertyname/{id}` | `id:integer, body: { name?, sort?, exclusive? }` | `200 { propertyname: { id, name, sort, exclusive } }` | Update property name | | `GET` | `/quote` | — | `200 { quotes: { id, number, type, entityId, date, createdAt }[] }` | List quotes | -| `POST` | `/quote` | `body: { number, date, lines: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[], entityId? }` | `201 { quote: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Create quote | +| `POST` | `/quote` | `body: { number, date, lines: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[], entityId? }` | `201 { quote: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Create quote | | `DELETE` | `/quote/{id}` | `id:integer` | `204 void` | Delete quote | | `GET` | `/quote/{id}` | `id:integer` | `200 { quote: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Get quote | -| `PATCH` | `/quote/{id}` | `id:integer, body: { number?, date?, entityId?, lines?: { amount, net, taxrate, sku?, name?, pu?, rebate?, comment? }[] }` | `200 { quote: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Update quote | +| `PATCH` | `/quote/{id}` | `id:integer, body: { number?, date?, entityId?, lines?: { amount, net, taxrate, sku?: string, name?, pu?, rebate?, comment? }[] }` | `200 { quote: { id, number, type, entityId, date, createdAt, lines: { id, sku, name, pu, amount, net, rebate, taxrate, comment }[] } }` | Update quote | | `POST` | `/serper/image` | `body: { url: string }` | `200 avif` | Crawl image from URL, crop white/transparency borders, and return AVIF | | `POST` | `/serper/images` | `body: { q, gl?, hl?, num?, preferSource? }` | `200 SSE` | Search images via Serper.dev and stream WebP thumbnails (SSE) | -| `POST` | `/stock/adjust` | `body: { sku?, warehouseId?, amount?, text?, lines?: { sku, amount, warehouseId?, text? }[] }` | `201 { manifest, lines: object[], ledger: { id, manifestarticle_id, transaction_type, amount, transaction_text, hash_version, prev_hash, sku, created_at }[] }` | Adjust stock manually | +| `POST` | `/stock/adjust` | `body: { sku?: string, warehouseId?, amount?, text?, lines?: { sku: string, amount, warehouseId?, text? }[] }` | `201 { manifest, lines: object[], ledger: { id, manifestarticle_id, transaction_type, amount, transaction_text, hash_version, prev_hash, sku, created_at }[] }` | Adjust stock manually | | `GET` | `/supplier` | — | `200 { suppliers: { id, name, createdAt, updatedAt }[] }` | List suppliers | | `POST` | `/supplier` | `body: { name: string }` | `201 { supplier: { id, name, createdAt, updatedAt } }` | Create supplier | | `DELETE` | `/supplier/{id}` | `id:integer` | `204 void` | Delete supplier | diff --git a/docs/api/routes/article/_sku/articlelist.put.md b/docs/api/routes/article/_sku/articlelist.put.md index b4290df..efd0441 100644 --- a/docs/api/routes/article/_sku/articlelist.put.md +++ b/docs/api/routes/article/_sku/articlelist.put.md @@ -17,6 +17,10 @@ Configures the article as a Stückliste covering a list of count * article. A fa | Field | Required | Type | Description | |-------|----------|------|-------------| | `items` | yes | object[] | | +| `items[].articleId` | no | integer | | +| `items[].sku` | no | string | | +| `items[].count` | no | integer | | +| `items[].sort` | no | integer | | ```json { diff --git a/docs/api/routes/article/_sku/optionslist.put.md b/docs/api/routes/article/_sku/optionslist.put.md index 26900ee..fdd86d3 100644 --- a/docs/api/routes/article/_sku/optionslist.put.md +++ b/docs/api/routes/article/_sku/optionslist.put.md @@ -17,6 +17,11 @@ Configures options for an article. Some options can be mandated (come with the a | Field | Required | Type | Description | |-------|----------|------|-------------| | `options` | yes | object[] | | +| `options[].articleId` | no | integer | | +| `options[].sku` | no | string | | +| `options[].count` | no | integer | | +| `options[].sort` | no | integer | | +| `options[].mandated` | no | boolean | | ```json { diff --git a/docs/api/routes/article/_sku/property.put.md b/docs/api/routes/article/_sku/property.put.md index 9dd2d5f..fb8065f 100644 --- a/docs/api/routes/article/_sku/property.put.md +++ b/docs/api/routes/article/_sku/property.put.md @@ -17,6 +17,9 @@ Replaces all properties for an article. | Field | Required | Type | Description | |-------|----------|------|-------------| | `properties` | yes | object[] | | +| `properties[].propertyId` | yes | integer | | +| `properties[].value` | yes | string | | +| `properties[].sort` | no | integer | | ```json { diff --git a/docs/api/routes/article/set-prices.md b/docs/api/routes/article/set-prices.md index 7a9ab22..da15cd2 100644 --- a/docs/api/routes/article/set-prices.md +++ b/docs/api/routes/article/set-prices.md @@ -20,6 +20,9 @@ Empty `prices` clears all POS prices. Returns the same shape as GET by SKU. | Field | Required | Type | Description | |-------|----------|------|-------------| | `prices` | yes | object[] | | +| `prices[].price` | yes | number | Net price | +| `prices[].tax` | yes | number | Tax rate (e.g. 19) | +| `prices[].minmultiplier` | no | number | Defaults to 1; must be unique per request | ```json { diff --git a/docs/api/routes/stock/adjust.md b/docs/api/routes/stock/adjust.md index d6a0aa7..86235a9 100644 --- a/docs/api/routes/stock/adjust.md +++ b/docs/api/routes/stock/adjust.md @@ -7,16 +7,22 @@ Adjust stock manually Writes signed (+ or -) entries to the append-only ledger under an implicit manifest of type "adjustment", allocating a number from the "adjustment" number scheme. +**Note:** `sku` must be a string (e.g. `"25"`), not a number. For single-item adjustments, provide `sku` and `amount` at top-level. For batch adjustments, provide `lines: [{ sku, amount, ... }]`. + ## Body (JSON) | Field | Required | Type | Description | |-------|----------|------|-------------| -| `sku` | no | string | Article SKU (for single-item adjustment) | +| `sku` | no | string | Article SKU string (e.g. "25", not a number; for single-item adjustment) | | `warehouseId` | no | integer | Target warehouse ID | | `amount` | no | number | Signed adjustment quantity (+ to add, - to remove) | | `text` | no | string | Reason or memo | | `lines` | no | object[] | Batch adjustment lines | +| `lines[].sku` | yes | string | Article SKU string (e.g. "25", not a number) | +| `lines[].warehouseId` | no | integer | Target warehouse ID | +| `lines[].amount` | yes | number | Signed adjustment quantity (+ to add, - to remove) | +| `lines[].text` | no | string | Reason or memo | ```json { diff --git a/scripts/generateApiDocs.js b/scripts/generateApiDocs.js index 565b587..ace2f9c 100644 --- a/scripts/generateApiDocs.js +++ b/scripts/generateApiDocs.js @@ -197,6 +197,15 @@ function renderSchemaPropertiesTable(schema, components) { } const desc = (prop.description || '').replace(/[\r\n|]+/g, ' ').trim(); lines.push(`| \`${name}\` | ${isReq} | ${type} | ${desc} |`); + if (prop.type === 'array' && prop.items?.properties) { + const itemReq = new Set(prop.items.required || []); + for (const [subName, subProp] of Object.entries(prop.items.properties)) { + const subIsReq = itemReq.has(subName) ? 'yes' : 'no'; + const subType = subProp.type || schemaRefName(subProp) || ''; + const subDesc = (subProp.description || '').replace(/[\r\n|]+/g, ' ').trim(); + lines.push(`| \`${name}[].${subName}\` | ${subIsReq} | ${subType} | ${subDesc} |`); + } + } } lines.push(''); return lines; @@ -557,7 +566,7 @@ function summarizeParamSchema(schema, components) { const sub = summarizeParamSchema(propSchema, components); return `${k}${optSuffix}: ${sub}`; } - if (sortedProps.length === 1 && propSchema?.type) { + if ((k === 'sku' || sortedProps.length === 1) && propSchema?.type) { return `${k}${optSuffix}: ${propSchema.type}`; } return `${k}${optSuffix}`; diff --git a/src/api/openapi.js b/src/api/openapi.js index 171d3e8..15847be 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -975,10 +975,11 @@ * description: Sort order * StockAdjustmentInput: * type: object + * description: Stock adjustment input. Provide `sku` and `amount` for single-item adjustments, or `lines` for batch adjustments. Note that `sku` must be a string (e.g. "25"), not a number. * properties: * sku: * type: string - * description: Article SKU (for single-item adjustment) + * description: Article SKU string (e.g. "25", not a number; for single-item adjustment) * warehouseId: * type: integer * description: Target warehouse ID @@ -998,12 +999,16 @@ * properties: * sku: * type: string + * description: Article SKU string (e.g. "25", not a number) * warehouseId: * type: integer + * description: Target warehouse ID * amount: * type: number + * description: Signed adjustment quantity (+ to add, - to remove) * text: * type: string + * description: Reason or memo * StockAdjustmentResult: * type: object * properties: diff --git a/src/api/routes/stock.js b/src/api/routes/stock.js index b114a10..7e8acb3 100644 --- a/src/api/routes/stock.js +++ b/src/api/routes/stock.js @@ -65,6 +65,8 @@ export function register(app, basePath = '') { * description: | * Writes signed (+ or -) entries to the append-only ledger under an implicit manifest * of type "adjustment", allocating a number from the "adjustment" number scheme. + * + * **Note:** `sku` must be a string (e.g. `"25"`), not a number. For single-item adjustments, provide `sku` and `amount` at top-level. For batch adjustments, provide `lines: [{ sku, amount, ... }]`. * security: * - cookieAuth: [] * x-markdown: stock/adjust.md