This commit is contained in:
seb
2026-10-01 17:40:04 +02:00
parent 62c3e69ed4
commit 84e547660e
53 changed files with 4604 additions and 2059 deletions

View File

@@ -113,6 +113,7 @@ import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEnti
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
import posterminalGetProductList from './store/posterminal/getProductList.js';
import posterminalSetMaxKnownTombstoneId from './store/posterminal/setMaxKnownTombstoneId.js';
import langCreate from './store/lang/create.js';
import langDelete from './store/lang/delete.js';
import langList from './store/lang/list.js';
@@ -181,6 +182,11 @@ import attributevalueOrder from './store/attributevalue/order.js';
import attributevalueDelete from './store/attributevalue/delete.js';
import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js';
import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js';
import categorypropertyListByCategoryId from './store/categoryproperty/listByCategoryId.js';
import categorypropertySetByCategoryId from './store/categoryproperty/setByCategoryId.js';
import categorypropertyDelete from './store/categoryproperty/delete.js';
import categoryattributevalueListByCategoryId from './store/categoryattributevalue/listByCategoryId.js';
import categoryattributevalueSetByCategoryId from './store/categoryattributevalue/setByCategoryId.js';
import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
@@ -388,6 +394,7 @@ export default class LibDashStore {
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
getCategoryList: posterminalGetCategoryList(provider),
getProductList: posterminalGetProductList(provider),
setMaxKnownTombstoneId: posterminalSetMaxKnownTombstoneId(provider),
};
this.google_auth = {
@@ -490,6 +497,17 @@ export default class LibDashStore {
setByArticleSku: articleattributevalueSetByArticleSku(provider),
};
this.categoryproperty = {
listByCategoryId: categorypropertyListByCategoryId(provider),
setByCategoryId: categorypropertySetByCategoryId(provider),
delete: categorypropertyDelete(provider),
};
this.categoryattributevalue = {
listByCategoryId: categoryattributevalueListByCategoryId(provider),
setByCategoryId: categoryattributevalueSetByCategoryId(provider),
};
this.articlelist = {
getItemsByArticleSku: articlelistGetItems(provider),
setItemsByArticleSku: articlelistSetItems(provider),

View File

@@ -17,6 +17,29 @@ WHERE id = @articleId
`;
export const CREATE_TOMBSTONE = `
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
SELECT
a.company_id,
a.id AS article_id,
a.sku,
COALESCE(
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC)
) AS name,
COALESCE(
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS taxrate,
COALESCE(
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC)
) AS net
FROM article a
INNER JOIN company co ON co.id = a.company_id
WHERE a.id = @articleId
`;
export const DELETE_PICS_BY_ARTICLE = `
DELETE p
FROM pic p

View File

@@ -9,3 +9,10 @@ FROM articlehistory
WHERE article_id = @articleId
ORDER BY [date] DESC, id DESC
`;
export const LIST_BY_ARTICLE_ID_LIMITED = `
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text]
FROM articlehistory
WHERE article_id = @articleId
ORDER BY [date] DESC, id DESC
`;

View File

@@ -8,3 +8,13 @@ UPDATE articletext
SET name = @name, slug = @slug
WHERE article_id = @articleId AND language_id = @languageId
`;
export const LIST_SLUGS_BY_COMPANY_AND_LANGUAGE = `
SELECT at.slug, at.article_id AS articleId
FROM articletext at
INNER JOIN article a ON a.id = at.article_id
WHERE a.company_id = @companyId
AND at.language_id = @languageId
AND (at.slug = @baseSlug OR at.slug LIKE @likePattern)
`;

View File

@@ -0,0 +1,41 @@
export const LIST_BY_CATEGORY_ID = `
SELECT
cav.id,
cav.category_id AS categoryId,
an.id AS attributeId,
an.name AS attributeName,
an.exclusive,
av.id AS attributeValueId,
av.value,
cav.sort
FROM categoryattributevalue cav
INNER JOIN attributevalue av ON av.id = cav.attributevalue_id
INNER JOIN attributename an ON an.id = av.attribute_id
WHERE cav.category_id = @categoryId
ORDER BY an.sort ASC, an.id ASC, cav.sort ASC, cav.id ASC
`;
export const DELETE_BY_CATEGORY_ID = `
DELETE FROM categoryattributevalue
WHERE category_id = @categoryId
`;
export const INSERT_ROW = `
INSERT INTO categoryattributevalue (category_id, attributevalue_id, sort)
OUTPUT INSERTED.id, INSERTED.category_id AS categoryId, INSERTED.attributevalue_id AS attributeValueId, INSERTED.sort
VALUES (@categoryId, @attributeValueId, @sort)
`;
export const GET_VALUE_DETAIL = `
SELECT
av.id AS attributeValueId,
av.value,
av.sort,
an.id AS attributeId,
an.name AS attributeName,
an.exclusive,
an.company_id AS companyId
FROM attributevalue av
INNER JOIN attributename an ON an.id = av.attribute_id
WHERE av.id = @attributeValueId
`;

View File

@@ -0,0 +1,30 @@
export const LIST_BY_CATEGORY_ID = `
SELECT
cp.id,
cp.category_id AS categoryId,
cp.property_id AS propertyId,
pn.name AS name,
pn.exclusive,
cp.value,
cp.sort
FROM categoryproperty cp
INNER JOIN propertyname pn ON pn.id = cp.property_id
WHERE cp.category_id = @categoryId
ORDER BY cp.sort ASC, cp.id ASC
`;
export const DELETE_BY_CATEGORY_ID = `
DELETE FROM categoryproperty
WHERE category_id = @categoryId
`;
export const DELETE_BY_CATEGORY_AND_PROPERTY_ID = `
DELETE FROM categoryproperty
WHERE category_id = @categoryId AND property_id = @propertyId
`;
export const INSERT_ROW = `
INSERT INTO categoryproperty (category_id, property_id, value, sort)
OUTPUT INSERTED.id, INSERTED.category_id AS categoryId, INSERTED.property_id AS propertyId, INSERTED.value, INSERTED.sort
VALUES (@categoryId, @propertyId, @value, @sort)
`;

View File

