feat(schema): add articlesupplier table and manifestarticle custom sku/name fields

This commit is contained in:
seb
2026-09-14 22:43:03 +02:00
parent af416246b4
commit 2e16a22c99
9 changed files with 1708 additions and 1517 deletions

View File

@@ -285,6 +285,22 @@ Suppliers are `entity` rows in the company `customergroup` named `Lieferanten` (
--- ---
## `articlesupplier`
Maps articles to suppliers (`entity`) with purchase pricing, stock, packaging unit, and supplier SKU:
| Column | Type | Constraints / Description |
| --- | --- | --- |
| `id` | `INT` | Primary Key (`IDENTITY(1,1)`) |
| `article_id` | `INT` | FK → `article(id)` (`ON DELETE CASCADE`) |
| `entity_id` | `INT` | FK → `entity(id)` (`ON DELETE CASCADE`) |
| `price_id` | `INT` | FK → `price(id)` (`ON DELETE NO ACTION`), nullable |
| `SupplierSKU` | `NVARCHAR(100)` | Supplier's SKU code, nullable |
| `SupplierStock` | `FLOAT` | Stock available at supplier, nullable |
| `pu` | `FLOAT` | Packaging unit (Verpackungseinheit), default `1` |
---
## `delivery` ## `delivery`
Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type = 'delivery'`. Auth via selected company (`google_auth.selected_company` + membership). `manifest.company_id` scopes the list (`ON DELETE NO ACTION` — avoid SQL Server multiple cascade paths with `entity_id`). Company-scoped goods receipts on `manifest` / `manifestarticle` with fixed `type = 'delivery'`. Auth via selected company (`google_auth.selected_company` + membership). `manifest.company_id` scopes the list (`ON DELETE NO ACTION` — avoid SQL Server multiple cascade paths with `entity_id`).
@@ -311,6 +327,8 @@ type DeliveryLine = {
id: number, id: number,
articleId: number | null, articleId: number | null,
sku: string | null, sku: string | null,
customsku: string | null,
customname: string | null,
amount: number, amount: number,
net: number, net: number,
rebate: number, rebate: number,
@@ -321,7 +339,7 @@ type DeliveryLine = {
type Delivery = DeliverySummary & { lines: DeliveryLine[] } type Delivery = DeliverySummary & { lines: DeliveryLine[] }
``` ```
`create` inserts header + lines in one transaction. The `number` is auto-allocated via `getnextnumberwithprefixandpadding` (`numbers` type `delivery`); supply `externalNumber` for an external (supplier) reference. `supplierId` null or a Lieferanten `entity` of the company (else `Supplier not found`). Each line requires `sku` (resolved in the company category tree → `Article not found`), `amount`, `net`, `tax` (rate → `tax.ensure`); `rebate` defaults `0`. Empty/invalid `date` → `Invalid delivery`; bad line payload → `Invalid lines`. `create` inserts header + lines in one transaction. The `number` is auto-allocated via `getnextnumberwithprefixandpadding` (`numbers` type `delivery`); supply `externalNumber` for an external (supplier) reference. `supplierId` null or a Lieferanten `entity` of the company (else `Supplier not found`). Each line requires either `sku` (resolved in the company category tree → `Article not found`; `customsku` and `customname` forced `null`) or custom line fields (`customsku` / `customname` with `articleId` set to `null`). If `article_id` is set, `customsku` and `customname` must be null (`CK_manifestarticle_article_or_custom`). `amount`, `net`, `tax` (rate → `tax.ensure`); `rebate` defaults `0`. Empty/invalid `date` → `Invalid delivery`; bad line payload → `Invalid lines`.
`update` patches header fields that are present. If `lines` is present, replace-all lines; if omitted, leave lines unchanged. Returns full `Delivery`. `update` patches header fields that are present. If `lines` is present, replace-all lines; if omitted, leave lines unchanged. Returns full `Delivery`.
@@ -635,7 +653,7 @@ Auth via the article's `categoryarticle` category + `resolveCategoryAccess`. Err
`orderImages` sets `articlepic.sort` to `0..n-1` in the given order. `fullHashes` must list every image of the article exactly once. Errors: `Article not found`, `Invalid image order`, `Not authorized`. `orderImages` sets `articlepic.sort` to `0..n-1` in the given order. `fullHashes` must list every image of the article exactly once. Errors: `Article not found`, `Invalid image order`, `Not authorized`.
`delete` removes the article when it has no child articles (`fatherarticle_id` FK is `ON DELETE NO ACTION`). Cascades: `articlehistory`, `articleean`, `categoryarticle`, `price`, `articletext`, `articlelistarticle`, `optionslistarticle`, `articlepic`; deletes linked `pic` rows and owned `articlelist` / `optionslist`. `manifestarticle.article_id` is set to `NULL` and `comment` gets `article <name> deleted` appended. Errors: `Article not found`, `Article has children`, `Not authorized`. `delete` removes the article when it has no child articles (`fatherarticle_id` FK is `ON DELETE NO ACTION`). Cascades: `articlehistory`, `articleean`, `categoryarticle`, `price`, `articlesupplier`, `articletext`, `articlelistarticle`, `optionslistarticle`, `articlepic`; deletes linked `pic` rows and owned `articlelist` / `optionslist`. `manifestarticle.article_id` is set to `NULL` and `comment` gets `article <name> deleted` appended. Errors: `Article not found`, `Article has children`, `Not authorized`.
--- ---

View File

@@ -355,6 +355,7 @@ const CLUSTERS = [
'articlelistarticle', 'articlelistarticle',
'optionslist', 'optionslist',
'optionslistarticle', 'optionslistarticle',
'articlesupplier',
], ],
}, },
{ {
@@ -368,6 +369,7 @@ const CLUSTERS = [
tables: ['tax', 'price', 'manifest', 'manifestarticle', 'ledger'], tables: ['tax', 'price', 'manifest', 'manifestarticle', 'ledger'],
}, },
{ id: 'units', label: 'Units', tables: ['unit'] }, { id: 'units', label: 'Units', tables: ['unit'] },
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory'] },
]; ];
const COLORS = { const COLORS = {

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: 187 KiB

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -528,6 +528,27 @@ BEGIN
END END
`; `;
export const INIT_ARTICLE_SUPPLIER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articlesupplier')
BEGIN
CREATE TABLE articlesupplier (
id INT IDENTITY(1,1) PRIMARY KEY,
article_id INT NOT NULL,
entity_id INT NOT NULL,
price_id INT NULL,
SupplierSKU NVARCHAR(100) NULL,
SupplierStock FLOAT NULL,
pu FLOAT NOT NULL CONSTRAINT DF_articlesupplier_pu DEFAULT 1,
CONSTRAINT FK_articlesupplier_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE CASCADE,
CONSTRAINT FK_articlesupplier_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE,
CONSTRAINT FK_articlesupplier_price FOREIGN KEY (price_id) REFERENCES price(id) ON DELETE NO ACTION
);
CREATE INDEX IX_articlesupplier_article_entity
ON articlesupplier (article_id, entity_id);
END
`;
export const INIT_MANIFEST = ` export const INIT_MANIFEST = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifest') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'manifest')
BEGIN BEGIN
@@ -552,6 +573,8 @@ BEGIN
id INT IDENTITY(1,1) PRIMARY KEY, id INT IDENTITY(1,1) PRIMARY KEY,
manifest_id INT NOT NULL, manifest_id INT NOT NULL,
article_id INT NULL, article_id INT NULL,
customsku NVARCHAR(100) NULL,
customname NVARCHAR(255) NULL,
amount FLOAT NOT NULL, amount FLOAT NOT NULL,
net FLOAT NOT NULL, net FLOAT NOT NULL,
comment NVARCHAR(MAX) NULL, comment NVARCHAR(MAX) NULL,
@@ -559,9 +582,41 @@ BEGIN
tax_id INT NOT NULL, tax_id INT NOT NULL,
CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE, CONSTRAINT FK_manifestarticle_manifest FOREIGN KEY (manifest_id) REFERENCES manifest(id) ON DELETE CASCADE,
CONSTRAINT FK_manifestarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE SET NULL, CONSTRAINT FK_manifestarticle_article FOREIGN KEY (article_id) REFERENCES article(id) ON DELETE SET NULL,
CONSTRAINT FK_manifestarticle_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE CASCADE CONSTRAINT FK_manifestarticle_tax FOREIGN KEY (tax_id) REFERENCES tax(id) ON DELETE CASCADE,
CONSTRAINT CK_manifestarticle_article_or_custom CHECK (
(article_id IS NOT NULL AND customsku IS NULL AND customname IS NULL)
OR (article_id IS NULL)
)
); );
END END
IF NOT EXISTS (
SELECT 1 FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.manifestarticle') AND name = N'customsku'
)
BEGIN
EXEC('ALTER TABLE dbo.manifestarticle ADD customsku NVARCHAR(100) NULL');
END
IF NOT EXISTS (
SELECT 1 FROM sys.columns
WHERE object_id = OBJECT_ID(N'dbo.manifestarticle') AND name = N'customname'
)
BEGIN
EXEC('ALTER TABLE dbo.manifestarticle ADD customname NVARCHAR(255) NULL');
END
IF NOT EXISTS (
SELECT 1 FROM sys.check_constraints
WHERE name = N'CK_manifestarticle_article_or_custom'
)
BEGIN
EXEC('ALTER TABLE dbo.manifestarticle ADD CONSTRAINT CK_manifestarticle_article_or_custom
CHECK (
(article_id IS NOT NULL AND customsku IS NULL AND customname IS NULL)
OR (article_id IS NULL)
)');
END
`; `;
export const INIT_LEDGER = ` export const INIT_LEDGER = `
@@ -773,6 +828,7 @@ export const SCHEMA_SCRIPTS = [
INIT_CATEGORYTEXT, INIT_CATEGORYTEXT,
INIT_ARTICLENAME, INIT_ARTICLENAME,
INIT_PRICE, INIT_PRICE,
INIT_ARTICLE_SUPPLIER,
INIT_MANIFEST, INIT_MANIFEST,
INIT_MANIFESTARTICLE, INIT_MANIFESTARTICLE,
INIT_LEDGER, INIT_LEDGER,

View File

@@ -33,6 +33,8 @@ SELECT
ma.id, ma.id,
ma.article_id AS articleId, ma.article_id AS articleId,
a.sku, a.sku,
ma.customsku,
ma.customname,
ma.amount, ma.amount,
ma.net, ma.net,
ma.rebate, ma.rebate,
@@ -79,6 +81,8 @@ export const CREATE_LINE = `
INSERT INTO manifestarticle ( INSERT INTO manifestarticle (
manifest_id, manifest_id,
article_id, article_id,
customsku,
customname,
amount, amount,
net, net,
comment, comment,
@@ -89,6 +93,8 @@ OUTPUT INSERTED.id
VALUES ( VALUES (
@manifestId, @manifestId,
@articleId, @articleId,
@customsku,
@customname,
@amount, @amount,
@net, @net,
@comment, @comment,

View File

@@ -39,6 +39,8 @@ SELECT
ma.id, ma.id,
ma.article_id AS articleId, ma.article_id AS articleId,
a.sku, a.sku,
ma.customsku,
ma.customname,
ma.amount, ma.amount,
ma.net, ma.net,
ma.rebate, ma.rebate,
@@ -85,6 +87,8 @@ export const CREATE_LINE = `
INSERT INTO manifestarticle ( INSERT INTO manifestarticle (
manifest_id, manifest_id,
article_id, article_id,
customsku,
customname,
amount, amount,
net, net,
comment, comment,
@@ -95,6 +99,8 @@ OUTPUT INSERTED.id
VALUES ( VALUES (
@manifestId, @manifestId,
@articleId, @articleId,
@customsku,
@customname,
@amount, @amount,
@net, @net,
@comment, @comment,

View File

@@ -7,6 +7,7 @@ export const TABLES = [
'posterminal', 'posterminal',
'numbers', 'numbers',
'ledger', 'ledger',
'articlesupplier',
'manifestarticle', 'manifestarticle',
'manifest', 'manifest',
'price', 'price',

View File

@@ -5,7 +5,7 @@ import request from './request.js';
* Normalizes and validates an array of manifest line inputs. * Normalizes and validates an array of manifest line inputs.
* *
* @param {Array<object>} lines - The raw line inputs. * @param {Array<object>} lines - The raw line inputs.
* @returns {Array<{sku: string, amount: number, net: number, rebate: number, tax: number, comment: string|null}>} The normalized lines. * @returns {Array<{sku: string|null, customsku: string|null, customname: string|null, amount: number, net: number, rebate: number, tax: number, comment: string|null}>} The normalized lines.
* @throws {Error} If the lines are invalid. * @throws {Error} If the lines are invalid.
*/ */
export function normalizeLines(lines) { export function normalizeLines(lines) {
@@ -15,7 +15,16 @@ export function normalizeLines(lines) {
return lines.map((entry) => { return lines.map((entry) => {
const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null; const sku = entry?.sku?.trim?.() ? entry.sku.trim() : null;
if (!sku) { let customsku = entry?.customsku?.trim?.() ? entry.customsku.trim() : null;
let customname =
entry?.customname?.trim?.() ? entry.customname.trim() : null;
// If an article SKU is present, the custom fields must be null.
if (sku) {
customsku = null;
customname = null;
} else if (!customsku && !customname) {
// Must have either a catalog SKU or at least one custom field.
throw new Error('Invalid lines'); throw new Error('Invalid lines');
} }
@@ -38,7 +47,7 @@ export function normalizeLines(lines) {
throw new Error('Invalid lines'); throw new Error('Invalid lines');
} }
return { sku, amount, net, rebate, tax, comment }; return { sku, customsku, customname, amount, net, rebate, tax, comment };
}); });
} }
@@ -72,6 +81,8 @@ export function shapeLine(row) {
id: row.id, id: row.id,
articleId: row.articleId ?? null, articleId: row.articleId ?? null,
sku: row.sku ?? null, sku: row.sku ?? null,
customsku: row.customsku ?? null,
customname: row.customname ?? null,
amount: row.amount, amount: row.amount,
net: row.net, net: row.net,
rebate: row.rebate, rebate: row.rebate,
@@ -112,17 +123,16 @@ export async function insertLines(
{ manifestId, rootCategoryId, lines, ensureTax }, { manifestId, rootCategoryId, lines, ensureTax },
) { ) {
for (const line of lines) { for (const line of lines) {
const articleId = await resolveArticleId( const articleId = line.sku
pool, ? await resolveArticleId(pool, tx, rootCategoryId, line.sku)
tx, : null;
rootCategoryId,
line.sku,
);
const taxId = await ensureTax(line.tax, tx); const taxId = await ensureTax(line.tax, tx);
await request(pool, tx) await request(pool, tx)
.input('manifestId', manifestId) .input('manifestId', manifestId)
.input('articleId', articleId) .input('articleId', articleId)
.input('customsku', line.sku ? null : line.customsku ?? null)
.input('customname', line.sku ? null : line.customname ?? null)
.input('amount', line.amount) .input('amount', line.amount)
.input('net', line.net) .input('net', line.net)
.input('comment', line.comment) .input('comment', line.comment)