This commit is contained in:
seb
2026-10-23 00:47:38 +02:00
parent e31566e6ee
commit 5fc316461d
10 changed files with 266 additions and 4 deletions

View File

@@ -228,6 +228,16 @@ SET updated_at = SYSUTCDATETIME()
WHERE id = @articleId
`;
export const TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT = `
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE optionslist_id IN (
SELECT ola.optionslist_id
FROM optionslistarticle ola
WHERE ola.article_id = @articleId
)
`;
export const CREATE = `
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
OUTPUT INSERTED.id

View File

@@ -483,6 +483,25 @@ WHERE pr.minmultiplier = 1
GROUP BY pr.article_id, pr.customergroup_id
`;
export const LIST_POS_MANDATED_OPTIONS = `
SELECT
a.id AS articleId,
ola.count,
COALESCE(opt_at.name, opt_a.sku) AS depositName,
opt_pr.net AS depositNetPrice,
COALESCE(opt_ptax.rate, opt_ctax.rate, 0) AS depositTaxRate
FROM article a
INNER JOIN optionslistarticle ola ON ola.optionslist_id = a.optionslist_id AND ola.mandated = 1
INNER JOIN article opt_a ON opt_a.id = ola.article_id
INNER JOIN company co ON co.id = a.company_id
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id
LEFT JOIN articletext opt_at ON opt_at.article_id = opt_a.id AND opt_at.language_id = co.default_language_id
LEFT JOIN price opt_pr ON opt_pr.article_id = opt_a.id AND opt_pr.customergroup_id = co.poscustomergroup_id AND opt_pr.minmultiplier = 1
LEFT JOIN tax opt_ptax ON opt_ptax.id = opt_pr.tax_id
LEFT JOIN tax opt_ctax ON opt_ctax.id = co.taxrate
ORDER BY ola.sort ASC, ola.id ASC
`;
export const RECORD_OBITUARY = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT pt.id, @entityType, @entityId

View File

@@ -11,7 +11,8 @@ export const {
INSERT_POSTERMINAL_OBITUARY,
LIST_CHILDREN_BY_PARENT_ID, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE,
SET_PARENT_SORT,
SET_IS_PARENT, TOGGLE_PARENT, TOUCH_UPDATED_AT, UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
SET_IS_PARENT, TOGGLE_PARENT, TOUCH_UPDATED_AT, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT,
UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
} = queries;

View File

@@ -9,6 +9,7 @@ export const {
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,
LIST_POS_MANDATED_OPTIONS,
TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES,
} = queries;

View File

@@ -49,6 +49,9 @@ export default function del(provider) {
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
// Touch any parent articles that used this article as an option before deleting it
await runner.query(articleQueries.TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT, { articleId: id });
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
if (!deleted.rowsAffected) {
throw new Error('Article not found');

View File

@@ -6,8 +6,14 @@ import {
CREATE as SQLITE_CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
} from '../../providers/sqlite3/queries/price.js';
import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js';
import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js';
import {
TOUCH_UPDATED_AT as MSSQL_TOUCH,
TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT as MSSQL_TOUCH_OPTION_PARENTS,
} from '../../providers/mssql/queries/article.js';
import {
TOUCH_UPDATED_AT as SQLITE_TOUCH,
TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT as SQLITE_TOUCH_OPTION_PARENTS,
} from '../../providers/sqlite3/queries/article.js';
import taxEnsure from '../tax/ensure.js';
import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js';
@@ -68,6 +74,7 @@ export default function setPricesBySku(provider) {
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_PRICE;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchOptionParentsSql = isSqlite ? SQLITE_TOUCH_OPTION_PARENTS : MSSQL_TOUCH_OPTION_PARENTS;
/**
* Replaces the price list of an article by its SKU.
@@ -147,6 +154,9 @@ export default function setPricesBySku(provider) {
// Touch article updated_at so POS and other consumers pick up the price change
await runner.query(touchArticleSql, { articleId });
// Touch any parent articles using this article as an option so their deposit price updates
await runner.query(touchOptionParentsSql, { articleId });
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');

View File

@@ -235,6 +235,9 @@ export default function updateBySku(provider) {
// Touch article updated_at so all consumers pick up changes
await runner.query(articleQueries.TOUCH_UPDATED_AT, { articleId });
// Touch any parent articles that use this article as an option so their deposit name/details update on POS
await runner.query(articleQueries.TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT, { articleId });
const article = await load(articleId, languageId, runner);
if (!article) {
throw new Error('Article not found');

View File

@@ -1,5 +1,7 @@
import * as mssqlQueries from '../../providers/mssql/queries/optionslist.js';
import * as sqliteQueries from '../../providers/sqlite3/queries/optionslist.js';
import { TOUCH_UPDATED_AT as MSSQL_TOUCH } from '../../providers/mssql/queries/article.js';
import { TOUCH_UPDATED_AT as SQLITE_TOUCH } from '../../providers/sqlite3/queries/article.js';
import resolveArticleBySku from '../article/resolveArticleBySku.js';
import getOptionsByArticleSku from './getOptionsByArticleSku.js';
import {
@@ -46,6 +48,7 @@ export default function setOptionsByArticleSku(provider) {
const getOptions = getOptionsByArticleSku(provider);
const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqliteQueries : mssqlQueries;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
/**
* Sets or clears the options list for an article.
@@ -108,6 +111,7 @@ export default function setOptionsByArticleSku(provider) {
type: 'optionslist_cleared',
});
}
await runner.query(touchArticleSql, { articleId });
}
return [];
}
@@ -236,6 +240,9 @@ export default function setOptionsByArticleSku(provider) {
options: result,
});
// Touch article updated_at so POS and other consumers pick up options/deposit changes
await runner.query(touchArticleSql, { articleId });
return result;
};

View File

@@ -3,12 +3,14 @@ import {
LIST_POS_PRODUCT_CATEGORIES as MSSQL_CATEGORIES,
LIST_POS_CUSTOMER_GROUP_IDS as MSSQL_CG_IDS,
LIST_POS_PRODUCT_PRICES as MSSQL_PRICES,
LIST_POS_MANDATED_OPTIONS as MSSQL_MANDATED_OPTIONS,
} from '../../providers/mssql/queries/posterminal.js';
import {
LIST_POS_PRODUCTS as SQLITE_PRODUCTS,
LIST_POS_PRODUCT_CATEGORIES as SQLITE_CATEGORIES,
LIST_POS_CUSTOMER_GROUP_IDS as SQLITE_CG_IDS,
LIST_POS_PRODUCT_PRICES as SQLITE_PRICES,
LIST_POS_MANDATED_OPTIONS as SQLITE_MANDATED_OPTIONS,
} from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js';
@@ -36,6 +38,7 @@ export default function getProductList(provider) {
const categoriesSql = isSqlite ? SQLITE_CATEGORIES : MSSQL_CATEGORIES;
const cgIdsSql = isSqlite ? SQLITE_CG_IDS : MSSQL_CG_IDS;
const pricesSql = isSqlite ? SQLITE_PRICES : MSSQL_PRICES;
const mandatedOptionsSql = isSqlite ? SQLITE_MANDATED_OPTIONS : MSSQL_MANDATED_OPTIONS;
/**
* Lists products for a POS terminal modified after the cursor.
@@ -52,7 +55,7 @@ export default function getProductList(provider) {
const runner = makeRunner(provider, tx);
const pid = Number(posterminalId);
const [prodRes, catRes, cgRes, priceRes] = await Promise.all([
const [prodRes, catRes, cgRes, priceRes, optRes] = await Promise.all([
runner.query(productsSql, {
posterminalId: pid,
cursor: normalizeCursor(cursor),
@@ -61,6 +64,7 @@ export default function getProductList(provider) {
runner.query(categoriesSql, { posterminalId: pid }),
runner.query(cgIdsSql, { posterminalId: pid }),
runner.query(pricesSql, { posterminalId: pid }),
runner.query(mandatedOptionsSql, { posterminalId: pid }),
]);
const categoriesByArticle = new Map();
@@ -79,6 +83,24 @@ export default function getProductList(provider) {
overridesByArticle.get(row.articleId).set(row.customerGroupId, row.netPrice);
}
const mandatedOptionsByArticle = new Map();
for (const row of optRes.rows) {
const gross =
Number(row.count || 1) *
Number(row.depositNetPrice || 0) *
(1 + Number(row.depositTaxRate || 0) / 100);
if (!mandatedOptionsByArticle.has(row.articleId)) {
mandatedOptionsByArticle.set(row.articleId, {
names: [row.depositName],
totalPrice: gross,
});
} else {
const existing = mandatedOptionsByArticle.get(row.articleId);
existing.names.push(row.depositName);
existing.totalPrice += gross;
}
}
const customerGroupIds = cgRes.rows.map((row) => row.id);
return prodRes.rows.map((product) => {
@@ -100,6 +122,10 @@ export default function getProductList(provider) {
});
const categoryIds = categoriesByArticle.get(product.id) ?? [];
const mandatedOpt = mandatedOptionsByArticle.get(product.id);
const hasDeposit = Boolean(mandatedOpt);
const depositName = mandatedOpt ? mandatedOpt.names.join(' + ') : '';
const depositPrice = mandatedOpt ? mandatedOpt.totalPrice.toFixed(2) : '0.0';
return {
_id: String(product.id),
@@ -123,6 +149,9 @@ export default function getProductList(provider) {
attributes: [],
use_stock: product.trackStock ? '1' : '0',
quantity: String(product.fBestand ?? 0),
deposit: hasDeposit ? '1' : '0',
d_price: depositPrice,
deposit_name: depositName,
};
});
};