@@ -108,3 +108,28 @@ VALUES (
@taxrate
)
`;
export const CHECK_ARTICLE_IS_FATHER = `
SELECT
a.id,
a.is_father AS isFather,
COALESCE(
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id),
a.sku
) AS articleName,
COALESCE(
(SELECT MIN(asup.pu) FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku),
(SELECT MIN(asup2.pu) FROM articlesupplier asup2 WHERE asup2.article_id = a.id),
a.unitamount,
1
) AS pu
FROM article a
INNER JOIN manifest m ON m.company_id = a.company_id
INNER JOIN company co ON co.id = m.company_id
WHERE m.id = @manifestId
AND (a.sku = @sku OR EXISTS (
SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku
))
`;

View File

@@ -5,7 +5,8 @@ SELECT
token,
pairingpin,
paired_at AS pairedAt,
clientserial AS clientserial
clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid
FROM posterminal
WHERE company_id = @companyId
ORDER BY id
@@ -19,7 +20,8 @@ SELECT
token,
pairingpin,
paired_at AS pairedAt,
clientserial AS clientserial
clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid
FROM posterminal
WHERE id = @id
`;
@@ -32,7 +34,8 @@ SELECT
token,
pairingpin,
paired_at AS pairedAt,
clientserial AS clientserial
clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid
FROM posterminal
WHERE pairingpin = @pairingpin
`;
@@ -44,17 +47,23 @@ WHERE id = @id
`;
export const CREATE = `
INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial)
OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial
VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial)
INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial, maxknowntombstoneid)
OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial, INSERTED.maxknowntombstoneid AS maxknowntombstoneid
VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial, @maxknowntombstoneid)
`;
export const UPDATE = `
UPDATE posterminal
SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial
SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknowntombstoneid = @maxknowntombstoneid
WHERE id = @id AND company_id = @companyId
`;
export const SET_MAX_KNOWN_TOMBSTONE_ID = `
UPDATE posterminal
SET maxknowntombstoneid = @maxknowntombstoneid
WHERE id = @id
`;
export const DELETE_BY_COMPANY_AND_ID = `
DELETE FROM posterminal
WHERE id = @id AND company_id = @companyId
@@ -122,7 +131,8 @@ SELECT
token,
pairingpin,
paired_at AS pairedAt,
clientserial AS clientserial
clientserial AS clientserial,
maxknowntombstoneid AS maxknowntombstoneid
FROM posterminal
WHERE token = @token
`;

View File

