u
This commit is contained in:
@@ -30,17 +30,17 @@ Entry: `src/index.js` → `export default class LibDashStore`. Constructor requi
|
|||||||
- Company scoping: no `companyId` arg. `resolveSelectedCompanyAccess(provider)` reads `google_auth.selected_company` → `{ companyId, companyuserId, rootCategoryId }`. Missing → throw `No company selected`. Exceptions taking explicit `companyId`: `setSelectedCompany`, `companyuser.add`, `company.delete`, internals.
|
- Company scoping: no `companyId` arg. `resolveSelectedCompanyAccess(provider)` reads `google_auth.selected_company` → `{ companyId, companyuserId, rootCategoryId }`. Missing → throw `No company selected`. Exceptions taking explicit `companyId`: `setSelectedCompany`, `companyuser.add`, `company.delete`, internals.
|
||||||
- SKU resolution: `store/article/resolveArticleBySku.js` (`RESOLVE_ARTICLE_ACCESS_BY_SKU`) checks company + membership. Throws `Article not found` / `Parent not found` / `Not authorized`.
|
- SKU resolution: `store/article/resolveArticleBySku.js` (`RESOLVE_ARTICLE_ACCESS_BY_SKU`) checks company + membership. Throws `Article not found` / `Parent not found` / `Not authorized`.
|
||||||
- Errors are plain `Error` with exact message strings; HTTP mapping happens in `dashback` route `*ErrorStatus()` functions. Keep messages stable/greppable. Conflict → `409` (e.g. `EAN already in use`, `Warehouse in use in ledger`, `Stock tracking is disabled...`).
|
- Errors are plain `Error` with exact message strings; HTTP mapping happens in `dashback` route `*ErrorStatus()` functions. Keep messages stable/greppable. Conflict → `409` (e.g. `EAN already in use`, `Warehouse in use in ledger`, `Stock tracking is disabled...`).
|
||||||
- Manifest docs (`delivery/order/quote/adjustment`): header in `manifest`, lines in `manifestarticle`, helpers in `src/store/manifestLines.js` (`normalizeLines`, `insertLines`, `parseDate`, `shapeLine`) and `manifestDoc/helpers.js`. `insertLines` rejects father articles (`A father article cannot be booked on documents (SKU)`), resolves name/pu, ensures tax.
|
- Manifest docs (`delivery/order/quote/adjustment`): header in `manifest`, lines in `manifestarticle`, helpers in `src/store/manifestLines.js` (`normalizeLines`, `insertLines`, `parseDate`, `shapeLine`) and `manifestDoc/helpers.js`. `insertLines` rejects parent articles (`A parent article cannot be booked on documents (SKU)`), resolves name/pu, ensures tax.
|
||||||
- Numbers: `store/numbers/nextNumber.js`, types `sku, entity, delivery, adjustment`.
|
- Numbers: `store/numbers/nextNumber.js`, types `sku, entity, delivery, adjustment`.
|
||||||
|
|
||||||
### Ledger + stock (important)
|
### Ledger + stock (important)
|
||||||
|
|
||||||
- `ledger` is append-only, tamper-chained: `hash_version=1`, `prev_hash = SHA-512(canonical JSON of prev row)` or `TODO_TSE_BASED_INITHASH` for genesis. See `store/ledger/add.js` (`shape`, `computePrevHash`, `INIT_HASH`).
|
- `ledger` is append-only, tamper-chained: `hash_version=1`, `prev_hash = SHA-512(canonical JSON of prev row)` or `TODO_TSE_BASED_INITHASH` for genesis. See `store/ledger/add.js` (`shape`, `computePrevHash`, `INIT_HASH`).
|
||||||
- Stock is **derived**: `SUM(ledger.amount) GROUP BY warehouse`, joined `ledger → manifestarticle → manifest`, matched on `ma.sku = article.sku OR ma.sku IN (SELECT sku FROM articlesupplier WHERE article_id = ...)`, filtered by `m.company_id`. See `providers/*/queries/article.js` (`warehousesJson`/`stock_calc`) and `posterminal.js` (`fBestand`). No `stock` column on article.
|
- Stock is **derived**: `SUM(ledger.amount) GROUP BY warehouse`, joined `ledger → manifestarticle → manifest`, matched on `ma.sku = article.sku OR ma.sku IN (SELECT sku FROM articlesupplier WHERE article_id = ...)`, filtered by `m.company_id`. See `providers/*/queries/article.js` (`warehousesJson`/`stock_calc`) and `posterminal.js` (`fBestand`). No `stock` column on article.
|
||||||
- `store/stock/adjust.js` creates implicit manifest `type='adjustment'` (number from `adjustment` scheme) + one `manifestarticle` line per item (`amount: abs(amount)`) + one `ledger` row per item (`amount: signed`) + `articlehistory` (`Bestandskorrektur ±n (text)`). Validates: father → `A father article cannot have stock (SKU)`; Stückliste (`articlelist_id != null`) → `A Stückliste cannot have manual stock adjustments (SKU)`; **`track_stock==0` → `Stock tracking is disabled for article (SKU)`** (added as bugfix; checked before number allocation/manifest write, so batch is all-or-nothing).
|
- `store/stock/adjust.js` creates implicit manifest `type='adjustment'` (number from `adjustment` scheme) + one `manifestarticle` line per item (`amount: abs(amount)`) + one `ledger` row per item (`amount: signed`) + `articlehistory` (`Bestandskorrektur ±n (text)`). Validates: parent → `A parent article cannot have stock (SKU)`; Stückliste (`articlelist_id != null`) → `A Stückliste cannot have manual stock adjustments (SKU)`; **`track_stock==0` → `Stock tracking is disabled for article (SKU)`** (added as bugfix; checked before number allocation/manifest write, so batch is all-or-nothing).
|
||||||
- `store/ledger/add.js` enforces the same guard for **any warehouse-scoped entry** (`warehouse_id != null` + line has SKU): looks up `article` by `sku` or `articlesupplier.sku` in same company, rejects if any match has `track_stock==0`. Non-warehouse entries (e.g. `PAYMENT`, memo `NOTE`) are still allowed. Guard runs before `GET_LATEST_ROW`/`INSERT_ROW`, inside the same runner/tx.
|
- `store/ledger/add.js` enforces the same guard for **any warehouse-scoped entry** (`warehouse_id != null` + line has SKU): looks up `article` by `sku` or `articlesupplier.sku` in same company, rejects if any match has `track_stock==0`. Non-warehouse entries (e.g. `PAYMENT`, memo `NOTE`) are still allowed. Guard runs before `GET_LATEST_ROW`/`INSERT_ROW`, inside the same runner/tx.
|
||||||
- `POST /ledger` with `warehouse_id` is therefore also blocked for untracked articles; delivery receipts booked via ledger go through the same check.
|
- `POST /ledger` with `warehouse_id` is therefore also blocked for untracked articles; delivery receipts booked via ledger go through the same check.
|
||||||
- `article.track_stock`: `BIT/INTEGER DEFAULT 1`. Managed via `article.createUnderParent({track_stock})`, `updateBySku`, `setTrackStockBySku`, `POST /article` (`track_stock`/`trackStock`, default true). `toggleFather` sets `track_stock=0` when becoming father. POS surfaces it as `use_stock '1'/'0'` (`store/posterminal/getProductList.js`).
|
- `article.track_stock`: `BIT/INTEGER DEFAULT 1`. Managed via `article.createUnderParent({track_stock})`, `updateBySku`, `setTrackStockBySku`, `POST /article` (`track_stock`/`trackStock`, default true). `toggleParent` sets `track_stock=0` when becoming parent. POS surfaces it as `use_stock '1'/'0'` (`store/posterminal/getProductList.js`).
|
||||||
|
|
||||||
## dashback (Express API)
|
## dashback (Express API)
|
||||||
|
|
||||||
|
|||||||
@@ -79,24 +79,24 @@ function articleErrorStatus(error) {
|
|||||||
if (error.message === 'Invalid track_stock') return 400;
|
if (error.message === 'Invalid track_stock') return 400;
|
||||||
if (error.message === 'Invalid categoryIds') return 400;
|
if (error.message === 'Invalid categoryIds') return 400;
|
||||||
if (error.message === 'Attribute name not found') return 404;
|
if (error.message === 'Attribute name not found') return 404;
|
||||||
if (error.message === 'attributeNameId is required to become a father article') return 400;
|
if (error.message === 'attributeNameId is required to become a parent article') return 400;
|
||||||
if (error.message?.includes('already used by an ancestor father')) return 400;
|
if (error.message?.includes('already used by an ancestor parent')) return 400;
|
||||||
if (error.message?.includes('cannot have attribute values for its variant attribute')) return 400;
|
if (error.message?.includes('cannot have attribute values for its variant attribute')) return 400;
|
||||||
if (error.message?.includes('A father article cannot have') || error.message?.includes('father article cannot have')) return 400;
|
if (error.message?.includes('A parent article cannot have') || error.message?.includes('parent article cannot have')) return 400;
|
||||||
if (error.message === 'Cannot disable father mode on an article that still has children') return 400;
|
if (error.message === 'Cannot disable parent mode on an article that still has children') return 400;
|
||||||
if (error.message === 'Article is not a father article') return 400;
|
if (error.message === 'Article is not a parent article') return 400;
|
||||||
if (error.message === 'Attribute value is required for child article' || error.message === 'Attribute value is required') return 400;
|
if (error.message === 'Attribute value is required for child article' || error.message === 'Attribute value is required') return 400;
|
||||||
if (error.message === 'Attribute value not found or does not match father attribute') return 400;
|
if (error.message === 'Attribute value not found or does not match parent attribute') return 400;
|
||||||
if (error.message === 'Child article does not belong to this father') return 400;
|
if (error.message === 'Child article does not belong to this parent') return 400;
|
||||||
if (error.message === 'Article already belongs to another father') return 409;
|
if (error.message === 'Article already belongs to another parent') return 409;
|
||||||
if (error.message?.includes('sku is not allowed when creating a child article')) return 400;
|
if (error.message?.includes('sku is not allowed when creating a child article')) return 400;
|
||||||
if (error.message === 'sku is required' || error.message?.includes('sku is required')) return 400;
|
if (error.message === 'sku is required' || error.message?.includes('sku is required')) return 400;
|
||||||
if (error.message === 'Article is already connected to a father article') return 400;
|
if (error.message === 'Article is already connected to a parent article') return 400;
|
||||||
if (error.message === 'An article cannot be a child of itself') return 400;
|
if (error.message === 'An article cannot be a child of itself') return 400;
|
||||||
if (error.message === 'Variant with this attribute value already exists') return 400;
|
if (error.message === 'Variant with this attribute value already exists') return 400;
|
||||||
if (error.message?.startsWith('Child article') && error.message?.includes('already exists')) return 400;
|
if (error.message?.startsWith('Child article') && error.message?.includes('already exists')) return 400;
|
||||||
if (error.message?.toLowerCase().includes('duplicate variant')) return 400;
|
if (error.message?.toLowerCase().includes('duplicate variant')) return 400;
|
||||||
if (error.message === 'Father article not found') return 404;
|
if (error.message === 'Parent article not found') return 404;
|
||||||
if (
|
if (
|
||||||
error.message?.includes('Stückliste') ||
|
error.message?.includes('Stückliste') ||
|
||||||
error.message?.toLowerCase().includes('option') ||
|
error.message?.toLowerCase().includes('option') ||
|
||||||
@@ -766,7 +766,7 @@ export function register(app, basePath = '') {
|
|||||||
* article:
|
* article:
|
||||||
* $ref: '#/components/schemas/ArticleDetail'
|
* $ref: '#/components/schemas/ArticleDetail'
|
||||||
* 400:
|
* 400:
|
||||||
* description: Invalid prices, POS customer group required, or father article cannot have prices
|
* description: Invalid prices, POS customer group required, or parent article cannot have prices
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
@@ -1803,18 +1803,18 @@ export function register(app, basePath = '') {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
* /article/{sku}/father:
|
* /article/{sku}/parent:
|
||||||
* post:
|
* post:
|
||||||
* summary: Toggle father mode for an article
|
* summary: Toggle parent mode for an article
|
||||||
* description: |
|
* description: |
|
||||||
* Enables or disables father article status. Enabling requires specifying an
|
* Enables or disables parent article status. Enabling requires specifying an
|
||||||
* attributeNameId; disabling requires the article to have zero children.
|
* attributeNameId; disabling requires the article to have zero children.
|
||||||
*
|
*
|
||||||
* `attributeNameId` is **conditionally required**: it is optional in the schema
|
* `attributeNameId` is **conditionally required**: it is optional in the schema
|
||||||
* because it is only required when `isFather` is `true`. Omitting it (or passing
|
* because it is only required when `isParent` is `true`. Omitting it (or passing
|
||||||
* `0`/a negative value) together with `isFather: true` returns `400`
|
* `0`/a negative value) together with `isParent: true` returns `400`
|
||||||
* `attributeNameId is required to become a father article`. It must not be sent
|
* `attributeNameId is required to become a parent article`. It must not be sent
|
||||||
* when `isFather` is `false`.
|
* when `isParent` is `false`.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -1832,17 +1832,17 @@ export function register(app, basePath = '') {
|
|||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
* type: object
|
* type: object
|
||||||
* required: [isFather]
|
* required: [isParent]
|
||||||
* properties:
|
* properties:
|
||||||
* isFather:
|
* isParent:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: True to make article a father, false to revert to regular article
|
* description: True to make article a parent, false to revert to regular article
|
||||||
* attributeNameId:
|
* attributeNameId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: |
|
* description: |
|
||||||
* **Conditionally required when `isFather` is `true`** (required iff isFather=true).
|
* **Conditionally required when `isParent` is `true`** (required iff isParent=true).
|
||||||
* The attributename id used for the article's variants. Omitting it while
|
* The attributename id used for the article's variants. Omitting it while
|
||||||
* enabling father mode returns 400. Must be omitted when `isFather` is `false`.
|
* enabling parent mode returns 400. Must be omitted when `isParent` is `false`.
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Updated article
|
* description: Updated article
|
||||||
@@ -1855,25 +1855,25 @@ export function register(app, basePath = '') {
|
|||||||
* $ref: '#/components/schemas/Article'
|
* $ref: '#/components/schemas/Article'
|
||||||
* 400:
|
* 400:
|
||||||
* description: |
|
* description: |
|
||||||
* Invalid input, `attributeNameId` missing while `isFather` is true, article still
|
* Invalid input, `attributeNameId` missing while `isParent` is true, article still
|
||||||
* has children, article has stock/suppliers/a Stückliste/options, or the attribute is
|
* has children, article has stock/suppliers/a Stückliste/options, or the attribute is
|
||||||
* already used by an ancestor father article.
|
* already used by an ancestor parent article.
|
||||||
* 404:
|
* 404:
|
||||||
* description: Article or attribute name not found
|
* description: Article or attribute name not found
|
||||||
*/
|
*/
|
||||||
app.post(`${basePath}/article/:sku/father`, async (req, res) => {
|
app.post(`${basePath}/article/:sku/parent`, async (req, res) => {
|
||||||
const parsed = parseSku(req.params.sku);
|
const parsed = parseSku(req.params.sku);
|
||||||
if (parsed.error) return res.status(400).json({ error: parsed.error });
|
if (parsed.error) return res.status(400).json({ error: parsed.error });
|
||||||
|
|
||||||
const isFather = req.body?.isFather;
|
const isParent = req.body?.isParent;
|
||||||
if (typeof isFather !== 'boolean') {
|
if (typeof isParent !== 'boolean') {
|
||||||
return res.status(400).json({ error: 'isFather boolean is required' });
|
return res.status(400).json({ error: 'isParent boolean is required' });
|
||||||
}
|
}
|
||||||
const attributeNameId = req.body?.attributeNameId ?? req.body?.fatherAttributeNameId;
|
const attributeNameId = req.body?.attributeNameId ?? req.body?.parentAttributeNameId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await db.toggleFather(req.user.id, parsed.sku, {
|
const result = await db.toggleParent(req.user.id, parsed.sku, {
|
||||||
isFather,
|
isParent,
|
||||||
attributeNameId,
|
attributeNameId,
|
||||||
});
|
});
|
||||||
emitArticleSignal(req, parsed.sku);
|
emitArticleSignal(req, parsed.sku);
|
||||||
@@ -1881,7 +1881,7 @@ export function register(app, basePath = '') {
|
|||||||
res.status(200).json(result);
|
res.status(200).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const status = articleErrorStatus(error);
|
const status = articleErrorStatus(error);
|
||||||
if (status === 500) console.error('Toggle father failed:', error);
|
if (status === 500) console.error('Toggle parent failed:', error);
|
||||||
res.status(status).json({ error: error.message });
|
res.status(status).json({ error: error.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1890,8 +1890,8 @@ export function register(app, basePath = '') {
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /article/{sku}/children:
|
* /article/{sku}/children:
|
||||||
* get:
|
* get:
|
||||||
* summary: List child articles of a father article
|
* summary: List child articles of a parent article
|
||||||
* description: Returns variants/children linked to this father article.
|
* description: Returns variants/children linked to this parent article.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -1902,7 +1902,7 @@ export function register(app, basePath = '') {
|
|||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Father article SKU
|
* description: Parent article SKU
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: List of children
|
* description: List of children
|
||||||
@@ -1922,7 +1922,7 @@ export function register(app, basePath = '') {
|
|||||||
* type: string
|
* type: string
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
* fathersort:
|
* parentsort:
|
||||||
* type: integer
|
* type: integer
|
||||||
* attributeValueId:
|
* attributeValueId:
|
||||||
* type: integer
|
* type: integer
|
||||||
@@ -1949,8 +1949,8 @@ export function register(app, basePath = '') {
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /article/{sku}/children:
|
* /article/{sku}/children:
|
||||||
* post:
|
* post:
|
||||||
* summary: Create and attach a new child article to a father article
|
* summary: Create and attach a new child article to a parent article
|
||||||
* description: Automatically generates SKU as fatherSku-nextFreeIndex and name from father name + value. Rejects a caller-supplied sku.
|
* description: Automatically generates SKU as parentSku-nextFreeIndex and name from parent name + value. Rejects a caller-supplied sku.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -1961,7 +1961,7 @@ export function register(app, basePath = '') {
|
|||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Father article SKU
|
* description: Parent article SKU
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
@@ -1971,10 +1971,10 @@ export function register(app, basePath = '') {
|
|||||||
* properties:
|
* properties:
|
||||||
* attributeValueId:
|
* attributeValueId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: ID of existing attribute value for the father's attribute
|
* description: ID of existing attribute value for the parent's attribute
|
||||||
* value:
|
* value:
|
||||||
* type: string
|
* type: string
|
||||||
* description: String value to find or create under the father's attribute
|
* description: String value to find or create under the parent's attribute
|
||||||
* name:
|
* name:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Optional custom variant name
|
* description: Optional custom variant name
|
||||||
@@ -1989,10 +1989,10 @@ export function register(app, basePath = '') {
|
|||||||
* article:
|
* article:
|
||||||
* $ref: '#/components/schemas/Article'
|
* $ref: '#/components/schemas/Article'
|
||||||
* 400:
|
* 400:
|
||||||
* description: Father not configured as father, missing value, duplicate variant, or sku was provided in body
|
* description: Parent not configured as parent, missing value, duplicate variant, or sku was provided in body
|
||||||
* put:
|
* put:
|
||||||
* summary: Attach an existing article as a child to a father article
|
* summary: Attach an existing article as a child to a parent article
|
||||||
* description: Attaches an existing article to this father article. Idempotent if already a child. Returns 409 if the article already belongs to another father.
|
* description: Attaches an existing article to this parent article. Idempotent if already a child. Returns 409 if the article already belongs to another parent.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -2003,7 +2003,7 @@ export function register(app, basePath = '') {
|
|||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Father article SKU
|
* description: Parent article SKU
|
||||||
* requestBody:
|
* requestBody:
|
||||||
* required: true
|
* required: true
|
||||||
* content:
|
* content:
|
||||||
@@ -2018,13 +2018,13 @@ export function register(app, basePath = '') {
|
|||||||
* description: SKU of the existing article to attach as child
|
* description: SKU of the existing article to attach as child
|
||||||
* attributeValueId:
|
* attributeValueId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: ID of existing attribute value for the father's attribute
|
* description: ID of existing attribute value for the parent's attribute
|
||||||
* value:
|
* value:
|
||||||
* type: string
|
* type: string
|
||||||
* description: String value to find or create under the father's attribute
|
* description: String value to find or create under the parent's attribute
|
||||||
* reparent:
|
* reparent:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: Whether to allow re-parenting if child already belongs to another father
|
* description: Whether to allow re-parenting if child already belongs to another parent
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Attached child article and list of children
|
* description: Attached child article and list of children
|
||||||
@@ -2042,9 +2042,9 @@ export function register(app, basePath = '') {
|
|||||||
* 400:
|
* 400:
|
||||||
* description: Missing sku or attribute value, or invalid input
|
* description: Missing sku or attribute value, or invalid input
|
||||||
* 404:
|
* 404:
|
||||||
* description: Father or child article not found
|
* description: Parent or child article not found
|
||||||
* 409:
|
* 409:
|
||||||
* description: Article already belongs to another father, or variant value already exists
|
* description: Article already belongs to another parent, or variant value already exists
|
||||||
*/
|
*/
|
||||||
app.post(`${basePath}/article/:sku/children`, async (req, res) => {
|
app.post(`${basePath}/article/:sku/children`, async (req, res) => {
|
||||||
const parsed = parseSku(req.params.sku);
|
const parsed = parseSku(req.params.sku);
|
||||||
@@ -2099,8 +2099,8 @@ export function register(app, basePath = '') {
|
|||||||
* @openapi
|
* @openapi
|
||||||
* /article/{sku}/children/{childSku}:
|
* /article/{sku}/children/{childSku}:
|
||||||
* delete:
|
* delete:
|
||||||
* summary: Remove a child article from a father article
|
* summary: Remove a child article from a parent article
|
||||||
* description: Unlinks the child from the father (or deletes if deleteArticle query/body is true).
|
* description: Unlinks the child from the parent (or deletes if deleteArticle query/body is true).
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -2111,7 +2111,7 @@ export function register(app, basePath = '') {
|
|||||||
* required: true
|
* required: true
|
||||||
* schema:
|
* schema:
|
||||||
* type: string
|
* type: string
|
||||||
* description: Father article SKU
|
* description: Parent article SKU
|
||||||
* - in: path
|
* - in: path
|
||||||
* name: childSku
|
* name: childSku
|
||||||
* required: true
|
* required: true
|
||||||
@@ -2122,7 +2122,7 @@ export function register(app, basePath = '') {
|
|||||||
* name: deleteArticle
|
* name: deleteArticle
|
||||||
* schema:
|
* schema:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* description: If true, permanently deletes the child article (and cascades its entire descendant subtree if it is a father) instead of just unlinking
|
* description: If true, permanently deletes the child article (and cascades its entire descendant subtree if it is a parent) instead of just unlinking
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Child removed
|
* description: Child removed
|
||||||
@@ -2224,7 +2224,7 @@ export function register(app, basePath = '') {
|
|||||||
* /article/{sku}/articlelist:
|
* /article/{sku}/articlelist:
|
||||||
* put:
|
* put:
|
||||||
* summary: Set Stückliste (bundle items) for an article
|
* summary: Set Stückliste (bundle items) for an article
|
||||||
* description: Configures the article as a Stückliste covering a list of count * article. A father article cannot be a Stückliste. Can be combined with options. Pass an empty array to remove the Stückliste.
|
* description: Configures the article as a Stückliste covering a list of count * article. A parent article cannot be a Stückliste. Can be combined with options. Pass an empty array to remove the Stückliste.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -2271,7 +2271,7 @@ export function register(app, basePath = '') {
|
|||||||
* items:
|
* items:
|
||||||
* type: object
|
* type: object
|
||||||
* 400:
|
* 400:
|
||||||
* description: Article is a father, has options, or contains itself
|
* description: Article is a parent, has options, or contains itself
|
||||||
* 404:
|
* 404:
|
||||||
* description: Article or component article not found
|
* description: Article or component article not found
|
||||||
*/
|
*/
|
||||||
@@ -2362,7 +2362,7 @@ export function register(app, basePath = '') {
|
|||||||
* /article/{sku}/optionslist:
|
* /article/{sku}/optionslist:
|
||||||
* put:
|
* put:
|
||||||
* summary: Set options list for an article
|
* summary: Set options list for an article
|
||||||
* description: Configures options for an article. Some options can be mandated (come with the article in any case). A father article cannot have options. Can be combined with a Stückliste. Pass an empty array to remove options.
|
* description: Configures options for an article. Some options can be mandated (come with the article in any case). A parent article cannot have options. Can be combined with a Stückliste. Pass an empty array to remove options.
|
||||||
* tags:
|
* tags:
|
||||||
* - Article
|
* - Article
|
||||||
* security:
|
* security:
|
||||||
@@ -2412,7 +2412,7 @@ export function register(app, basePath = '') {
|
|||||||
* items:
|
* items:
|
||||||
* type: object
|
* type: object
|
||||||
* 400:
|
* 400:
|
||||||
* description: Article is a father, a Stückliste, or contains itself
|
* description: Article is a parent, a Stückliste, or contains itself
|
||||||
* 404:
|
* 404:
|
||||||
* description: Article or option article not found
|
* description: Article or option article not found
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function articleAttributeValueErrorStatus(error) {
|
|||||||
if (error.message === 'Company not found' || error.message === 'Article not found') return 404;
|
if (error.message === 'Company not found' || error.message === 'Article not found') return 404;
|
||||||
if (error.message?.startsWith('Attribute value not found')) return 404;
|
if (error.message?.startsWith('Attribute value not found')) return 404;
|
||||||
if (error.message?.includes('exclusive')) return 400;
|
if (error.message?.includes('exclusive')) return 400;
|
||||||
if (error.message?.includes('father article') || error.message?.includes('Child article must have')) return 400;
|
if (error.message?.includes('parent article') || error.message?.includes('Child article must have')) return 400;
|
||||||
if (error.message === 'Invalid attribute values array' || error.message === 'Invalid attributeValueId' || error.message === 'Duplicate attributeValueId') return 400;
|
if (error.message === 'Invalid attribute values array' || error.message === 'Invalid attributeValueId' || error.message === 'Duplicate attributeValueId') return 400;
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export function register(app, basePath = '') {
|
|||||||
* get:
|
* get:
|
||||||
* tags: [Category]
|
* tags: [Category]
|
||||||
* summary: Articles in category
|
* summary: Articles in category
|
||||||
* description: Returns articles in a category the user is authorized for (omits child articles of a father article). Each article includes `childCount` (the number of its child articles).
|
* description: Returns articles in a category the user is authorized for (omits child articles of a parent article). Each article includes `childCount` (the number of its child articles).
|
||||||
* security:
|
* security:
|
||||||
* - cookieAuth: []
|
* - cookieAuth: []
|
||||||
* x-markdown: category/articles.md
|
* x-markdown: category/articles.md
|
||||||
@@ -396,11 +396,11 @@ export function register(app, basePath = '') {
|
|||||||
try {
|
try {
|
||||||
const allArticles = await db.getCategoryArticles(req.user.id, parsed.id);
|
const allArticles = await db.getCategoryArticles(req.user.id, parsed.id);
|
||||||
const articles = allArticles
|
const articles = allArticles
|
||||||
.filter((a) => a.fatherArticleId == null)
|
.filter((a) => a.parentArticleId == null)
|
||||||
.map((a) => ({
|
.map((a) => ({
|
||||||
...a,
|
...a,
|
||||||
childCount: Number(a.childCount ?? 0),
|
childCount: Number(a.childCount ?? 0),
|
||||||
isFather: Boolean(a.isFather ?? a.is_father),
|
isParent: Boolean(a.isParent ?? a.is_parent),
|
||||||
}));
|
}));
|
||||||
res.status(200).json({ articles });
|
res.status(200).json({ articles });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ function deliveryErrorStatus(error) {
|
|||||||
if (error.message === 'Invalid file') return 400;
|
if (error.message === 'Invalid file') return 400;
|
||||||
if (error.message === 'Invalid JSON in "data" field') return 400;
|
if (error.message === 'Invalid JSON in "data" field') return 400;
|
||||||
if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
|
if (error.message === 'Only PDF or text files are accepted as attachments') return 400;
|
||||||
if (error.message?.includes('father article') || error.message?.includes('A father article')) return 400;
|
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
|
||||||
if (
|
if (
|
||||||
error.message === 'Delivery in use in ledger' ||
|
error.message === 'Delivery in use in ledger' ||
|
||||||
error.message === 'Delivery lines already booked on ledger' ||
|
error.message === 'Delivery lines already booked on ledger' ||
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function orderErrorStatus(error) {
|
|||||||
if (error.message === 'Invalid entityId') return 400;
|
if (error.message === 'Invalid entityId') return 400;
|
||||||
if (error.message === 'Invalid order') return 400;
|
if (error.message === 'Invalid order') return 400;
|
||||||
if (error.message === 'Invalid lines') return 400;
|
if (error.message === 'Invalid lines') return 400;
|
||||||
if (error.message?.includes('father article') || error.message?.includes('A father article')) return 400;
|
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -318,9 +318,9 @@ export function register(app, basePath = '') {
|
|||||||
* type: array
|
* type: array
|
||||||
* items:
|
* items:
|
||||||
* type: string
|
* type: string
|
||||||
* isFather:
|
* isParent:
|
||||||
* type: boolean
|
* type: boolean
|
||||||
* fatherSku:
|
* parentSku:
|
||||||
* type: string
|
* type: string
|
||||||
* nullable: true
|
* nullable: true
|
||||||
* variants:
|
* variants:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function quoteErrorStatus(error) {
|
|||||||
if (error.message === 'Invalid entityId') return 400;
|
if (error.message === 'Invalid entityId') return 400;
|
||||||
if (error.message === 'Invalid quote') return 400;
|
if (error.message === 'Invalid quote') return 400;
|
||||||
if (error.message === 'Invalid lines') return 400;
|
if (error.message === 'Invalid lines') return 400;
|
||||||
if (error.message?.includes('father article') || error.message?.includes('A father article')) return 400;
|
if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400;
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function stockErrorStatus(error) {
|
|||||||
if (error.message?.includes('Amount must be')) return 400;
|
if (error.message?.includes('Amount must be')) return 400;
|
||||||
if (error.message?.includes('Invalid amount')) return 400;
|
if (error.message?.includes('Invalid amount')) return 400;
|
||||||
if (error.message === 'No items to adjust') return 400;
|
if (error.message === 'No items to adjust') return 400;
|
||||||
if (error.message?.includes('cannot have stock') || error.message?.includes('A father article cannot have') || error.message?.includes('father article') || error.message?.includes('Stückliste')) return 400;
|
if (error.message?.includes('cannot have stock') || error.message?.includes('A parent article cannot have') || error.message?.includes('parent article') || error.message?.includes('Stückliste')) return 400;
|
||||||
if (error.message?.includes('Stock tracking is disabled')) return 409;
|
if (error.message?.includes('Stock tracking is disabled')) return 409;
|
||||||
return 500;
|
return 500;
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export function register(app, basePath = '') {
|
|||||||
* schema:
|
* schema:
|
||||||
* $ref: '#/components/schemas/StockAdjustmentResult'
|
* $ref: '#/components/schemas/StockAdjustmentResult'
|
||||||
* 400:
|
* 400:
|
||||||
* description: Missing fields, invalid amount, father article cannot have stock, or no company selected
|
* description: Missing fields, invalid amount, parent article cannot have stock, or no company selected
|
||||||
* content:
|
* content:
|
||||||
* application/json:
|
* application/json:
|
||||||
* schema:
|
* schema:
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ export async function listExport(googleAuthId, categoryId) {
|
|||||||
return store.article.listExport(googleAuthId, categoryId);
|
return store.article.listExport(googleAuthId, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toggleFather(googleAuthId, sku, options) {
|
export async function toggleParent(googleAuthId, sku, options) {
|
||||||
const store = await getStore();
|
const store = await getStore();
|
||||||
return store.article.toggleFather(googleAuthId, sku, options);
|
return store.article.toggleParent(googleAuthId, sku, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addChild(googleAuthId, sku, options) {
|
export async function addChild(googleAuthId, sku, options) {
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export async function listArticles(companyId, { categoryId, limit = 50, offset =
|
|||||||
|
|
||||||
const whereClauses = [
|
const whereClauses = [
|
||||||
'a.company_id = @companyId',
|
'a.company_id = @companyId',
|
||||||
'a.fatherarticle_id IS NULL',
|
'a.parentarticle_id IS NULL',
|
||||||
];
|
];
|
||||||
if (categoryId != null) {
|
if (categoryId != null) {
|
||||||
whereClauses.push('ca.category_id = @categoryId');
|
whereClauses.push('ca.category_id = @categoryId');
|
||||||
@@ -280,9 +280,9 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
brand_pic.thumbw AS brandThumbW,
|
brand_pic.thumbw AS brandThumbW,
|
||||||
brand_pic.thumbh AS brandThumbH,
|
brand_pic.thumbh AS brandThumbH,
|
||||||
brand_pic.thumbhash AS brandThumbHash,
|
brand_pic.thumbhash AS brandThumbHash,
|
||||||
a.is_father AS isFather,
|
a.is_parent AS isParent,
|
||||||
a.fatherarticle_id AS fatherArticleId,
|
a.parentarticle_id AS parentArticleId,
|
||||||
father.sku AS fatherSku
|
parent.sku AS parentSku
|
||||||
FROM article a
|
FROM article a
|
||||||
LEFT JOIN articletext at
|
LEFT JOIN articletext at
|
||||||
ON at.article_id = a.id AND at.language_id = @languageId
|
ON at.article_id = a.id AND at.language_id = @languageId
|
||||||
@@ -297,7 +297,7 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
ORDER BY bp.sort, bp.id
|
ORDER BY bp.sort, bp.id
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)
|
)
|
||||||
LEFT JOIN article father ON father.id = a.fatherarticle_id
|
LEFT JOIN article parent ON parent.id = a.parentarticle_id
|
||||||
WHERE a.sku = @sku AND a.company_id = @companyId`
|
WHERE a.sku = @sku AND a.company_id = @companyId`
|
||||||
: `SELECT
|
: `SELECT
|
||||||
a.id,
|
a.id,
|
||||||
@@ -319,9 +319,9 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
brand_pic.thumbw AS brandThumbW,
|
brand_pic.thumbw AS brandThumbW,
|
||||||
brand_pic.thumbh AS brandThumbH,
|
brand_pic.thumbh AS brandThumbH,
|
||||||
brand_pic.thumbhash AS brandThumbHash,
|
brand_pic.thumbhash AS brandThumbHash,
|
||||||
a.is_father AS isFather,
|
a.is_parent AS isParent,
|
||||||
a.fatherarticle_id AS fatherArticleId,
|
a.parentarticle_id AS parentArticleId,
|
||||||
father.sku AS fatherSku
|
parent.sku AS parentSku
|
||||||
FROM article a
|
FROM article a
|
||||||
LEFT JOIN articletext at
|
LEFT JOIN articletext at
|
||||||
ON at.article_id = a.id AND at.language_id = @languageId
|
ON at.article_id = a.id AND at.language_id = @languageId
|
||||||
@@ -336,7 +336,7 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
ORDER BY bp.sort ASC, bp.id ASC
|
ORDER BY bp.sort ASC, bp.id ASC
|
||||||
) first_brand_pic
|
) first_brand_pic
|
||||||
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
|
LEFT JOIN pic brand_pic ON brand_pic.id = first_brand_pic.pic_id
|
||||||
LEFT JOIN article father ON father.id = a.fatherarticle_id
|
LEFT JOIN article parent ON parent.id = a.parentarticle_id
|
||||||
WHERE a.sku = @sku AND a.company_id = @companyId`;
|
WHERE a.sku = @sku AND a.company_id = @companyId`;
|
||||||
|
|
||||||
const result = await provider.query(sql, {
|
const result = await provider.query(sql, {
|
||||||
@@ -396,12 +396,12 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
ORDER BY pr.minmultiplier ASC`,
|
ORDER BY pr.minmultiplier ASC`,
|
||||||
{ articleId, posCustomerGroupId: company.posCustomerGroupId }
|
{ articleId, posCustomerGroupId: company.posCustomerGroupId }
|
||||||
),
|
),
|
||||||
row.isFather
|
row.isParent
|
||||||
? provider.query(
|
? provider.query(
|
||||||
`SELECT
|
`SELECT
|
||||||
child.id,
|
child.id,
|
||||||
child.sku,
|
child.sku,
|
||||||
child.fathersort,
|
child.parentsort,
|
||||||
child.track_stock AS trackStock,
|
child.track_stock AS trackStock,
|
||||||
child.unitamount,
|
child.unitamount,
|
||||||
child.weight,
|
child.weight,
|
||||||
@@ -413,8 +413,8 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId
|
LEFT JOIN articletext at_child ON at_child.article_id = child.id AND at_child.language_id = @languageId
|
||||||
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
|
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
|
||||||
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id
|
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||||
WHERE child.fatherarticle_id = @articleId
|
WHERE child.parentarticle_id = @articleId
|
||||||
ORDER BY child.fathersort ASC, child.id ASC`,
|
ORDER BY child.parentsort ASC, child.id ASC`,
|
||||||
{ articleId, languageId: company.languageId }
|
{ articleId, languageId: company.languageId }
|
||||||
)
|
)
|
||||||
: Promise.resolve({ rows: [] }),
|
: Promise.resolve({ rows: [] }),
|
||||||
@@ -480,12 +480,12 @@ export async function getArticleBySku(companyId, sku) {
|
|||||||
thumbHash: p.thumbHash,
|
thumbHash: p.thumbHash,
|
||||||
})),
|
})),
|
||||||
eans: eansRes.rows.map((e) => e.ean),
|
eans: eansRes.rows.map((e) => e.ean),
|
||||||
isFather: Boolean(row.isFather),
|
isParent: Boolean(row.isParent),
|
||||||
fatherSku: row.fatherSku ?? null,
|
parentSku: row.parentSku ?? null,
|
||||||
variants: variantsRes.rows.map((v) => ({
|
variants: variantsRes.rows.map((v) => ({
|
||||||
id: v.id,
|
id: v.id,
|
||||||
sku: v.sku,
|
sku: v.sku,
|
||||||
sort: v.fathersort,
|
sort: v.parentsort,
|
||||||
name: v.name ?? '',
|
name: v.name ?? '',
|
||||||
slug: v.slug ?? '',
|
slug: v.slug ?? '',
|
||||||
attributeValueId: v.attributeValueId ?? null,
|
attributeValueId: v.attributeValueId ?? null,
|
||||||
|
|||||||
2
todo.md
2
todo.md
@@ -9,7 +9,7 @@ Nichts wird gespeichert (verifiziert), daher ist es zwar kosmetisch, aber fehler
|
|||||||
Beobachtungen, keine Fehler
|
Beobachtungen, keine Fehler
|
||||||
PUT /artificlelist/ optionslistDoppelte Einträge ablehnen statt zusammenführen – vertretbar, aber nicht dokumentiert.
|
PUT /artificlelist/ optionslistDoppelte Einträge ablehnen statt zusammenführen – vertretbar, aber nicht dokumentiert.
|
||||||
Lange Werte werden von der Datenbank stillschweigend abgeschnitten, anstatt validiert zu werden (5000 Zeichen → 201 mit einer abgeschnittenen Zeile).
|
Lange Werte werden von der Datenbank stillschweigend abgeschnitten, anstatt validiert zu werden (5000 Zeichen → 201 mit einer abgeschnittenen Zeile).
|
||||||
POST /article/{sku}/childrenDie Dokumentation beschreibt ein optionales Element, skudas von der API jedoch immer abgelehnt wird sku is not allowed.... Der angegebene Pfad ist nicht funktionsfähig – um einen bestehenden Artikel einzubinden, ist eine direkte Verknüpfung erforderlich POST /article/{sku}/father, die in der Dokumentation nicht beschrieben wird. Eine Korrektur der Dokumentation oder eine zusätzliche Route wäre wünschenswert.
|
POST /article/{sku}/childrenDie Dokumentation beschreibt ein optionales Element, skudas von der API jedoch immer abgelehnt wird sku is not allowed.... Der angegebene Pfad ist nicht funktionsfähig – um einen bestehenden Artikel einzubinden, ist eine direkte Verknüpfung erforderlich POST /article/{sku}/parent, die in der Dokumentation nicht beschrieben wird. Eine Korrektur der Dokumentation oder eine zusätzliche Route wäre wünschenswert.
|
||||||
⚠️ Aufräumarbeiten unvollständig – Handlungsbedarf erforderlich
|
⚠️ Aufräumarbeiten unvollständig – Handlungsbedarf erforderlich
|
||||||
Mein Aufräumskript löste eine Fehlermeldung aus GET /articleIndex( windowSize must be an integer between 1 and 1000; ich habe 100000 übergeben), bevor irgendwelche Löschvorgänge durchgeführt wurden. Es wurde nichts entfernt. Zurück blieb:
|
Mein Aufräumskript löste eine Fehlermeldung aus GET /articleIndex( windowSize must be an integer between 1 and 1000; ich habe 100000 übergeben), bevor irgendwelche Löschvorgänge durchgeführt wurden. Es wurde nichts entfernt. Zurück blieb:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user