@@ -352,10 +352,11 @@ BEGIN
ALTER TABLE article ADD fatherattributename_id INT NULL;
ALTER TABLE article ADD CONSTRAINT FK_article_fatherattributename FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
END
END
ELSE IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
BEGIN
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
BEGIN
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
END
END
`;
@@ -374,6 +375,27 @@ BEGIN
END
`;
export const INIT_ARTICLETOMBSTONE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
BEGIN
CREATE TABLE articletombstone (
id INT IDENTITY(1,1) PRIMARY KEY,
company_id INT NOT NULL,
article_id INT NULL,
sku NVARCHAR(100) NULL,
name NVARCHAR(255) NULL,
taxrate DECIMAL(18,4) NULL,
net FLOAT NULL,
deleted_at DATETIME2 NOT NULL CONSTRAINT DF_articletombstone_deleted_at DEFAULT SYSUTCDATETIME(),
CONSTRAINT CK_articletombstone_article_or_sku CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
CONSTRAINT FK_articletombstone_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IX_articletombstone_company_deleted
ON articletombstone (company_id, deleted_at);
END
`;
export const INIT_ARTICLEEAN = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
BEGIN
@@ -791,10 +813,18 @@ BEGIN
pairingpin NVARCHAR(50) NOT NULL,
paired_at DATETIME2 NULL,
clientserial NVARCHAR(255) NULL,
maxknowntombstoneid INT NULL,
CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX UQ_posterminal_token ON posterminal(token) WHERE token IS NOT NULL;
END
ELSE
BEGIN
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('posterminal') AND name = 'maxknowntombstoneid')
BEGIN
ALTER TABLE posterminal ADD maxknowntombstoneid INT NULL;
END
END
`;
export const INIT_POSTERMINALCATEGORY = `
@@ -980,6 +1010,36 @@ BEGIN
END
`;
export const INIT_CATEGORYPROPERTY = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryproperty')
BEGIN
CREATE TABLE categoryproperty (
id INT IDENTITY(1,1) PRIMARY KEY,
category_id INT NOT NULL,
property_id INT NOT NULL,
value NVARCHAR(MAX) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_categoryproperty_sort DEFAULT 0,
CONSTRAINT FK_categoryproperty_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
CONSTRAINT FK_categoryproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
);
END
`;
export const INIT_CATEGORYATTRIBUTEVALUE = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryattributevalue')
BEGIN
CREATE TABLE categoryattributevalue (
id INT IDENTITY(1,1) PRIMARY KEY,
category_id INT NOT NULL,
attributevalue_id INT NOT NULL,
sort INT NOT NULL CONSTRAINT DF_categoryattributevalue_sort DEFAULT 0,
CONSTRAINT FK_categoryattributevalue_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
CONSTRAINT FK_categoryattributevalue_attributevalue FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
CONSTRAINT UQ_categoryattributevalue_category_attributevalue UNIQUE (category_id, attributevalue_id)
);
END
`;
/** DDL scripts in dependency order for schema init. */
export const SCHEMA_SCRIPTS = [
INIT_META,
@@ -1017,6 +1077,8 @@ export const SCHEMA_SCRIPTS = [
INIT_ARTICLE_SUPPLIER,
INIT_ARTICLEPROPERTY,
INIT_ARTICLEATTRIBUTEVALUE,
INIT_CATEGORYPROPERTY,
INIT_CATEGORYATTRIBUTEVALUE,
INIT_MANIFEST,
INIT_MANIFESTARTICLE,
INIT_WAREHOUSE,
@@ -1026,6 +1088,7 @@ export const SCHEMA_SCRIPTS = [
INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_COMPANYUSER,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,
INIT_SYNCPOS,
INIT_COMPANYSETTING,

View File

@@ -426,6 +426,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
if (posterminalCols.length > 0 && !posterminalCols.includes('clientserial')) {
this.db.exec(`ALTER TABLE posterminal ADD COLUMN clientserial TEXT`);
}
if (posterminalCols.length > 0 && !posterminalCols.includes('maxknowntombstoneid')) {
this.db.exec(`ALTER TABLE posterminal ADD COLUMN maxknowntombstoneid INTEGER`);
}
const propertynameCols = this.db.pragma('table_info("propertyname")').map((c) => c.name);
if (propertynameCols.length > 0 && !propertynameCols.includes('exclusive')) {

View File

@@ -7,6 +7,29 @@ export const {
GET_CHILDREN_IDS, GET_LIST_IDS,
} = queries;
export const CREATE_TOMBSTONE = `
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
SELECT
a.company_id,
a.id AS article_id,
a.sku,
COALESCE(
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
(SELECT at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC LIMIT 1)
) AS name,
COALESCE(
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
) AS taxrate,
COALESCE(
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
) AS net
FROM article a
INNER JOIN company co ON co.id = a.company_id
WHERE a.id = @articleId
`;
export const DELETE_PICS_BY_ARTICLE = `
DELETE FROM pic
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)

View File

@@ -2,4 +2,12 @@ import * as source from '../../mssql/queries/articlehistory.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'articlehistory');
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
export const LIST_BY_ARTICLE_ID_LIMITED = `
SELECT id, companyuser_id AS companyuserId, date, text
FROM articlehistory
WHERE article_id = @articleId
ORDER BY date DESC, id DESC
LIMIT @limit
`;

View File

@@ -2,4 +2,8 @@ import * as source from '../../mssql/queries/articletext.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'articletext');
export const { CREATE, UPDATE_BY_ARTICLE_AND_LANGUAGE } = queries;
export const {
CREATE,
UPDATE_BY_ARTICLE_AND_LANGUAGE,
LIST_SLUGS_BY_COMPANY_AND_LANGUAGE,
} = queries;

View File

@@ -0,0 +1,10 @@
import * as source from '../../mssql/queries/categoryattributevalue.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'categoryattributevalue');
export const {
LIST_BY_CATEGORY_ID,
DELETE_BY_CATEGORY_ID,
INSERT_ROW,
GET_VALUE_DETAIL,
} = queries;

View File

@@ -0,0 +1,10 @@
import * as source from '../../mssql/queries/categoryproperty.js';
import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'categoryproperty');
export const {
LIST_BY_CATEGORY_ID,
DELETE_BY_CATEGORY_ID,
DELETE_BY_CATEGORY_AND_PROPERTY_ID,
INSERT_ROW,
} = queries;

View File

@@ -3,7 +3,7 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'manifestDoc');
export const {
CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST, DELETE_MANIFEST,
GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE,
QUOTE_TYPE, UPDATE_MANIFEST,
CHECK_ARTICLE_IS_FATHER, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
DELETE_MANIFEST, GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS,
ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST,
} = queries;

View File

@@ -5,7 +5,7 @@ const queries = translateQueries(source, 'posterminal');
export const {
CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY,
LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF, SET_OFF_ALL, UPDATE,
FIND_BY_PAIRINGPIN, PAIR,
FIND_BY_PAIRINGPIN, PAIR, SET_MAX_KNOWN_TOMBSTONE_ID,
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,

View File

@@ -495,6 +495,23 @@ CREATE TABLE IF NOT EXISTS articlehistory (
);
`;
export const INIT_ARTICLETOMBSTONE = `
CREATE TABLE IF NOT EXISTS articletombstone (
id INTEGER PRIMARY KEY,
company_id INTEGER NOT NULL,
article_id INTEGER,
sku TEXT COLLATE NOCASE,
name TEXT,
taxrate NUMERIC,
net REAL,
deleted_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS IX_articletombstone_company_deleted
ON articletombstone (company_id, deleted_at);
`;
export const INIT_ENTITYHISTORY = `
CREATE TABLE IF NOT EXISTS entityhistory (
id INTEGER PRIMARY KEY,
@@ -539,6 +556,7 @@ CREATE TABLE IF NOT EXISTS posterminal (
pairingpin TEXT NOT NULL,
paired_at TEXT,
clientserial TEXT,
maxknowntombstoneid INTEGER,
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS UQ_posterminal_token
@@ -684,6 +702,30 @@ CREATE TABLE IF NOT EXISTS articleattributevalue (
);
`;
export const INIT_CATEGORYPROPERTY = `
CREATE TABLE IF NOT EXISTS categoryproperty (
id INTEGER PRIMARY KEY,
category_id INTEGER NOT NULL,
property_id INTEGER NOT NULL,
value TEXT NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
);
`;
export const INIT_CATEGORYATTRIBUTEVALUE = `
CREATE TABLE IF NOT EXISTS categoryattributevalue (
id INTEGER PRIMARY KEY,
category_id INTEGER NOT NULL,
attributevalue_id INTEGER NOT NULL,
sort INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
UNIQUE (category_id, attributevalue_id)
);
`;
/** DDL scripts in dependency order for SQLite schema initialization. */
export const SCHEMA_SCRIPTS = [
INIT_META,
@@ -720,6 +762,8 @@ export const SCHEMA_SCRIPTS = [
INIT_ARTICLE_SUPPLIER,
INIT_ARTICLEPROPERTY,
INIT_ARTICLEATTRIBUTEVALUE,
INIT_CATEGORYPROPERTY,
INIT_CATEGORYATTRIBUTEVALUE,
INIT_MANIFEST,
INIT_MANIFESTARTICLE,
INIT_WAREHOUSE,
@@ -727,6 +771,7 @@ export const SCHEMA_SCRIPTS = [
INIT_NUMBERS,
INIT_COMPANYUSER,
INIT_ARTICLEHISTORY,
INIT_ARTICLETOMBSTONE,
INIT_ENTITYHISTORY,
INIT_SYNCPOS,
INIT_COMPANYSETTING,

View File

@@ -15,11 +15,12 @@ import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries
import resolveArticleBySku from './resolveArticleBySku.js';
import getBySku from './getBySku.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
export default function addChild(provider) {
const resolve = resolveArticleBySku(provider);
const load = getBySku(provider);
const resolveSlug = resolveUniqueSlug(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteArticle : mssqlArticle;
@@ -117,10 +118,46 @@ export default function addChild(provider) {
throw new Error('Attribute value is required for child article');
}
// Compute child SKU: father.sku + '-' + finalValueId
const childSku = `${father.sku}-${finalValueId}`;
// Check if father already has a variant with this attribute value
const existingVariantRes = await runner.query(
`SELECT a.id
FROM article a
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
WHERE a.fatherarticle_id = @fatherId
AND aav.attributevalue_id = @finalValueId`,
{ fatherId, finalValueId },
);
if (existingVariantRes.rows.length > 0) {
throw new Error('Variant with this attribute value already exists');
}
// Check if child SKU already exists
// Compute child SKU:
// If options.sku is provided, use it.
// Otherwise, derive using next free index: fatherSku-1, fatherSku-2, ...
let childSku = (typeof options.sku === 'string' && options.sku.trim()) || null;
if (!childSku) {
const existingSkusRes = await runner.query(
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
{ companyId, prefix: `${father.sku}-%` },
);
const usedIndices = new Set();
const prefix = `${father.sku}-`;
for (const row of existingSkusRes.rows) {
if (row.sku && row.sku.startsWith(prefix)) {
const numPart = row.sku.slice(prefix.length);
if (/^\d+$/.test(numPart)) {
usedIndices.add(parseInt(numPart, 10));
}
}
}
let nextIndex = 1;
while (usedIndices.has(nextIndex)) {
nextIndex++;
}
childSku = `${father.sku}-${nextIndex}`;
}
// Check if child SKU already exists (e.g. orphan holding SKU or custom SKU)
const existingChildRes = await runner.query(
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
{ childSku, companyId },
@@ -129,9 +166,16 @@ export default function addChild(provider) {
throw new Error(`Child article "${childSku}" already exists`);
}
// Next fathersort
const countRes = await runner.query(getChildCountSql, { articleId: fatherId });
const fathersort = countRes.rows[0]?.count ?? 0;
// Next free fathersort (smallest non-negative integer not used by any child of this father)
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
}
// Create child article
const rawTrack = options.track_stock ?? options.trackStock;
@@ -186,7 +230,11 @@ export default function addChild(provider) {
// Create article text
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
const childSlug = slugify(childName);
const childSlug = await resolveSlug(runner, {
companyId,
languageId,
name: childName,
});
await runner.query(createArticleTextSql, {
articleId: childId,
languageId,
@@ -194,13 +242,20 @@ export default function addChild(provider) {
slug: childSlug,
});
// History
// History on child
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
});
// History on father
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
});
return load(googleAuthId, childSku, runner);
};

View File

@@ -11,12 +11,13 @@ import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import nextSku from '../numbers/nextSku.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
export default function createUnderParent(provider) {
const access = resolveCategoryAccess(provider);
const ensureUnit = ensureDefaultUnit(provider);
const allocateSku = nextSku(provider);
const resolveSlug = resolveUniqueSlug(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
@@ -53,7 +54,7 @@ export default function createUnderParent(provider) {
const unitId = await ensureUnit(runner, languageId, companyId);
const sku = await allocateSku(companyId, runner);
const slug = slugify(name);
const slug = await resolveSlug(runner, { companyId, languageId, name });
let article;
try {

View File

@@ -1,5 +1,7 @@
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
@@ -7,6 +9,7 @@ export default function del(provider) {
const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
/**
* Deletes an article by its SKU, refusing when it still has children unless cascade is true.
@@ -40,6 +43,8 @@ export default function del(provider) {
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
await runner.query(queries.CREATE_TOMBSTONE, { articleId: id });
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
@@ -64,16 +69,38 @@ export default function del(provider) {
await deleteArticleAndLists(id);
};
const fatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const fatherArticleId = fatherRes.rows[0]?.fatherarticle_id;
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
if (childCount.rows[0].count > 0) {
if (!cascade) {
throw new Error('Article has children');
}
await deleteSubtree(articleId);
return;
} else {
await deleteArticleAndLists(articleId);
}
await deleteArticleAndLists(articleId);
if (fatherArticleId != null) {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
{ fatherArticleId },
);
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
const childRow = remainingChildrenRes.rows[i];
if (Number(childRow.fathersort) !== i) {
await runner.query(articleQueries.SET_FATHER_SORT, {
articleId: childRow.id,
fatherArticleId,
fathersort: i,
});
}
}
}
};
if (tx) {

View File

@@ -1,28 +1,50 @@
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY, LIST_BY_ARTICLE_ID_LIMITED as MSSQL_QUERY_LIMITED } from '../../providers/mssql/queries/articlehistory.js';
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY, LIST_BY_ARTICLE_ID_LIMITED as SQLITE_QUERY_LIMITED } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function getHistoryBySku(provider) {
const resolve = resolveArticleBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
const isSqlite = provider?.dialect === 'sqlite3';
const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY;
const sqlLimited = isSqlite ? SQLITE_QUERY_LIMITED : MSSQL_QUERY_LIMITED;
/**
* Returns the change history of an article by its SKU.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {string} sku - The article SKU.
* @param {object} [opts] - Optional options.
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
*
* @example
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
* // => [{ id, companyuserId, date, text }]
*
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
* // => up to 5 most recent entries
*/
return async (googleAuthId, sku, tx) => {
const { articleId } = await resolve(googleAuthId, sku, tx);
const runner = makeRunner(provider, tx);
const result = await runner.query(sql, { articleId });
return async (googleAuthId, sku, opts, tx) => {
// Support legacy call signature: getHistoryBySku(googleAuthId, sku, tx)
let limit = null;
let resolvedTx = tx;
if (opts != null && typeof opts === 'object' && !opts._isTx) {
limit = opts.limit != null ? Number(opts.limit) : null;
} else if (opts != null) {
resolvedTx = opts;
}
const runner = makeRunner(provider, resolvedTx);
const { articleId } = await resolve(googleAuthId, sku, runner);
let result;
if (limit != null && Number.isInteger(limit) && limit > 0) {
result = await runner.query(sqlLimited, { articleId, limit });
} else {
result = await runner.query(sql, { articleId });
}
return result.rows.map((row) => ({
id: row.id,
@@ -32,4 +54,3 @@ export default function getHistoryBySku(provider) {
}));
};
}

View File

@@ -30,7 +30,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
import ensureByPath from '../category/ensureByPath.js';
import ensureDefaultUnit from '../unit/ensureDefault.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
// Normalizes a numeric value that may arrive as a string using German decimal
// notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty.
@@ -49,6 +49,7 @@ export default function importRows(provider) {
const companyAccess = resolveSelectedCompanyAccess(provider);
const ensureUnit = ensureDefaultUnit(provider);
const ensureCategoryByPath = ensureByPath(provider);
const resolveSlug = resolveUniqueSlug(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
@@ -167,7 +168,7 @@ export default function importRows(provider) {
try {
await runInTx(async (runner) => {
const unitId = await ensureUnit(runner, companyId, languageId);
const slug = slugify(name);
const slug = await resolveSlug(runner, { companyId, languageId, name });
const rawTrack = row.track_stock ?? row.trackStock;
const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
@@ -286,22 +287,52 @@ export default function importRows(provider) {
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
const childId = child.rows[0]?.articleId;
if (childId) {
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
const fatherId = father.rows[0]?.articleId;
const father = await runner.query(
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: fatherSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (fatherId && fatherId !== childId) {
const childCount = await runner.query(getChildCountSql, {
articleId: fatherId,
});
const count = childCount.rows[0].count;
const wasFather = Boolean(fatherRow.is_father);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
}
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort: count,
fathersort,
});
await runner.query(articleQueries.SET_IS_FATHER, {
articleId: fatherId,
});
if (!wasFather) {
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
const childName = String(row?.name ?? sku).trim();
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
});
}
}
});

View File

@@ -28,7 +28,7 @@ export default function removeChild(provider) {
*/
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
const run = async (runner) => {
const { articleId: fatherId, companyuserId } = await resolve(
const { articleId: fatherId, languageId, companyuserId } = await resolve(
googleAuthId,
fatherSku,
runner,
@@ -45,9 +45,45 @@ export default function removeChild(provider) {
throw new Error('Child article does not belong to this father');
}
// Fetch child name before any deletion (needed for history regardless of path)
const childNameRes = await runner.query(
`SELECT at.name FROM articletext at
INNER JOIN article a ON a.id = at.article_id
WHERE a.id = @childId AND at.language_id = @languageId`,
{ childId, languageId },
);
const childName = childNameRes.rows[0]?.name || childSku;
const recompactFatherSort = async () => {
const remainingChildrenRes = await runner.query(
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
{ fatherId },
);
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
const childRow = remainingChildrenRes.rows[i];
if (Number(childRow.fathersort) !== i) {
await runner.query(queries.SET_FATHER_SORT, {
articleId: childRow.id,
fatherArticleId: fatherId,
fathersort: i,
});
}
}
};
if (options.deleteArticle) {
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
await del(googleAuthId, childSku, { cascade }, runner);
await recompactFatherSort();
// History on father (child article is gone so we can only log on father)
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: `Variante "${childName}" (${childSku}) gelöscht`,
});
return {
success: true,
fatherSku,
@@ -81,13 +117,22 @@ export default function removeChild(provider) {
);
}
// Add history
await recompactFatherSort();
// Add history on child
await runner.query(insertHistorySql, {
articleId: childId,
companyuserId,
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
});
// Add history on father
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: `Variante "${childName}" (${childSku}) getrennt`,
});
return {
success: true,
fatherSku,

View File

@@ -0,0 +1,51 @@
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as MSSQL_LIST_SLUGS } from '../../providers/mssql/queries/articletext.js';
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as SQLITE_LIST_SLUGS } from '../../providers/sqlite3/queries/articletext.js';
import slugify from '../slugify.js';
export default function resolveUniqueSlug(provider) {
const isSqlite = provider?.dialect === 'sqlite3';
const listSlugsSql = isSqlite ? SQLITE_LIST_SLUGS : MSSQL_LIST_SLUGS;
/**
* Resolves a company- and language-unique slug for an article.
* Disambiguates duplicate slugs with a numeric suffix (-2, -3, ...).
*
* @param {object} runner - DB runner or tx.
* @param {object} params
* @param {number} params.companyId - The company ID.
* @param {number} params.languageId - The language ID.
* @param {string} params.name - The article name.
* @param {number} [params.excludeArticleId] - Optional article ID to exclude (e.g. for updates).
* @returns {Promise<string>} The unique slug.
*/
return async (runner, { companyId, languageId, name, excludeArticleId }) => {
const baseSlug = slugify(name) || 'article';
const likePattern = `${baseSlug}-%`;
const res = await runner.query(listSlugsSql, {
companyId,
languageId,
baseSlug,
likePattern,
});
const existingSlugs = new Set();
for (const row of res.rows) {
const artId = row.articleId ?? row.article_id;
if (excludeArticleId != null && artId === excludeArticleId) {
continue;
}
existingSlugs.add(row.slug);
}
if (!existingSlugs.has(baseSlug)) {
return baseSlug;
}
let suffix = 2;
while (existingSlugs.has(`${baseSlug}-${suffix}`)) {
suffix++;
}
return `${baseSlug}-${suffix}`;
};
}

View File

@@ -47,25 +47,29 @@ export default function setBrandIdBySku(provider) {
throw new Error('Invalid brandId');
}
// Read old brand for history
// Read old brand name (null = no brand assigned)
const oldBrandRes = await runner.query(
`SELECT b.name FROM article a LEFT JOIN brand b ON b.id = a.brand_id WHERE a.id = @articleId`,
{ articleId },
);
const oldBrandName = oldBrandRes.rows[0]?.name ?? '';
const oldBrandName = oldBrandRes.rows[0]?.name ?? null;
let resolvedBrandId = null;
let newBrandName = '';
let newBrandName = null;
if (brandId !== null) {
const brand = await runner.query(getBrandSql, {
brandId,
companyId,
});
if (!brand.rows[0]) {
// Verify brand belongs to this company
const brandCheck = await runner.query(getBrandSql, { brandId, companyId });
if (!brandCheck.rows[0]) {
throw new Error('Brand not found');
}
resolvedBrandId = brandId;
newBrandName = brand.rows[0].name ?? '';
// GET_BRAND_BY_COMPANY_AND_ID only selects `id`, so fetch name separately
const brandNameRes = await runner.query(
'SELECT name FROM brand WHERE id = @brandId',
{ brandId },
);
newBrandName = brandNameRes.rows[0]?.name ?? null;
}
const result = await runner.query(updateBrandSql, {
@@ -77,12 +81,12 @@ export default function setBrandIdBySku(provider) {
throw new Error('Article not found');
}
// Write history if brand changed
// null = no brand; treat as distinct sentinel so first assignment is logged
if (oldBrandName !== newBrandName) {
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Marke geändert von "${oldBrandName}" auf "${newBrandName}"`,
text: `Marke ge\u00e4ndert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`,
});
}
@@ -94,4 +98,3 @@ export default function setBrandIdBySku(provider) {
return article;
};
}

View File

@@ -2,6 +2,8 @@ import * as mssqlArticle from '../../providers/mssql/queries/article.js';
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
@@ -13,6 +15,7 @@ export default function setFatherBySku(provider) {
const queries = isSqlite ? sqliteArticle : mssqlArticle;
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Links an article to a father article (V/K compound) by their SKUs.
@@ -30,14 +33,47 @@ export default function setFatherBySku(provider) {
*/
return async (googleAuthId, sku, fatherSku, tx) => {
const runSet = async (runner) => {
const { articleId, languageId, companyId } = await resolve(googleAuthId, sku, runner);
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const childTextRes = await runner.query(
'SELECT name FROM articletext WHERE article_id = @articleId AND language_id = @languageId',
{ articleId, languageId },
);
const childName = childTextRes.rows[0]?.name || sku;
if (fatherSku == null || fatherSku === '') {
const prevFatherRes = await runner.query(
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
{ articleId },
);
const prevFatherId = prevFatherRes.rows[0]?.fatherarticle_id;
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku,
companyId,
fatherArticleId: null,
});
if (prevFatherId) {
const prevFatherTextRes = await runner.query(
'SELECT sku FROM article WHERE id = @prevFatherId',
{ prevFatherId },
);
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
});
await runner.query(insertHistorySql, {
articleId: prevFatherId,
companyuserId,
text: `Variante "${childName}" (${sku}) getrennt`,
});
}
const unlinked = await load(articleId, languageId, runner);
if (!unlinked) {
throw new Error('Article not found');
@@ -45,8 +81,12 @@ export default function setFatherBySku(provider) {
return unlinked;
}
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
const fatherId = father.rows[0]?.articleId;
const father = await runner.query(
'SELECT id, is_father, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
{ sku: fatherSku, companyId },
);
const fatherRow = father.rows[0];
const fatherId = fatherRow?.id;
if (!fatherId) {
throw new Error('Father not found');
}
@@ -54,22 +94,49 @@ export default function setFatherBySku(provider) {
throw new Error('Invalid father');
}
const childCount = await runner.query(getChildCountSql, {
articleId: fatherId,
});
const count = childCount.rows[0].count;
const wasFather = Boolean(fatherRow.is_father);
const existingSortsRes = await runner.query(
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
{ fatherId },
);
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
let fathersort = 0;
while (usedSorts.has(fathersort)) {
fathersort++;
}
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
sku,
companyId,
fatherArticleId: fatherId,
fathersort: count,
fathersort,
});
await runner.query(queries.SET_IS_FATHER, {
articleId: fatherId,
});
if (!wasFather) {
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: 'Artikel zu Vaterartikel konvertiert',
});
}
await runner.query(insertHistorySql, {
articleId: fatherId,
companyuserId,
text: `Variante "${childName}" (${sku}) hinzugefügt`,
});
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
});
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');

View File

@@ -11,7 +11,7 @@ import { GET_FULL_BY_SI as SQLITE_GET_UNIT } from '../../providers/sqlite3/queri
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
import makeRunner from '../runner.js';
import slugify from '../slugify.js';
import resolveUniqueSlug from './resolveUniqueSlug.js';
function formatUnitLabel(amount, symbol) {
const sym = symbol ?? '';
@@ -24,6 +24,7 @@ function formatUnitLabel(amount, symbol) {
export default function updateBySku(provider) {
const resolve = resolveArticleBySku(provider);
const load = queryBySku(provider);
const resolveSlug = resolveUniqueSlug(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
@@ -144,7 +145,12 @@ export default function updateBySku(provider) {
}
if (hasName) {
const slug = slugify(patch.name);
const slug = await resolveSlug(runner, {
companyId,
languageId,
name: patch.name,
excludeArticleId: articleId,
});
const update = await runner.query(updateTextSql, {
articleId,
languageId,

View File

@@ -44,7 +44,7 @@ export default function setByArticleSku(provider) {
`SELECT aav.attributevalue_id AS attributeValueId, an.name AS attributeName, av.value
FROM articleattributevalue aav
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
INNER JOIN attributename an ON an.id = av.attributename_id
INNER JOIN attributename an ON an.id = av.attribute_id
WHERE aav.article_id = @articleId
ORDER BY aav.sort ASC, aav.id ASC`,
{ articleId },
@@ -57,7 +57,36 @@ export default function setByArticleSku(provider) {
for (let i = 0; i < values.length; i++) {
const item = values[i];
const valId = typeof item === 'object' && item !== null ? Number(item.attributeValueId) : Number(item);
let rawId;
if (typeof item === 'object' && item !== null) {
rawId = item.attributeValueId ?? item.attributevalueId ?? item.attribute_value_id ?? item.valueId ?? item.id;
} else {
rawId = item;
}
let valId = Number(rawId);
if ((!Number.isInteger(valId) || valId <= 0) && typeof item === 'object' && item !== null && typeof item.value === 'string' && item.value.trim()) {
const valStr = item.value.trim();
let targetAttrId = item.attributeId != null ? Number(item.attributeId) : null;
if (!targetAttrId && (item.attributeName || item.name)) {
const attrName = String(item.attributeName || item.name).trim();
const nameRes = await runner.query(
'SELECT id FROM attributename WHERE company_id = @companyId AND name = @attrName',
{ companyId, attrName },
);
targetAttrId = nameRes.rows[0]?.id;
}
if (targetAttrId) {
const valRes = await runner.query(
'SELECT id FROM attributevalue WHERE attribute_id = @attributeId AND value = @value',
{ attributeId: targetAttrId, value: valStr },
);
if (valRes.rows[0]?.id) {
valId = valRes.rows[0].id;
}
}
}
if (!Number.isInteger(valId) || valId <= 0) {
throw new Error('Invalid attributeValueId');
}
@@ -80,8 +109,8 @@ export default function setByArticleSku(provider) {
exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName);
}
const sort = typeof item === 'object' && item !== null && item.sort != null && Number.isInteger(item.sort)
? item.sort
const sort = typeof item === 'object' && item !== null && (item.sort != null || item.sort_order != null) && Number.isInteger(Number(item.sort ?? item.sort_order))
? Number(item.sort ?? item.sort_order)
: i;
validated.push({

View File

@@ -4,12 +4,16 @@ import {
import {
DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE,
} from '../../providers/sqlite3/queries/articleproperty.js';
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import makeRunner from '../runner.js';
export default function deleteArticleProperty(provider) {
const resolve = resolveArticleBySku(provider);
const deleteSql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/**
* Deletes a property from an article by SKU and propertyId.
@@ -22,7 +26,26 @@ export default function deleteArticleProperty(provider) {
*/
return async (googleAuthId, sku, propertyId, tx) => {
const runner = makeRunner(provider, tx);
const { articleId } = await resolve(googleAuthId, sku, runner);
const { articleId, companyuserId } = await resolve(googleAuthId, sku, runner);
// Read the property name + value before deleting (for the history entry)
const propRes = await runner.query(
`SELECT ap.value, pn.name
FROM articleproperty ap
INNER JOIN propertyname pn ON pn.id = ap.property_id
WHERE ap.article_id = @articleId AND ap.property_id = @propertyId`,
{ articleId, propertyId },
);
const prop = propRes.rows[0];
await runner.query(deleteSql, { articleId, propertyId });
if (prop) {
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: `Eigenschaft "${prop.name}" (${prop.value}) gelöscht`,
});
}
};
}

View File

@@ -0,0 +1,34 @@
import { LIST_BY_CATEGORY_ID as MSSQL_QUERY } from '../../providers/mssql/queries/categoryattributevalue.js';
import { LIST_BY_CATEGORY_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/categoryattributevalue.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
export default function listByCategoryId(provider) {
const resolve = resolveCategoryAccess(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
/**
* Lists the attribute values assigned to a category by its ID.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The category ID.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>}
*/
return async (googleAuthId, categoryId, tx) => {
const runner = makeRunner(provider, tx);
await resolve(googleAuthId, categoryId, runner);
const result = await runner.query(sql, { categoryId });
return result.rows.map((row) => ({
id: row.id,
categoryId: row.categoryId,
attributeId: row.attributeId,
attributeName: row.attributeName,
exclusive: Boolean(row.exclusive),
attributeValueId: row.attributeValueId,
value: row.value,
sort: row.sort,
}));
};
}

View File

@@ -0,0 +1,155 @@
import {
DELETE_BY_CATEGORY_ID as MSSQL_DELETE,
INSERT_ROW as MSSQL_INSERT,
GET_VALUE_DETAIL as MSSQL_GET_DETAIL,
} from '../../providers/mssql/queries/categoryattributevalue.js';
import {
DELETE_BY_CATEGORY_ID as SQLITE_DELETE,
INSERT_ROW as SQLITE_INSERT,
GET_VALUE_DETAIL as SQLITE_GET_DETAIL,
} from '../../providers/sqlite3/queries/categoryattributevalue.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
export default function setByCategoryId(provider) {
const resolve = resolveCategoryAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
/**
* Replaces all attribute values for a category by its ID.
* Validates company ownership and enforces the `exclusive` flag on attributes.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The category ID.
* @param {Array<number|{attributeValueId: number, sort?: number}>} values - Attribute value IDs or items.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>}
*/
return async (googleAuthId, categoryId, values = [], tx) => {
if (!Array.isArray(values)) {
throw new Error('Invalid attribute values array');
}
const runSet = async (runner) => {
const { companyId } = await resolve(googleAuthId, categoryId, runner);
const validated = [];
const seenValues = new Set();
const exclusiveAttributesSeen = new Map(); // attributeId -> attributeName
for (let i = 0; i < values.length; i++) {
const item = values[i];
let rawId;
if (typeof item === 'object' && item !== null) {
rawId = item.attributeValueId ?? item.attributevalueId ?? item.attribute_value_id ?? item.valueId ?? item.id;
} else {
rawId = item;
}
let valId = Number(rawId);
if ((!Number.isInteger(valId) || valId <= 0) && typeof item === 'object' && item !== null && typeof item.value === 'string' && item.value.trim()) {
const valStr = item.value.trim();
let targetAttrId = item.attributeId != null ? Number(item.attributeId) : null;
if (!targetAttrId && (item.attributeName || item.name)) {
const attrName = String(item.attributeName || item.name).trim();
const nameRes = await runner.query(
'SELECT id FROM attributename WHERE company_id = @companyId AND name = @attrName',
{ companyId, attrName },
);
targetAttrId = nameRes.rows[0]?.id;
}
if (targetAttrId) {
const valRes = await runner.query(
'SELECT id FROM attributevalue WHERE attribute_id = @targetAttrId AND value = @valStr',
{ targetAttrId, valStr },
);
if (valRes.rows[0]?.id) {
valId = valRes.rows[0].id;
}
}
}
if (!Number.isInteger(valId) || valId <= 0) {
throw new Error(`Invalid attributeValueId at index ${i}`);
}
if (seenValues.has(valId)) {
continue; // de-duplicate identical value assignments
}
seenValues.add(valId);
const detailRes = await runner.query(getDetailSql, { attributeValueId: valId });
const detail = detailRes.rows[0];
if (!detail) {
throw new Error(`Attribute value not found: ${valId}`);
}
if (detail.companyId !== companyId) {
throw new Error(`Attribute value does not belong to company: ${valId}`);
}
const isExclusive = Boolean(detail.exclusive);
if (isExclusive) {
if (exclusiveAttributesSeen.has(detail.attributeId)) {
const attrName = exclusiveAttributesSeen.get(detail.attributeId);
throw new Error(`Attribute "${attrName}" is exclusive and can only have one value`);
}
exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName);
}
const sort = typeof item === 'object' && item !== null && item.sort != null && Number.isInteger(item.sort)
? item.sort
: i;
validated.push({
attributeValueId: valId,
attributeId: detail.attributeId,
attributeName: detail.attributeName,
exclusive: isExclusive,
value: detail.value,
sort,
});
}
await runner.query(deleteSql, { categoryId });
const inserted = [];
for (const item of validated) {
const res = await runner.query(insertSql, {
categoryId,
attributeValueId: item.attributeValueId,
sort: item.sort,
});
const id = res.rows[0]?.id ?? res.insertId;
inserted.push({
id,
categoryId,
attributeId: item.attributeId,
attributeName: item.attributeName,
exclusive: item.exclusive,
attributeValueId: item.attributeValueId,
value: item.value,
sort: item.sort,
});
}
// Touch category updated_at
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
await runner.query(touchSql, { categoryId });
return inserted;
};
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -0,0 +1,36 @@
import {
DELETE_BY_CATEGORY_AND_PROPERTY_ID as MSSQL_DELETE,
} from '../../providers/mssql/queries/categoryproperty.js';
import {
DELETE_BY_CATEGORY_AND_PROPERTY_ID as SQLITE_DELETE,
} from '../../providers/sqlite3/queries/categoryproperty.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
export default function deleteCategoryProperty(provider) {
const resolve = resolveCategoryAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
/**
* Deletes a property from a category by categoryId and propertyId.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The category ID.
* @param {number} propertyId - The property id.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<void>}
*/
return async (googleAuthId, categoryId, propertyId, tx) => {
const runner = makeRunner(provider, tx);
await resolve(googleAuthId, categoryId, runner);
await runner.query(deleteSql, { categoryId, propertyId });
// Touch category updated_at
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
await runner.query(touchSql, { categoryId });
};
}

View File

@@ -0,0 +1,33 @@
import { LIST_BY_CATEGORY_ID as MSSQL_QUERY } from '../../providers/mssql/queries/categoryproperty.js';
import { LIST_BY_CATEGORY_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/categoryproperty.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
export default function listByCategoryId(provider) {
const resolve = resolveCategoryAccess(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
/**
* Lists the properties of a category by its ID.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The category ID.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
*/
return async (googleAuthId, categoryId, tx) => {
const runner = makeRunner(provider, tx);
await resolve(googleAuthId, categoryId, runner);
const result = await runner.query(sql, { categoryId });
return result.rows.map((row) => ({
id: row.id,
categoryId: row.categoryId,
propertyId: row.propertyId,
name: row.name,
exclusive: Boolean(row.exclusive),
value: row.value,
sort: row.sort,
}));
};
}

View File

@@ -0,0 +1,117 @@
import {
DELETE_BY_CATEGORY_ID as MSSQL_DELETE,
INSERT_ROW as MSSQL_INSERT,
} from '../../providers/mssql/queries/categoryproperty.js';
import {
DELETE_BY_CATEGORY_ID as SQLITE_DELETE,
INSERT_ROW as SQLITE_INSERT,
} from '../../providers/sqlite3/queries/categoryproperty.js';
import {
GET_BY_COMPANY_AND_ID as MSSQL_GET_PROP,
} from '../../providers/mssql/queries/propertyname.js';
import {
GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP,
} from '../../providers/sqlite3/queries/propertyname.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import makeRunner from '../runner.js';
export default function setByCategoryId(provider) {
const resolve = resolveCategoryAccess(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
const getPropSql = isSqlite ? SQLITE_GET_PROP : MSSQL_GET_PROP;
/**
* Replaces all properties for a category by its ID.
* Enforces exclusive flag on property names.
*
* @param {number} googleAuthId - The id of the Google auth user.
* @param {number} categoryId - The category ID.
* @param {Array<{propertyId: number, value: string, sort?: number}>} properties - The properties to set.
* @param {object} [tx] - Optional transaction.
* @returns {Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
*/
return async (googleAuthId, categoryId, properties = [], tx) => {
if (!Array.isArray(properties)) {
throw new Error('Invalid properties array');
}
const runSet = async (runner) => {
const { companyId } = await resolve(googleAuthId, categoryId, runner);
const validated = [];
const seenExclusiveProps = new Set();
for (let i = 0; i < properties.length; i++) {
const item = properties[i];
const propertyId = Number(item.propertyId);
if (!Number.isInteger(propertyId) || propertyId <= 0) {
throw new Error('Invalid propertyId');
}
const propRes = await runner.query(getPropSql, { id: propertyId, companyId });
const prop = propRes.rows[0];
if (!prop) {
throw new Error(`Property not found: ${propertyId}`);
}
const isExclusive = Boolean(prop.exclusive);
if (isExclusive) {
if (seenExclusiveProps.has(propertyId)) {
throw new Error(`Property "${prop.name}" is exclusive and can only have one value`);
}
seenExclusiveProps.add(propertyId);
}
const value = item.value != null ? String(item.value).trim() : '';
const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i;
validated.push({
propertyId,
name: prop.name,
exclusive: isExclusive,
value,
sort,
});
}
await runner.query(deleteSql, { categoryId });
const inserted = [];
for (const item of validated) {
const res = await runner.query(insertSql, {
categoryId,
propertyId: item.propertyId,
value: item.value,
sort: item.sort,
});
const id = res.rows[0]?.id ?? res.insertId;
inserted.push({
id,
categoryId,
propertyId: item.propertyId,
name: item.name,
exclusive: item.exclusive,
value: item.value,
sort: item.sort,
});
}
// Touch category updated_at
const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
await runner.query(touchSql, { categoryId });
return inserted;
};
if (tx) {
return runSet(makeRunner(provider, tx));
}
if (typeof provider?.transaction === 'function') {
return provider.transaction(runSet);
}
return runSet(makeRunner(provider));
};
}

View File

@@ -1,5 +1,11 @@
import { CREATE_LINE as MSSQL_CREATE_LINE } from '../providers/mssql/queries/manifestDoc.js';
import { CREATE_LINE as SQLITE_CREATE_LINE } from '../providers/sqlite3/queries/manifestDoc.js';
import {
CHECK_ARTICLE_IS_FATHER as MSSQL_CHECK_ARTICLE_IS_FATHER,
CREATE_LINE as MSSQL_CREATE_LINE,
} from '../providers/mssql/queries/manifestDoc.js';
import {
CHECK_ARTICLE_IS_FATHER as SQLITE_CHECK_ARTICLE_IS_FATHER,
CREATE_LINE as SQLITE_CREATE_LINE,
} from '../providers/sqlite3/queries/manifestDoc.js';
import makeRunner from './runner.js';
/**
@@ -24,7 +30,7 @@ export function normalizeLines(lines) {
}
const pu =
entry?.pu === undefined || entry?.pu === null ? 1 : entry.pu;
entry?.pu === undefined || entry?.pu === null ? null : entry.pu;
const amount = entry?.amount;
const net = entry?.net;
const rebate =
@@ -36,7 +42,7 @@ export function normalizeLines(lines) {
: String(entry.comment);
if (
typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0 ||
(pu !== null && (typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0)) ||
typeof amount !== 'number' || !Number.isFinite(amount) ||
typeof net !== 'number' || !Number.isFinite(net) ||
typeof rebate !== 'number' || !Number.isFinite(rebate) ||
@@ -116,8 +122,32 @@ export async function insertLines(
const { manifestId, lines, ensureTax } = params;
const isSqlite = runner?.dialect === 'sqlite3';
const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE;
const checkFatherSql = isSqlite ? SQLITE_CHECK_ARTICLE_IS_FATHER : MSSQL_CHECK_ARTICLE_IS_FATHER;
for (const line of lines) {
let resolvedName = line.name;
let resolvedPu = line.pu;
if (line.sku) {
const fatherCheck = await runner.query(checkFatherSql, {
manifestId,
sku: line.sku,
});
const art = fatherCheck.rows[0];
if (art?.isFather) {
throw new Error(`A father article cannot be booked on documents (${line.sku})`);
}
if (!resolvedName && art?.articleName) {
resolvedName = art.articleName;
}
if (resolvedPu == null && art?.pu != null) {
resolvedPu = art.pu;
}
}
resolvedName = resolvedName || line.sku || null;
resolvedPu = resolvedPu ?? 1;
if (ensureTax) {
await ensureTax(line.taxrate, runner);
}
@@ -125,8 +155,8 @@ export async function insertLines(
await runner.query(createLineSql, {
manifestId,
sku: line.sku ?? null,
name: line.name ?? null,
pu: line.pu ?? 1,
name: resolvedName,
pu: resolvedPu,
amount: line.amount,
net: line.net,
comment: line.comment,

View File

@@ -0,0 +1,27 @@
import { SET_MAX_KNOWN_TOMBSTONE_ID as MSSQL_SET } from '../../providers/mssql/queries/posterminal.js';
import { SET_MAX_KNOWN_TOMBSTONE_ID as SQLITE_SET } from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js';
export default function setMaxKnownTombstoneId(provider) {
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SET : MSSQL_SET;
/**
* Updates the max known tombstone ID for a POS terminal.
*
* @param {number} id - The posterminal id.
* @param {number|null} maxknowntombstoneid - The highest synced tombstone ID.
* @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<{id: number, maxknowntombstoneid: number|null}>}
*/
return async (id, maxknowntombstoneid, tx) => {
await makeRunner(provider, tx).query(sql, {
id,
maxknowntombstoneid: maxknowntombstoneid != null ? Number(maxknowntombstoneid) : null,
});
return {
id,
maxknowntombstoneid: maxknowntombstoneid != null ? Number(maxknowntombstoneid) : null,
};
};
}

View File

@@ -60,6 +60,11 @@ export default function upsert(provider) {
throw new Error('Posterminal not found');
}
const maxknowntombstoneid =
input.maxknowntombstoneid !== undefined
? input.maxknowntombstoneid
: (existing.rows[0].maxknowntombstoneid ?? null);
await runner.query(updateSql, {
id,
companyId,
@@ -68,11 +73,13 @@ export default function upsert(provider) {
pairingpin,
pairedAt,
clientserial,
maxknowntombstoneid,
});
return { id, name, token, pairingpin, pairedAt, clientserial };
return { id, name, token, pairingpin, pairedAt, clientserial, maxknowntombstoneid };
}
const maxknowntombstoneid = input.maxknowntombstoneid ?? null;
const created = await runner.query(createSql, {
companyId,
name,
@@ -80,6 +87,7 @@ export default function upsert(provider) {
pairingpin,
pairedAt,
clientserial,
maxknowntombstoneid,
});
const row = created.rows[0] ?? {
@@ -89,6 +97,7 @@ export default function upsert(provider) {
pairingpin,
pairedAt,
clientserial,
maxknowntombstoneid,
};
return {
@@ -98,6 +107,7 @@ export default function upsert(provider) {
pairingpin: row.pairingpin,
pairedAt: row.pairedAt ?? null,
clientserial: row.clientserial ?? null,
maxknowntombstoneid: row.maxknowntombstoneid ?? null,
};
};
}

View File

@@ -123,54 +123,16 @@ export default function adjust(provider) {
throw new Error(`A father article cannot have stock (${item.sku})`);
}
const articlelistId = artRes.rows[0]?.articlelist_id;
if (articlelistId != null) {
const compRes = await runner.query(`
SELECT
ala.article_id AS articleId,
ala.count,
a.sku,
at.name AS articleName
FROM articlelistarticle ala
INNER JOIN article a ON a.id = ala.article_id
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
WHERE ala.articlelist_id = @articlelistId
ORDER BY ala.sort ASC, ala.id ASC
`, {
articlelistId,
languageId: resolved.languageId,
});
if (compRes.rows.length === 0) {
throw new Error(`Stückliste has no items (${item.sku})`);
}
for (const comp of compRes.rows) {
const compCount = Number(comp.count || 1);
const compAmount = item.amount * compCount;
const compText = item.text
? `${item.text} (${item.sku})`
: (compAmount > 0
? `Manual stock addition (+${compAmount}) [${item.sku}]`
: `Manual stock removal (${compAmount}) [${item.sku}]`);
resolvedArticles.push({
sku: comp.sku,
warehouseId: item.warehouseId,
amount: compAmount,
text: compText,
articleId: comp.articleId,
articleName: comp.articleName || comp.sku,
});
}
} else {
const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, {
articleId: resolved.articleId,
languageId: resolved.languageId,
});
const articleName = nameRes.rows[0]?.name ?? item.sku;
resolvedArticles.push({ ...item, ...resolved, articleName });
if (artRes.rows[0]?.articlelist_id != null) {
throw new Error(`A Stückliste cannot have manual stock adjustments (${item.sku})`);
}
const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, {
articleId: resolved.articleId,
languageId: resolved.languageId,
});
const articleName = nameRes.rows[0]?.name ?? item.sku;
resolvedArticles.push({ ...item, ...resolved, articleName });
}
// Allocate unique adjustment number using the dedicated adjustment number scheme