Compare commits

...

6 Commits

Author SHA1 Message Date
seb
8002ca56e6 u 2026-10-23 04:59:04 +02:00
seb
8a085986e5 u 2026-10-23 02:43:35 +02:00
seb
919713fa53 u 2026-10-23 01:38:08 +02:00
seb
5fc316461d u 2026-10-23 00:47:38 +02:00
seb
e31566e6ee u 2026-10-22 23:33:00 +02:00
seb
d6e5da93cb u 2026-10-22 23:30:15 +02:00
28 changed files with 1346 additions and 87 deletions

View File

@@ -0,0 +1,171 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function test() {
console.log('--- Testing setOn and setOff with subcategories ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-posterminal-subcats-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('subcat-tester@example.com');
const company = await store.company.create({
name: 'SubCat Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-subcat',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
// Create Category Tree:
// Root -> Cat 4 -> SubCat 41 -> SubCat 411
// Root -> Cat 5
const cat4 = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Getränke',
});
const subCat41 = await store.category.createUnderParent({
googleAuthId,
parentId: cat4.id,
name: 'Alkoholfrei',
});
const subCat411 = await store.category.createUnderParent({
googleAuthId,
parentId: subCat41.id,
name: 'Cola',
});
const cat5 = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Snacks',
});
// Create articles in various categories
const artCola = await store.article.createUnderParent({
googleAuthId,
parentId: subCat411.id,
name: 'Cola 0.5L',
});
const artSnack = await store.article.createUnderParent({
googleAuthId,
parentId: cat5.id,
name: 'Chips',
});
// Set old timestamps on categories and articles to test updated_at touch
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
console.log('1. Testing setOn on parent category Cat 4 (should enable Cat 4, SubCat 41, and SubCat 411)...');
const onResult = await store.posterminal.setOn(googleAuthId, terminal.id, cat4.id);
console.log('Category IDs after setOn(cat4):', onResult.categoryIds);
assert(onResult.categoryIds.includes(cat4.id), 'Should include cat4');
assert(onResult.categoryIds.includes(subCat41.id), 'Should include subCat41');
assert(onResult.categoryIds.includes(subCat411.id), 'Should include subCat411');
assert(!onResult.categoryIds.includes(cat5.id), 'Should NOT include cat5');
// Check that categories in the subtree were touched
const catsAfterOn = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
for (const c of catsAfterOn) {
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOn`);
}
// Check that articles in the subtree were touched
const artAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
assert.notEqual(artAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOn');
// Cat 5 article should NOT be touched
const artSnackAfterOn = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artSnack.id);
assert.equal(artSnackAfterOn.updated_at, '2020-01-01T00:00:00.000Z', 'Unrelated article updated_at should not be touched');
console.log('2. Also enabling Cat 5...');
await store.posterminal.setOn(googleAuthId, terminal.id, cat5.id);
// Reset timestamps to test setOff touch
provider.db.prepare("UPDATE category SET updated_at = '2020-01-01T00:00:00.000Z'").run();
provider.db.prepare("UPDATE article SET updated_at = '2020-01-01T00:00:00.000Z'").run();
console.log('3. Testing setOff on parent category Cat 4 (should disable Cat 4, SubCat 41, and SubCat 411)...');
const offResult = await store.posterminal.setOff(googleAuthId, terminal.id, cat4.id);
console.log('Category IDs after setOff(cat4):', offResult.categoryIds);
assert(!offResult.categoryIds.includes(cat4.id), 'Should NOT include cat4');
assert(!offResult.categoryIds.includes(subCat41.id), 'Should NOT include subCat41');
assert(!offResult.categoryIds.includes(subCat411.id), 'Should NOT include subCat411');
assert(offResult.categoryIds.includes(cat5.id), 'Should still include cat5');
// Check that categories in the subtree were touched on setOff
const catsAfterOff = provider.db.prepare('SELECT id, updated_at FROM category WHERE id IN (?, ?, ?)').all(cat4.id, subCat41.id, subCat411.id);
for (const c of catsAfterOff) {
assert.notEqual(c.updated_at, '2020-01-01T00:00:00.000Z', `Category ${c.id} updated_at should be touched on setOff`);
}
// Check that article in the subtree was touched on setOff
const artAfterOff = provider.db.prepare('SELECT id, updated_at FROM article WHERE id = ?').get(artCola.id);
assert.notEqual(artAfterOff.updated_at, '2020-01-01T00:00:00.000Z', 'Article updated_at should be touched on setOff');
// Verify obituaries generated
const obituariesAfter = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ? ORDER BY id ASC')
.all(terminal.id);
console.log('Obituaries after setOff:', obituariesAfter);
const catObituaryIds = obituariesAfter.filter(o => o.entity_type === 'category').map(o => o.entity_id);
assert(catObituaryIds.includes(cat4.id), 'Should have obituary for cat4');
assert(catObituaryIds.includes(subCat41.id), 'Should have obituary for subCat41');
assert(catObituaryIds.includes(subCat411.id), 'Should have obituary for subCat411');
const artObituaryIds = obituariesAfter.filter(o => o.entity_type === 'article').map(o => o.entity_id);
assert(artObituaryIds.includes(artCola.id), 'Should have obituary for artCola');
assert(!artObituaryIds.includes(artSnack.id), 'Should NOT have obituary for artSnack');
console.log('4. Testing setOn on an intermediate subcategory (SubCat 41)...');
const subOnResult = await store.posterminal.setOn(googleAuthId, terminal.id, subCat41.id);
console.log('Category IDs after setOn(subCat41):', subOnResult.categoryIds);
assert(!subOnResult.categoryIds.includes(cat4.id), 'Parent cat4 should not be enabled when enabling subCat41');
assert(subOnResult.categoryIds.includes(subCat41.id), 'subCat41 should be enabled');
assert(subOnResult.categoryIds.includes(subCat411.id), 'Child subCat411 should be enabled');
console.log('✅ ALL SUBCATEGORY SET_ON / SET_OFF TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
test()
.then(() => {
process.exit(0);
})
.catch(err => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,179 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing deposit via mandated optionslist ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-deposit-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Deposit Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create a category
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Getränke',
});
// 2. Create terminal and enable category
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-xyz',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
// 3. Create beverage article
const clubMate = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'Club Mate 0.5l',
sku: 'MATE-05',
});
await store.article.setPricesBySku(googleAuthId, clubMate.sku, [
{ net: 1.5, tax: 19 },
]);
// 4. Create deposit article
const pfandArt = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'Mehrwegpfand 0,08 €',
sku: 'PFAND-0.08',
track_stock: 0,
});
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
{ gross: 0.08, tax: 19 },
]);
// 5. Query products BEFORE linking mandated option
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
let mateProd = prods.find((p) => p.sku === 'MATE-05');
let pfandProd = prods.find((p) => p.sku === 'PFAND-0.08');
assert.ok(mateProd, 'Club Mate should be listed');
assert.equal(mateProd.deposit, '0');
assert.equal(mateProd.d_price, '0.0');
assert.equal(mateProd.deposit_name, '');
assert.ok(pfandProd, 'Pfand article should be listed');
assert.equal(pfandProd.deposit, '0');
// 6. Link Mehrwegpfand as mandated option to Club Mate
console.log('Linking Mehrwegpfand as mandated option...');
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, [
{
articleId: pfandArt.id,
sku: pfandArt.sku,
count: 1,
sort: 0,
mandated: true,
},
]);
// 7. Query products AFTER linking mandated option
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
console.log('Club Mate with deposit option:', {
sku: mateProd.sku,
deposit: mateProd.deposit,
d_price: mateProd.d_price,
deposit_name: mateProd.deposit_name,
});
assert.equal(mateProd.deposit, '1');
assert.equal(mateProd.d_price, '0.08');
assert.equal(mateProd.deposit_name, 'Mehrwegpfand 0,08 €');
// 8. Test option name change touching parent article
console.log('Testing option name change touching parent article...');
const mateBeforeRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
await new Promise((r) => setTimeout(r, 1100)); // wait >1s for timestamp delta
await store.article.updateBySku(googleAuthId, pfandArt.sku, {
name: 'Glas-Mehrwegpfand 0,08 €',
});
const mateAfterRename = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
assert.notEqual(
mateBeforeRename,
mateAfterRename,
'Club Mate updated_at must be bumped when its option article is renamed'
);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.deposit_name, 'Glas-Mehrwegpfand 0,08 €');
console.log('Renamed deposit reflected on parent product:', mateProd.deposit_name);
// 9. Test option price change touching parent article
console.log('Testing option price change touching parent article...');
const mateBeforePrice = mateAfterRename;
await new Promise((r) => setTimeout(r, 1100));
await store.article.setPricesBySku(googleAuthId, pfandArt.sku, [
{ gross: 0.15, tax: 19 },
]);
const mateAfterPrice = (await provider.query('SELECT updated_at FROM article WHERE id = @id', { id: clubMate.id })).rows[0].updated_at;
assert.notEqual(
mateBeforePrice,
mateAfterPrice,
'Club Mate updated_at must be bumped when its option article price changes'
);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.d_price, '0.15');
console.log('New deposit price reflected on parent product:', mateProd.d_price);
// 10. Test clearing options
console.log('Clearing options...');
await store.optionslist.setOptionsByArticleSku(googleAuthId, clubMate.sku, []);
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
mateProd = prods.find((p) => p.sku === 'MATE-05');
assert.equal(mateProd.deposit, '0');
assert.equal(mateProd.d_price, '0.0');
assert.equal(mateProd.deposit_name, '');
console.log('✅ ALL DEPOSIT OPTIONS TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -0,0 +1,159 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function main() {
console.log('--- Testing updated_at touch on all inventory movements ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-touch-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Inventory Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// Create warehouse
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Waren',
});
// Create article with past updated_at
const art = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
sku: 'TEST-SKU',
name: 'Test Article',
track_stock: 1,
});
const oldTime = '2020-01-01T00:00:00.000Z';
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
const getUpdatedAt = async (articleId) => {
const res = await provider.query(`SELECT updated_at FROM article WHERE id = @articleId`, { articleId });
return res.rows[0]?.updated_at;
};
const initialUpdatedAt = await getUpdatedAt(art.id);
assert.equal(initialUpdatedAt, oldTime, 'Should start with old updated_at');
// 1. Test store.stock.adjust
console.log('1. Testing store.stock.adjust touches article.updated_at...');
await store.stock.adjust(googleAuthId, {
sku: 'TEST-SKU',
warehouseId: wh.id,
amount: 2,
// reason omitted so it defaults to "Manual stock addition (+2)"
});
const afterAdjustUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(afterAdjustUpdatedAt, oldTime, 'stock.adjust should touch updated_at');
assert.ok(new Date(afterAdjustUpdatedAt).getTime() > new Date(oldTime).getTime());
// Check history was recorded with exact text
const historyRes = await provider.query(`SELECT text FROM articlehistory WHERE article_id = @id`, { id: art.id });
const adjustmentEntry = historyRes.rows.find((h) => h.text.includes('Bestandskorrektur +2 (Manual stock addition (+2))'));
assert.ok(adjustmentEntry, 'Should have exact article history entry');
// Reset updated_at to old time
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
// 2. Test Stückliste component touch
console.log('2. Testing Stückliste parent is touched when component has stock adjustment...');
const bundle = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
sku: 'BUNDLE-1',
name: 'Bundle 1',
track_stock: 0,
});
await store.articlelist.setItemsByArticleSku(googleAuthId, 'BUNDLE-1', [
{ sku: 'TEST-SKU', count: 1 },
]);
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: bundle.id });
await store.stock.adjust(googleAuthId, {
sku: 'TEST-SKU',
warehouseId: wh.id,
amount: 5,
text: 'Restock component',
});
const bundleUpdatedAt = await getUpdatedAt(bundle.id);
assert.notEqual(bundleUpdatedAt, oldTime, 'Stückliste bundle updated_at should be touched when component changes stock');
// 3. Test delivery acceptance touches article.updated_at
console.log('3. Testing delivery touches article.updated_at...');
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier Inc' });
const delivery = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
date: new Date().toISOString(),
lines: [{ sku: 'TEST-SKU', amount: 10, net: 5, taxrate: 19 }],
});
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
await store.delivery.book(googleAuthId, { id: delivery.id, warehouseId: wh.id });
const deliveryUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(deliveryUpdatedAt, oldTime, 'delivery.book should touch article.updated_at');
// 4. Test customer shipment touches article.updated_at
console.log('4. Testing shipment touches article.updated_at...');
const groups = await store.customergroup.list(googleAuthId);
const customer = await store.entity.create(googleAuthId, { name: 'Customer A', customergroupId: groups[0].id });
const order = await store.order.create(googleAuthId, {
number: 'AUF-000001',
date: new Date().toISOString(),
entityId: customer.id,
lines: [{ sku: 'TEST-SKU', amount: 3, net: 10, taxrate: 19 }],
});
const carrier = await store.carrier.create(googleAuthId, { name: 'DHL' });
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
await store.shipment.create(googleAuthId, {
orderId: order.id,
lines: [{ sku: 'TEST-SKU', amount: 3 }],
warehouseId: wh.id,
shipping: {
carrierId: carrier.id,
type: 'standard',
state: 'shipped',
warehouseId: wh.id,
},
});
const shipmentUpdatedAt = await getUpdatedAt(art.id);
assert.notEqual(shipmentUpdatedAt, oldTime, 'shipment.create should touch article.updated_at');
console.log('✅ ALL INVENTORY TOUCH TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
main().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

164
scratch/test-variants.js Normal file
View File

@@ -0,0 +1,164 @@
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import LibDashStore from '../src/index.js';
import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js';
async function run() {
console.log('--- Testing POS father/child variants and options ---');
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
const provider = new BetterSqlite3Provider({
filename: dbFile,
storageDir,
});
await provider.initialize();
const store = await new LibDashStore(provider);
try {
const googleAuthId = await store.google_auth.add('tester@example.com');
const company = await store.company.create({
name: 'Variant Corp',
iso639: 'de',
taxrate: 19,
reducedtaxrate: 7,
googleAuthId,
});
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
// 1. Create a category
const cat = await store.category.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Kleidung',
});
// 2. Create terminal and enable category
const terminal = await store.posterminal.upsert(googleAuthId, {
name: 'POS 1',
pairingpin: '1234',
});
await store.posterminal.pair(terminal.id, {
token: 'pos-token-xyz',
clientserial: 'SN-001',
pairedAt: new Date().toISOString(),
});
await store.posterminal.setOn(googleAuthId, terminal.id, cat.id);
// 3. Create attribute name "Farbe"
const attrName = await store.attributename.create(googleAuthId, {
name: 'Farbe',
sort: 1,
exclusive: true,
});
// 4. Create base article and convert to parent
const tShirt = await store.article.createUnderParent({
googleAuthId,
parentId: cat.id,
name: 'T-Shirt Basic',
sku: 'TSHIRT',
});
await store.article.toggleParent(googleAuthId, tShirt.sku, {
isParent: true,
attributeNameId: attrName.id,
});
// 5. Add children "Rot" and "Blau"
console.log('Adding children "Rot" and "Blau"...');
const childRot = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Rot',
name: 'T-Shirt Basic - Rot',
});
const childBlau = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Blau',
name: 'T-Shirt Basic - Blau',
});
// 6. Query POS products
let prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
let parentProd = prods.find((p) => p.sku === 'TSHIRT');
let rotProd = prods.find((p) => p.sku === childRot.article.sku);
let blauProd = prods.find((p) => p.sku === childBlau.article.sku);
console.log('Parent product:', {
sku: parentProd.sku,
is_parent: parentProd.is_parent,
parent: parentProd.parent,
variants: parentProd.variants,
options: parentProd.options,
});
assert.equal(parentProd.is_parent, '1');
assert.equal(parentProd.parent, '0');
assert.equal(parentProd.variants, '');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau' }]);
console.log('Child Rot:', {
sku: rotProd.sku,
is_parent: rotProd.is_parent,
parent: rotProd.parent,
variants: rotProd.variants,
sort: rotProd.sort,
options: rotProd.options,
});
assert.equal(rotProd.is_parent, '0');
assert.equal(rotProd.parent, String(parentProd._id));
assert.equal(rotProd.variants, 'Rot');
assert.equal(rotProd.sort, '0');
assert.equal(rotProd.options, null);
console.log('Child Blau:', {
sku: blauProd.sku,
is_parent: blauProd.is_parent,
parent: blauProd.parent,
variants: blauProd.variants,
sort: blauProd.sort,
options: blauProd.options,
});
assert.equal(blauProd.is_parent, '0');
assert.equal(blauProd.parent, String(parentProd._id));
assert.equal(blauProd.variants, 'Blau');
assert.equal(blauProd.sort, '1');
assert.equal(blauProd.options, null);
// 7. Add third child "Grün" and verify options list update
console.log('Adding third child "Grün"...');
const childGruen = await store.article.addChild(googleAuthId, tShirt.sku, {
value: 'Grün',
name: 'T-Shirt Basic - Grün',
});
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
parentProd = prods.find((p) => p.sku === 'TSHIRT');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Blau,Grün' }]);
console.log('Updated parent options:', parentProd.options);
// 8. Remove child "Blau" and verify options list update
console.log('Removing child "Blau"...');
await store.article.removeChild(googleAuthId, tShirt.sku, childBlau.article.sku, { deleteArticle: true });
prods = await store.posterminal.getProductList({ posterminalId: terminal.id });
parentProd = prods.find((p) => p.sku === 'TSHIRT');
assert.deepEqual(parentProd.options, [{ name: 'Farbe', value: 'Rot,Grün' }]);
console.log('Parent options after removal:', parentProd.options);
console.log('✅ ALL FATHER/CHILD VARIANT TESTS PASSED');
} finally {
try {
rmSync(tempDir, { recursive: true, force: true });
} catch {}
}
}
run().catch((err) => {
console.error('Test failed:', err);
process.exit(1);
});

View File

@@ -240,6 +240,47 @@ async function run() {
console.log('Obituaries after setOnAll:', obituariesAfterSetOnAll); console.log('Obituaries after setOnAll:', obituariesAfterSetOnAll);
assert.equal(obituariesAfterSetOnAll.length, 0); assert.equal(obituariesAfterSetOnAll.length, 0);
// 14. Test setCategoryIdsBySku generates obituary when removed from POS-enabled category
console.log('14. Testing setCategoryIdsBySku obituary generation and clearing...');
// Setup terminal: only has Cat A enabled
await store.posterminal.setOffAll(googleAuthId, terminal.id);
// Clear obituaries from setOffAll to start clean
provider.db.prepare('DELETE FROM posterminalobituary WHERE posterminal_id = ?').run(terminal.id);
await store.posterminal.setOn(googleAuthId, terminal.id, catA.id);
// Create article in Cat A
const artMoving = await store.article.createUnderParent({
googleAuthId,
parentId: catA.id,
name: 'Moving Article',
});
// Should have 0 obituaries on terminal
let obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
assert.equal(obits.length, 0);
// Move article from Cat A to Cat B (which is not enabled on terminal)
console.log('Moving article to Cat B (not on terminal)...');
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catB.id]);
obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after moving to Cat B:', obits);
assert.equal(obits.length, 1);
assert.deepEqual(obits[0], { entity_type: 'article', entity_id: artMoving.id });
// Move article back to Cat A (enabled on terminal)
console.log('Moving article back to Cat A (on terminal)...');
await store.article.setCategoryIdsBySku(googleAuthId, artMoving.sku, [catA.id]);
obits = provider.db
.prepare('SELECT entity_type, entity_id FROM posterminalobituary WHERE posterminal_id = ?')
.all(terminal.id);
console.log('Obituaries after moving back to Cat A:', obits);
assert.equal(obits.length, 0);
console.log('✅ ALL POSTERMINALOBITUARY TESTS PASSED'); console.log('✅ ALL POSTERMINALOBITUARY TESTS PASSED');
} finally { } finally {
try { try {

View File

@@ -117,7 +117,8 @@ WHERE id = @articleId
export const TOGGLE_PARENT = ` export const TOGGLE_PARENT = `
UPDATE article UPDATE article
SET is_parent = @isParent, SET is_parent = @isParent,
parentattributename_id = @parentAttributeNameId parentattributename_id = @parentAttributeNameId,
updated_at = SYSUTCDATETIME()
WHERE id = @articleId WHERE id = @articleId
`; `;
@@ -222,6 +223,33 @@ SET unit_id = @unitId, unitamount = @unitamount, updated_at = SYSUTCDATETIME()
WHERE id = @articleId WHERE id = @articleId
`; `;
export const TOUCH_UPDATED_AT = `
UPDATE article
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 TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT = `
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE articlelist_id IN (
SELECT ala.articlelist_id
FROM articlelistarticle ala
WHERE ala.article_id = @articleId
)
`;
export const CREATE = ` export const CREATE = `
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock) INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
OUTPUT INSERTED.id OUTPUT INSERTED.id
@@ -248,6 +276,28 @@ INSERT INTO categoryarticle (category_id, article_id)
VALUES (@categoryId, @articleId) VALUES (@categoryId, @articleId)
`; `;
export const LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE = `
SELECT DISTINCT pt.id AS posterminal_id
FROM posterminal pt
INNER JOIN posterminalcategory pc ON pc.posterminal_id = pt.id
INNER JOIN categoryarticle ca ON ca.category_id = pc.category_id
WHERE ca.article_id = @articleId
AND pt.company_id = @companyId
AND pt.token IS NOT NULL
`;
export const INSERT_POSTERMINAL_OBITUARY = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
VALUES (@posterminalId, @entityType, @entityId)
`;
export const CLEAR_POSTERMINAL_OBITUARY = `
DELETE FROM posterminalobituary
WHERE posterminal_id = @posterminalId
AND entity_type = @entityType
AND entity_id = @entityId
`;
export const GET_BRAND_BY_COMPANY_AND_ID = ` export const GET_BRAND_BY_COMPANY_AND_ID = `
SELECT id SELECT id
FROM brand FROM brand

View File

@@ -93,20 +93,32 @@ FROM posterminalcategory
WHERE posterminal_id = @posterminalId WHERE posterminal_id = @posterminalId
`; `;
/** setOn: insert if not exists (single terminal + single category). */ /** setOn: enable a category and all its subcategories on a terminal. */
export const SET_ON = ` export const SET_ON = `
IF NOT EXISTS ( ;WITH cats AS (
SELECT 1 FROM posterminalcategory SELECT id FROM category WHERE id = @categoryId
WHERE posterminal_id = @posterminalId AND category_id = @categoryId UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
) )
INSERT INTO posterminalcategory (posterminal_id, category_id) INSERT INTO posterminalcategory (posterminal_id, category_id)
VALUES (@posterminalId, @categoryId) SELECT @posterminalId, cats.id
FROM cats
WHERE NOT EXISTS (
SELECT 1 FROM posterminalcategory pc
WHERE pc.posterminal_id = @posterminalId AND pc.category_id = cats.id
)
`; `;
/** setOff: remove a single category from a single terminal. */ /** setOff: remove a category and all its subcategories from a terminal. */
export const SET_OFF = ` export const SET_OFF = `
;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
DELETE FROM posterminalcategory DELETE FROM posterminalcategory
WHERE posterminal_id = @posterminalId AND category_id = @categoryId WHERE posterminal_id = @posterminalId
AND category_id IN (SELECT id FROM cats)
`; `;
/** setOnAll: enable ALL company categories on one terminal. */ /** setOnAll: enable ALL company categories on one terminal. */
@@ -170,7 +182,7 @@ WHERE EXISTS (
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor
`; `;
export const COUNT_CATEGORIES = ` export const COUNT_CATEGORIES = `
@@ -219,7 +231,7 @@ WHERE a.articlelist_id IS NOT NULL
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor
`; `;
export const MAX_ORDER_ID = ` export const MAX_ORDER_ID = `
@@ -263,7 +275,7 @@ SELECT TOP (@limit)
COALESCE(pr.net, 0) AS netPrice, COALESCE(pr.net, 0) AS netPrice,
COALESCE(ptax.rate, ctax.rate, 0) AS taxRate, COALESCE(ptax.rate, ctax.rate, 0) AS taxRate,
a.created_at AS createdAt, a.created_at AS createdAt,
DATEDIFF_BIG(second, '1970-01-01', a.updated_at) AS lastChanged, (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) AS lastChanged,
COALESCE( COALESCE(
( (
SELECT TOP 1 p.hash SELECT TOP 1 p.hash
@@ -288,13 +300,15 @@ SELECT TOP (@limit)
) AS barcode, ) AS barcode,
a.is_parent AS isParent, a.is_parent AS isParent,
a.parentarticle_id AS parentArticleId, a.parentarticle_id AS parentArticleId,
a.parentsort AS parentsort,
( (
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.parentsort) SELECT TOP 1 av.value
FROM article fa FROM articleattributevalue aav
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
INNER JOIN articleattributevalue aav ON aav.article_id = a.id LEFT JOIN article fa ON fa.id = a.parentarticle_id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id WHERE aav.article_id = a.id
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
ORDER BY CASE WHEN av.attribute_id = fa.parentattributename_id THEN 0 ELSE 1 END, aav.sort ASC, aav.id ASC
) AS variantName, ) AS variantName,
CASE CASE
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
@@ -319,6 +333,7 @@ SELECT TOP (@limit)
) AS sort, ) AS sort,
a.track_stock AS trackStock, a.track_stock AS trackStock,
CASE CASE
WHEN a.track_stock = 0 THEN 0
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id
) THEN COALESCE(( ) THEN COALESCE((
@@ -337,6 +352,7 @@ SELECT TOP (@limit)
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id)
) )
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
GROUP BY ca.id GROUP BY ca.id
) comp_stock ON comp_stock.comp_id = ala.article_id ) comp_stock ON comp_stock.comp_id = ala.article_id
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
@@ -348,6 +364,7 @@ SELECT TOP (@limit)
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
AND ( AND (
ma.sku = a.sku ma.sku = a.sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id)
@@ -371,8 +388,8 @@ WHERE EXISTS (
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor AND (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) > @cursor
ORDER BY DATEDIFF_BIG(second, '1970-01-01', a.updated_at) ASC, a.id ASC ORDER BY (DATEDIFF_BIG(second, '1970-01-01', a.updated_at) * 1000000 + (a.id % 1000000)) ASC
`; `;
export const LIST_POS_PRODUCT_CATEGORIES = ` export const LIST_POS_PRODUCT_CATEGORIES = `
@@ -483,6 +500,40 @@ WHERE pr.minmultiplier = 1
GROUP BY pr.article_id, pr.customergroup_id 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 LIST_POS_PARENT_OPTIONS = `
SELECT
fa.id AS parentArticleId,
COALESCE(fan.name, 'Variante') AS attributeName,
av.value AS variantValue
FROM article fa
INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = fa.company_id
INNER JOIN article child ON child.parentarticle_id = fa.id
LEFT JOIN attributename fan ON fan.id = fa.parentattributename_id
LEFT JOIN articleattributevalue aav ON aav.article_id = child.id
LEFT JOIN attributevalue av ON av.id = aav.attributevalue_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
WHERE fa.is_parent = 1
ORDER BY fa.id ASC, child.parentsort ASC, child.id ASC
`;
export const RECORD_OBITUARY = ` export const RECORD_OBITUARY = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id) INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT pt.id, @entityType, @entityId SELECT pt.id, @entityType, @entityId

View File

@@ -3,13 +3,17 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'article'); const queries = translateQueries(source, 'article');
export const { export const {
ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CHECK_ARTICLE_WAREHOUSE_STOCKS, CHECK_HAS_WAREHOUSE, CREATE, CREATE_CHILD, ADD_CATEGORY, ADD_EAN, ARTICLE_LIST_COLUMNS, CHECK_ARTICLE_WAREHOUSE_STOCKS, CHECK_HAS_WAREHOUSE,
CLEAR_POSTERMINAL_OBITUARY, CREATE, CREATE_CHILD,
DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, EAN_EXISTS_FOR_OTHER_ARTICLE, DELETE_CATEGORIES_BY_ARTICLE, DELETE_EANS_BY_ARTICLE, EAN_EXISTS, EAN_EXISTS_FOR_OTHER_ARTICLE,
GET_BRAND_BY_COMPANY_AND_ID, GET_BRAND_BY_COMPANY_AND_ID,
GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU, GET_CATEGORY_ID_BY_SKU, GET_CATEGORY_TREE_FLAT, GET_ID_BY_SKU,
LIST_CHILDREN_BY_PARENT_ID, INSERT_POSTERMINAL_OBITUARY,
LIST_CHILDREN_BY_PARENT_ID, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE,
SET_PARENT_SORT, SET_PARENT_SORT,
SET_IS_PARENT, TOGGLE_PARENT, 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,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT,
UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT, UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
} = queries; } = queries;

View File

@@ -4,18 +4,34 @@ import { translateQueries } from './translate.js';
const queries = translateQueries(source, 'posterminal'); const queries = translateQueries(source, 'posterminal');
export const { export const {
CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY, 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, LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF_ALL, UPDATE,
FIND_BY_PAIRINGPIN, PAIR, SET_MAX_KNOWN_OBITUARY_ID, FIND_BY_PAIRINGPIN, PAIR, SET_MAX_KNOWN_OBITUARY_ID,
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS, FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID, COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES, LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
LIST_POS_MANDATED_OPTIONS, LIST_POS_PARENT_OPTIONS,
TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES, TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES,
} = queries; } = queries;
export const SET_ON = ` export const SET_ON = `
INSERT INTO posterminalcategory (posterminal_id, category_id) WITH RECURSIVE cats AS (
VALUES (@posterminalId, @categoryId) SELECT id FROM category WHERE id = @categoryId
ON CONFLICT (posterminal_id, category_id) DO NOTHING UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT OR IGNORE INTO posterminalcategory (posterminal_id, category_id)
SELECT @posterminalId, id FROM cats
`;
export const SET_OFF = `
WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
DELETE FROM posterminalcategory
WHERE posterminal_id = @posterminalId
AND category_id IN (SELECT id FROM cats)
`; `;
export const SET_ON_ALL = ` export const SET_ON_ALL = `
@@ -24,9 +40,8 @@ WITH RECURSIVE cats AS (
UNION ALL UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
) )
INSERT INTO posterminalcategory (posterminal_id, category_id) INSERT OR IGNORE INTO posterminalcategory (posterminal_id, category_id)
SELECT @posterminalId, id FROM cats WHERE 1 SELECT @posterminalId, id FROM cats
ON CONFLICT (posterminal_id, category_id) DO NOTHING
`; `;
export const LIST_POS_CUSTOMER_GROUPS = ` export const LIST_POS_CUSTOMER_GROUPS = `
@@ -147,7 +162,7 @@ SELECT
COALESCE(pr.net, 0) AS netPrice, COALESCE(pr.net, 0) AS netPrice,
COALESCE(ptax.rate, ctax.rate, 0) AS taxRate, COALESCE(ptax.rate, ctax.rate, 0) AS taxRate,
a.created_at AS createdAt, a.created_at AS createdAt,
CAST(strftime('%s', a.updated_at) AS INTEGER) AS lastChanged, (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) AS lastChanged,
COALESCE( COALESCE(
( (
SELECT p.hash SELECT p.hash
@@ -175,13 +190,15 @@ SELECT
) AS barcode, ) AS barcode,
a.is_parent AS isParent, a.is_parent AS isParent,
a.parentarticle_id AS parentArticleId, a.parentarticle_id AS parentArticleId,
a.parentsort AS parentsort,
( (
SELECT fan.name || '|' || av.value || '|' || fan.sort || '|' || a.parentsort SELECT av.value
FROM article fa FROM articleattributevalue aav
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
INNER JOIN articleattributevalue aav ON aav.article_id = a.id LEFT JOIN article fa ON fa.id = a.parentarticle_id
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id WHERE aav.article_id = a.id
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id AND (fa.parentattributename_id IS NULL OR av.attribute_id = fa.parentattributename_id)
ORDER BY CASE WHEN av.attribute_id = fa.parentattributename_id THEN 0 ELSE 1 END, aav.sort ASC, aav.id ASC
LIMIT 1 LIMIT 1
) AS variantName, ) AS variantName,
CASE CASE
@@ -207,6 +224,7 @@ SELECT
) AS sort, ) AS sort,
a.track_stock AS trackStock, a.track_stock AS trackStock,
CASE CASE
WHEN a.track_stock = 0 THEN 0
WHEN a.articlelist_id IS NOT NULL AND EXISTS ( WHEN a.articlelist_id IS NOT NULL AND EXISTS (
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id
) THEN COALESCE(( ) THEN COALESCE((
@@ -225,6 +243,7 @@ SELECT
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id)
) )
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
GROUP BY ca.id GROUP BY ca.id
) comp_stock ON comp_stock.comp_id = ala.article_id ) comp_stock ON comp_stock.comp_id = ala.article_id
WHERE ala.articlelist_id = a.articlelist_id WHERE ala.articlelist_id = a.articlelist_id
@@ -236,6 +255,7 @@ SELECT
INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id INNER JOIN manifestarticle ma ON ma.id = l.manifestarticle_id
INNER JOIN manifest m ON m.id = ma.manifest_id INNER JOIN manifest m ON m.id = ma.manifest_id
WHERE m.company_id = a.company_id WHERE m.company_id = a.company_id
AND l.warehouse_id IS NOT NULL
AND ( AND (
ma.sku = a.sku ma.sku = a.sku
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id) OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = a.id)
@@ -259,8 +279,8 @@ WHERE EXISTS (
WHERE ca.article_id = a.id WHERE ca.article_id = a.id
AND pc.posterminal_id = @posterminalId AND pc.posterminal_id = @posterminalId
) )
AND CAST(strftime('%s', a.updated_at) AS INTEGER) > @cursor AND (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) > @cursor
ORDER BY lastChanged ASC, a.id ASC ORDER BY (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) ASC
LIMIT @limit LIMIT @limit
`; `;

View File

@@ -271,6 +271,9 @@ export default function addChild(provider) {
await initArticleWarehouseSummary(runner, { companyId, articleId: childId }); await initArticleWarehouseSummary(runner, { companyId, articleId: childId });
} }
// Touch parent article updated_at so POS and other consumers refresh parent options
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
return load(googleAuthId, childSku, runner); return load(googleAuthId, childSku, runner);
}; };

View File

@@ -8,6 +8,8 @@ import {
} from '../../providers/sqlite3/queries/articlepic.js'; } from '../../providers/sqlite3/queries/articlepic.js';
import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js'; import { CREATE as MSSQL_CREATE_PIC } from '../../providers/mssql/queries/pic.js';
import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.js'; import { CREATE as SQLITE_CREATE_PIC } from '../../providers/sqlite3/queries/pic.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 processAvifImage from '../../util/processAvifImage.js'; import processAvifImage from '../../util/processAvifImage.js';
import newFullHash from '../../util/newFullHash.js'; import newFullHash from '../../util/newFullHash.js';
import resolveArticleBySku from './resolveArticleBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js';
@@ -21,6 +23,7 @@ export default function addImage(provider) {
const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT; const getSortSql = isSqlite ? SQLITE_GET_NEXT_SORT : MSSQL_GET_NEXT_SORT;
const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC; const createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC; const addArticlePicSql = isSqlite ? SQLITE_ADD_ARTICLEPIC : MSSQL_ADD_ARTICLEPIC;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
/** /**
* Adds an image to an article by its SKU, processing full, thumb, and 32px-edge small-thumb AVIF variants. * Adds an image to an article by its SKU, processing full, thumb, and 32px-edge small-thumb AVIF variants.
@@ -107,6 +110,8 @@ export default function addImage(provider) {
}); });
} }
await runner.query(touchArticleSql, { articleId });
return { return {
picId, picId,
sort: effectiveSort, sort: effectiveSort,

View File

@@ -49,6 +49,9 @@ export default function del(provider) {
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id }); 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 }); const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
if (!deleted.rowsAffected) { if (!deleted.rowsAffected) {
throw new Error('Article not found'); throw new Error('Article not found');

View File

@@ -2,6 +2,8 @@ import sharp from 'sharp';
import { encodeRGBA } from 'skelehash-wasm'; import { encodeRGBA } from 'skelehash-wasm';
import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js'; import { DELETE_BY_ARTICLE_AND_HASH as MSSQL_QUERY } from '../../providers/mssql/queries/pic.js';
import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.js'; import { DELETE_BY_ARTICLE_AND_HASH as SQLITE_QUERY } from '../../providers/sqlite3/queries/pic.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 './resolveArticleBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js';
import { recordImageDeleteHistory } from '../articlehistory/record.js'; import { recordImageDeleteHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
@@ -9,6 +11,7 @@ import makeRunner from '../runner.js';
export default function deleteImage(provider) { export default function deleteImage(provider) {
const resolve = resolveArticleBySku(provider); const resolve = resolveArticleBySku(provider);
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY; const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
const touchArticleSql = provider?.dialect === 'sqlite3' ? SQLITE_TOUCH : MSSQL_TOUCH;
/** /**
* Deletes an image from an article by its SKU and full image hash. * Deletes an image from an article by its SKU and full image hash.
@@ -95,6 +98,8 @@ export default function deleteImage(provider) {
fullHash, fullHash,
skelehash, skelehash,
}); });
await runner.query(touchArticleSql, { articleId });
}; };
if (tx) { if (tx) {

View File

@@ -1,5 +1,7 @@
import * as mssqlPic from '../../providers/mssql/queries/articlepic.js'; import * as mssqlPic from '../../providers/mssql/queries/articlepic.js';
import * as sqlitePic from '../../providers/sqlite3/queries/articlepic.js'; import * as sqlitePic from '../../providers/sqlite3/queries/articlepic.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 './resolveArticleBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js';
import { recordImageOrderHistory } from '../articlehistory/record.js'; import { recordImageOrderHistory } from '../articlehistory/record.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
@@ -8,6 +10,7 @@ export default function orderImages(provider) {
const resolve = resolveArticleBySku(provider); const resolve = resolveArticleBySku(provider);
const isSqlite = provider?.dialect === 'sqlite3'; const isSqlite = provider?.dialect === 'sqlite3';
const queries = isSqlite ? sqlitePic : mssqlPic; const queries = isSqlite ? sqlitePic : mssqlPic;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
/** /**
* Reorders the images of an article by its SKU. * Reorders the images of an article by its SKU.
@@ -69,6 +72,7 @@ export default function orderImages(provider) {
companyuserId, companyuserId,
fullHashes, fullHashes,
}); });
await runner.query(touchArticleSql, { articleId });
} }
return { fullHashes }; return { fullHashes };

View File

@@ -86,6 +86,9 @@ export default function removeChild(provider) {
childSku, childSku,
}); });
// Touch parent article updated_at so POS and other consumers refresh parent options
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
return { return {
success: true, success: true,
parentSku, parentSku,
@@ -131,6 +134,9 @@ export default function removeChild(provider) {
parentSku, parentSku,
}); });
// Touch parent article updated_at so POS and other consumers refresh parent options
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
return { return {
success: true, success: true,
parentSku, parentSku,

View File

@@ -1,10 +1,18 @@
import { import {
ADD_CATEGORY as MSSQL_ADD_CATEGORY, ADD_CATEGORY as MSSQL_ADD_CATEGORY,
CLEAR_POSTERMINAL_OBITUARY as MSSQL_CLEAR_OBITUARY,
DELETE_CATEGORIES_BY_ARTICLE as MSSQL_DELETE_CATEGORIES, DELETE_CATEGORIES_BY_ARTICLE as MSSQL_DELETE_CATEGORIES,
INSERT_POSTERMINAL_OBITUARY as MSSQL_INSERT_OBITUARY,
LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE as MSSQL_LIST_TERMINALS,
TOUCH_UPDATED_AT as MSSQL_TOUCH,
} from '../../providers/mssql/queries/article.js'; } from '../../providers/mssql/queries/article.js';
import { import {
ADD_CATEGORY as SQLITE_ADD_CATEGORY, ADD_CATEGORY as SQLITE_ADD_CATEGORY,
CLEAR_POSTERMINAL_OBITUARY as SQLITE_CLEAR_OBITUARY,
DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES, DELETE_CATEGORIES_BY_ARTICLE as SQLITE_DELETE_CATEGORIES,
INSERT_POSTERMINAL_OBITUARY as SQLITE_INSERT_OBITUARY,
LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE as SQLITE_LIST_TERMINALS,
TOUCH_UPDATED_AT as SQLITE_TOUCH,
} from '../../providers/sqlite3/queries/article.js'; } from '../../providers/sqlite3/queries/article.js';
import resolveCategoryAccess from '../category/resolveCategoryAccess.js'; import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
import queryBySku from './queryBySku.js'; import queryBySku from './queryBySku.js';
@@ -20,6 +28,10 @@ export default function setCategoryIdsBySku(provider) {
const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY; const addCategorySql = isSqlite ? SQLITE_ADD_CATEGORY : MSSQL_ADD_CATEGORY;
const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES; const deleteCategoriesSql = isSqlite ? SQLITE_DELETE_CATEGORIES : MSSQL_DELETE_CATEGORIES;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const listTerminalsSql = isSqlite ? SQLITE_LIST_TERMINALS : MSSQL_LIST_TERMINALS;
const insertObituarySql = isSqlite ? SQLITE_INSERT_OBITUARY : MSSQL_INSERT_OBITUARY;
const clearObituarySql = isSqlite ? SQLITE_CLEAR_OBITUARY : MSSQL_CLEAR_OBITUARY;
/** /**
* Replaces the category membership of an article by its SKU. * Replaces the category membership of an article by its SKU.
@@ -74,6 +86,15 @@ export default function setCategoryIdsBySku(provider) {
} }
} }
// Find paired POS terminals where this article was visible before the category change
const terminalsBeforeRes = await runner.query(listTerminalsSql, {
articleId,
companyId,
});
const terminalsBefore = new Set(
terminalsBeforeRes.rows.map((r) => r.posterminal_id),
);
await runner.query(deleteCategoriesSql, { articleId }); await runner.query(deleteCategoriesSql, { articleId });
for (const categoryId of normalized) { for (const categoryId of normalized) {
@@ -83,6 +104,42 @@ export default function setCategoryIdsBySku(provider) {
}); });
} }
// Find paired POS terminals where this article is visible after the category change
const terminalsAfterRes = await runner.query(listTerminalsSql, {
articleId,
companyId,
});
const terminalsAfter = new Set(
terminalsAfterRes.rows.map((r) => r.posterminal_id),
);
// 1. Terminals that had the article enabled before, but no longer have it in any enabled category:
// Record an obituary so POS sync removes the orphaned product.
for (const posterminalId of terminalsBefore) {
if (!terminalsAfter.has(posterminalId)) {
await runner.query(clearObituarySql, {
posterminalId,
entityType: 'article',
entityId: articleId,
});
await runner.query(insertObituarySql, {
posterminalId,
entityType: 'article',
entityId: articleId,
});
}
}
// 2. Terminals where the article IS enabled now:
// Clear any pending obituary so POS sync does not delete the active product.
for (const posterminalId of terminalsAfter) {
await runner.query(clearObituarySql, {
posterminalId,
entityType: 'article',
entityId: articleId,
});
}
// Write history if categories changed // Write history if categories changed
await recordCategoryChangeHistory(runner, { await recordCategoryChangeHistory(runner, {
articleId, articleId,
@@ -91,6 +148,9 @@ export default function setCategoryIdsBySku(provider) {
newCategoryIds: normalized, newCategoryIds: normalized,
}); });
// Touch article updated_at so POS and other consumers pick up category assignment changes
await runner.query(touchArticleSql, { articleId });
const article = await load(articleId, languageId, runner); const article = await load(articleId, languageId, runner);
if (!article) { if (!article) {
throw new Error('Article not found'); throw new Error('Article not found');

View File

@@ -2,11 +2,13 @@ import {
ADD_EAN as MSSQL_ADD_EAN, ADD_EAN as MSSQL_ADD_EAN,
DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS, DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS,
EAN_EXISTS_FOR_OTHER_ARTICLE as MSSQL_CHECK_CONFLICT, EAN_EXISTS_FOR_OTHER_ARTICLE as MSSQL_CHECK_CONFLICT,
TOUCH_UPDATED_AT as MSSQL_TOUCH,
} from '../../providers/mssql/queries/article.js'; } from '../../providers/mssql/queries/article.js';
import { import {
ADD_EAN as SQLITE_ADD_EAN, ADD_EAN as SQLITE_ADD_EAN,
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS, DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT, EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT,
TOUCH_UPDATED_AT as SQLITE_TOUCH,
} from '../../providers/sqlite3/queries/article.js'; } from '../../providers/sqlite3/queries/article.js';
import { isUniqueConstraintError } from '../../providers/provider.js'; import { isUniqueConstraintError } from '../../providers/provider.js';
import queryBySku from './queryBySku.js'; import queryBySku from './queryBySku.js';
@@ -22,6 +24,7 @@ export default function setEansBySku(provider) {
const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN; const addEanSql = isSqlite ? SQLITE_ADD_EAN : MSSQL_ADD_EAN;
const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS; const deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT; const checkConflictSql = isSqlite ? SQLITE_CHECK_CONFLICT : MSSQL_CHECK_CONFLICT;
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
/** /**
* Replaces the EAN list of an article by its SKU. * Replaces the EAN list of an article by its SKU.
@@ -98,6 +101,9 @@ export default function setEansBySku(provider) {
newEans: normalized, newEans: normalized,
}); });
// Touch article updated_at so POS and other consumers pick up the barcode change
await runner.query(touchArticleSql, { articleId });
const article = await load(articleId, languageId, runner); const article = await load(articleId, languageId, runner);
if (!article) { if (!article) {
throw new Error('Article not found'); throw new Error('Article not found');

View File

@@ -6,6 +6,14 @@ import {
CREATE as SQLITE_CREATE_PRICE, CREATE as SQLITE_CREATE_PRICE,
DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE, DELETE_BY_ARTICLE_AND_CUSTOMERGROUP as SQLITE_DELETE_PRICE,
} from '../../providers/sqlite3/queries/price.js'; } from '../../providers/sqlite3/queries/price.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 taxEnsure from '../tax/ensure.js';
import queryBySku from './queryBySku.js'; import queryBySku from './queryBySku.js';
import resolveArticleBySku from './resolveArticleBySku.js'; import resolveArticleBySku from './resolveArticleBySku.js';
@@ -65,6 +73,8 @@ export default function setPricesBySku(provider) {
const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE; const createPriceSql = isSqlite ? SQLITE_CREATE_PRICE : MSSQL_CREATE_PRICE;
const deletePriceSql = isSqlite ? SQLITE_DELETE_PRICE : MSSQL_DELETE_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. * Replaces the price list of an article by its SKU.
@@ -141,6 +151,12 @@ export default function setPricesBySku(provider) {
newPrices: normalized, newPrices: normalized,
}); });
// 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); const article = await load(articleId, languageId, runner);
if (!article) { if (!article) {
throw new Error('Article not found'); throw new Error('Article not found');

View File

@@ -232,6 +232,12 @@ 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); const article = await load(articleId, languageId, runner);
if (!article) { if (!article) {
throw new Error('Article not found'); throw new Error('Article not found');

View File

@@ -1,3 +1,12 @@
import {
TOUCH_UPDATED_AT as MSSQL_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
} from '../../providers/mssql/queries/article.js';
import {
TOUCH_UPDATED_AT as SQLITE_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
} from '../../providers/sqlite3/queries/article.js';
/** /**
* Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table. * Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table.
* *
@@ -142,6 +151,11 @@ export async function syncArticleWarehouseSummaryForLedger(runner, { companyId,
warehouseId, warehouseId,
stock, stock,
}); });
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
await runner.query(touchSql, { articleId });
await runner.query(touchStuecklisteSql, { articleId });
} }
} }

View File

@@ -11,6 +11,14 @@ import {
recordWarehouseHistory, recordWarehouseHistory,
} from '../articlehistory/record.js'; } from '../articlehistory/record.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import {
TOUCH_UPDATED_AT as MSSQL_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
} from '../../providers/mssql/queries/article.js';
import {
TOUCH_UPDATED_AT as SQLITE_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
} from '../../providers/sqlite3/queries/article.js';
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH'; export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
@@ -202,6 +210,10 @@ export default function add(provider) {
// Record history for acceptance // Record history for acceptance
for (const match of articleMatches) { for (const match of articleMatches) {
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
await runner.query(touchSql, { articleId: match.id });
await runner.query(touchStuecklisteSql, { articleId: match.id });
await recordAcceptanceHistory(runner, { await recordAcceptanceHistory(runner, {
articleId: match.id, articleId: match.id,
companyuserId, companyuserId,
@@ -287,6 +299,10 @@ export default function add(provider) {
}); });
for (const match of articleMatches) { for (const match of articleMatches) {
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
await runner.query(touchSql, { articleId: match.id });
await runner.query(touchStuecklisteSql, { articleId: match.id });
await recordShipmentHistory(runner, { await recordShipmentHistory(runner, {
articleId: match.id, articleId: match.id,
companyuserId, companyuserId,

View File

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

View File

@@ -5,7 +5,9 @@ import makeRunner from '../runner.js';
function normalizeCursor(cursor) { function normalizeCursor(cursor) {
const n = Number(cursor) || 0; const n = Number(cursor) || 0;
if (n <= 0) return 0; if (n <= 0) return 0;
return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); if (n < 1e11) return Math.floor(n * 1000000);
if (n < 1e14) return Math.floor((n / 1000) * 1000000);
return Math.floor(n);
} }
export default function getCompositeProductCount(provider) { export default function getCompositeProductCount(provider) {

View File

@@ -5,7 +5,9 @@ import makeRunner from '../runner.js';
function normalizeCursor(cursor) { function normalizeCursor(cursor) {
const n = Number(cursor) || 0; const n = Number(cursor) || 0;
if (n <= 0) return 0; if (n <= 0) return 0;
return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); if (n < 1e11) return Math.floor(n * 1000000);
if (n < 1e14) return Math.floor((n / 1000) * 1000000);
return Math.floor(n);
} }
export default function getProductCount(provider) { export default function getProductCount(provider) {

View File

@@ -3,19 +3,25 @@ import {
LIST_POS_PRODUCT_CATEGORIES as MSSQL_CATEGORIES, LIST_POS_PRODUCT_CATEGORIES as MSSQL_CATEGORIES,
LIST_POS_CUSTOMER_GROUP_IDS as MSSQL_CG_IDS, LIST_POS_CUSTOMER_GROUP_IDS as MSSQL_CG_IDS,
LIST_POS_PRODUCT_PRICES as MSSQL_PRICES, LIST_POS_PRODUCT_PRICES as MSSQL_PRICES,
LIST_POS_MANDATED_OPTIONS as MSSQL_MANDATED_OPTIONS,
LIST_POS_PARENT_OPTIONS as MSSQL_PARENT_OPTIONS,
} from '../../providers/mssql/queries/posterminal.js'; } from '../../providers/mssql/queries/posterminal.js';
import { import {
LIST_POS_PRODUCTS as SQLITE_PRODUCTS, LIST_POS_PRODUCTS as SQLITE_PRODUCTS,
LIST_POS_PRODUCT_CATEGORIES as SQLITE_CATEGORIES, LIST_POS_PRODUCT_CATEGORIES as SQLITE_CATEGORIES,
LIST_POS_CUSTOMER_GROUP_IDS as SQLITE_CG_IDS, LIST_POS_CUSTOMER_GROUP_IDS as SQLITE_CG_IDS,
LIST_POS_PRODUCT_PRICES as SQLITE_PRICES, LIST_POS_PRODUCT_PRICES as SQLITE_PRICES,
LIST_POS_MANDATED_OPTIONS as SQLITE_MANDATED_OPTIONS,
LIST_POS_PARENT_OPTIONS as SQLITE_PARENT_OPTIONS,
} from '../../providers/sqlite3/queries/posterminal.js'; } from '../../providers/sqlite3/queries/posterminal.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
function normalizeCursor(cursor) { function normalizeCursor(cursor) {
const n = Number(cursor) || 0; const n = Number(cursor) || 0;
if (n <= 0) return 0; if (n <= 0) return 0;
return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); if (n < 1e11) return Math.floor(n * 1000000);
if (n < 1e14) return Math.floor((n / 1000) * 1000000);
return Math.floor(n);
} }
function formatDateTime(value) { function formatDateTime(value) {
@@ -36,6 +42,8 @@ export default function getProductList(provider) {
const categoriesSql = isSqlite ? SQLITE_CATEGORIES : MSSQL_CATEGORIES; const categoriesSql = isSqlite ? SQLITE_CATEGORIES : MSSQL_CATEGORIES;
const cgIdsSql = isSqlite ? SQLITE_CG_IDS : MSSQL_CG_IDS; const cgIdsSql = isSqlite ? SQLITE_CG_IDS : MSSQL_CG_IDS;
const pricesSql = isSqlite ? SQLITE_PRICES : MSSQL_PRICES; const pricesSql = isSqlite ? SQLITE_PRICES : MSSQL_PRICES;
const mandatedOptionsSql = isSqlite ? SQLITE_MANDATED_OPTIONS : MSSQL_MANDATED_OPTIONS;
const parentOptionsSql = isSqlite ? SQLITE_PARENT_OPTIONS : MSSQL_PARENT_OPTIONS;
/** /**
* Lists products for a POS terminal modified after the cursor. * Lists products for a POS terminal modified after the cursor.
@@ -52,7 +60,7 @@ export default function getProductList(provider) {
const runner = makeRunner(provider, tx); const runner = makeRunner(provider, tx);
const pid = Number(posterminalId); const pid = Number(posterminalId);
const [prodRes, catRes, cgRes, priceRes] = await Promise.all([ const [prodRes, catRes, cgRes, priceRes, optRes, parentOptRes] = await Promise.all([
runner.query(productsSql, { runner.query(productsSql, {
posterminalId: pid, posterminalId: pid,
cursor: normalizeCursor(cursor), cursor: normalizeCursor(cursor),
@@ -61,6 +69,8 @@ export default function getProductList(provider) {
runner.query(categoriesSql, { posterminalId: pid }), runner.query(categoriesSql, { posterminalId: pid }),
runner.query(cgIdsSql, { posterminalId: pid }), runner.query(cgIdsSql, { posterminalId: pid }),
runner.query(pricesSql, { posterminalId: pid }), runner.query(pricesSql, { posterminalId: pid }),
runner.query(mandatedOptionsSql, { posterminalId: pid }),
runner.query(parentOptionsSql, { posterminalId: pid }),
]); ]);
const categoriesByArticle = new Map(); const categoriesByArticle = new Map();
@@ -79,6 +89,39 @@ export default function getProductList(provider) {
overridesByArticle.get(row.articleId).set(row.customerGroupId, row.netPrice); 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 parentOptionsMap = new Map();
for (const row of parentOptRes.rows) {
if (!row.variantValue) continue;
if (!parentOptionsMap.has(row.parentArticleId)) {
parentOptionsMap.set(row.parentArticleId, {
name: row.attributeName || 'Variante',
values: [],
});
}
const opt = parentOptionsMap.get(row.parentArticleId);
if (!opt.values.includes(row.variantValue)) {
opt.values.push(row.variantValue);
}
}
const customerGroupIds = cgRes.rows.map((row) => row.id); const customerGroupIds = cgRes.rows.map((row) => row.id);
return prodRes.rows.map((product) => { return prodRes.rows.map((product) => {
@@ -100,6 +143,28 @@ export default function getProductList(provider) {
}); });
const categoryIds = categoriesByArticle.get(product.id) ?? []; 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';
const isParent = Boolean(product.isParent);
const parentArticleId = Number(product.parentArticleId || 0);
const isChild = parentArticleId > 0;
const parentOpt = parentOptionsMap.get(product.id);
const options = isParent && parentOpt && parentOpt.values.length > 0
? [
{
name: parentOpt.name,
value: parentOpt.values.join(','),
},
]
: null;
const sortValue = isChild
? String(product.parentsort ?? 0)
: String(product.sort ?? 0);
return { return {
_id: String(product.id), _id: String(product.id),
@@ -112,17 +177,21 @@ export default function getProductList(provider) {
price: basePrice, price: basePrice,
created_at: formatDateTime(product.createdAt), created_at: formatDateTime(product.createdAt),
lastChanged: String(product.lastChanged), lastChanged: String(product.lastChanged),
sort: String(product.sort ?? 0), sort: sortValue,
categories_id: categoryIds[0] ?? '0', categories_id: categoryIds[0] ?? '0',
categories: categoryIds.map((categoryId) => ({ categoryId })), categories: categoryIds.map((categoryId) => ({ categoryId })),
prices, prices,
is_parent: product.isParent ? '1' : '0', is_parent: isParent ? '1' : '0',
parent: product.parentArticleId > 0 ? String(product.parentArticleId) : '0', parent: isChild ? String(parentArticleId) : '0',
variants: product.variantName ?? '', variants: isChild ? (product.variantName ?? '') : '',
isCompositeProduct: product.isCompositeProduct, isCompositeProduct: product.isCompositeProduct,
attributes: [], attributes: [],
options,
use_stock: product.trackStock ? '1' : '0', use_stock: product.trackStock ? '1' : '0',
quantity: String(product.fBestand ?? 0), quantity: String(product.fBestand ?? 0),
deposit: hasDeposit ? '1' : '0',
d_price: depositPrice,
deposit_name: depositName,
}; };
}); });
}; };

View File

@@ -21,25 +21,121 @@ export default function setOff(provider) {
? SQLITE_LIST_CATEGORY_IDS ? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS; : MSSQL_LIST_CATEGORY_IDS;
const addCategoryObituarySql = ` const touchSql = isSqlite
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id) ? `WITH RECURSIVE cats AS (
VALUES (@posterminalId, 'category', @categoryId) SELECT id FROM category WHERE id = @categoryId
`; UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
const addArticleObituariesSql = `
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT DISTINCT @posterminalId, 'article', ca.article_id
FROM categoryarticle ca
WHERE ca.category_id = @categoryId
AND NOT EXISTS (
SELECT 1
FROM categoryarticle ca2
INNER JOIN posterminalcategory pc ON pc.category_id = ca2.category_id
WHERE ca2.article_id = ca.article_id
AND pc.posterminal_id = @posterminalId
AND pc.category_id <> @categoryId
) )
`; UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (SELECT id FROM cats)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE category SET updated_at = SYSUTCDATETIME()
WHERE id IN (SELECT id FROM cats)`;
const touchArticlesSql = isSqlite
? `WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE article
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)`;
const addCategoryObituarySql = isSqlite
? `WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT pc.posterminal_id, 'category', pc.category_id
FROM posterminalcategory pc
WHERE pc.posterminal_id = @posterminalId
AND pc.category_id IN (SELECT id FROM cats)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT pc.posterminal_id, 'category', pc.category_id
FROM posterminalcategory pc
WHERE pc.posterminal_id = @posterminalId
AND pc.category_id IN (SELECT id FROM cats)`;
const addArticleObituariesSql = isSqlite
? `WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT DISTINCT @posterminalId, 'article', ca.article_id
FROM categoryarticle ca
WHERE ca.category_id IN (SELECT id FROM cats)
AND NOT EXISTS (
SELECT 1
FROM categoryarticle ca2
INNER JOIN posterminalcategory pc ON pc.category_id = ca2.category_id
WHERE ca2.article_id = ca.article_id
AND pc.posterminal_id = @posterminalId
AND pc.category_id NOT IN (SELECT id FROM cats)
)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
SELECT DISTINCT @posterminalId, 'article', ca.article_id
FROM categoryarticle ca
WHERE ca.category_id IN (SELECT id FROM cats)
AND NOT EXISTS (
SELECT 1
FROM categoryarticle ca2
INNER JOIN posterminalcategory pc ON pc.category_id = ca2.category_id
WHERE ca2.article_id = ca.article_id
AND pc.posterminal_id = @posterminalId
AND pc.category_id NOT IN (SELECT id FROM cats)
)`;
/** /**
* Removes a category assignment from a posterminal (turns it off). * Removes a category assignment from a posterminal (turns it off).
@@ -84,6 +180,9 @@ export default function setOff(provider) {
categoryId, categoryId,
}); });
await runner.query(touchSql, { categoryId });
await runner.query(touchArticlesSql, { categoryId });
const result = await runner.query(listCatIdsSql, { posterminalId }); const result = await runner.query(listCatIdsSql, { posterminalId });
return { return {

View File

@@ -21,30 +21,110 @@ export default function setOn(provider) {
? SQLITE_LIST_CATEGORY_IDS ? SQLITE_LIST_CATEGORY_IDS
: MSSQL_LIST_CATEGORY_IDS; : MSSQL_LIST_CATEGORY_IDS;
const touchSql = isSqlite const touchSql = isSqlite
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId" ? `WITH RECURSIVE cats AS (
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId'; SELECT id FROM category WHERE id = @categoryId
const touchArticlesSql = isSqlite UNION ALL
? `UPDATE article SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
)`
: `UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
)`;
const clearObituariesSql = `
DELETE FROM posterminalobituary
WHERE posterminal_id = @posterminalId
AND (
(entity_type = 'category' AND entity_id = @categoryId)
OR (entity_type = 'article' AND entity_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id = @categoryId
))
) )
`; UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (SELECT id FROM cats)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE category SET updated_at = SYSUTCDATETIME()
WHERE id IN (SELECT id FROM cats)`;
const touchArticlesSql = isSqlite
? `WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE article
SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
UPDATE article
SET updated_at = SYSUTCDATETIME()
WHERE id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
)`;
const clearObituariesSql = isSqlite
? `WITH RECURSIVE cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
DELETE FROM posterminalobituary
WHERE posterminal_id = @posterminalId
AND (
(entity_type = 'category' AND entity_id IN (SELECT id FROM cats))
OR (entity_type = 'article' AND entity_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
))
)`
: `;WITH cats AS (
SELECT id FROM category WHERE id = @categoryId
UNION ALL
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
)
DELETE FROM posterminalobituary
WHERE posterminal_id = @posterminalId
AND (
(entity_type = 'category' AND entity_id IN (SELECT id FROM cats))
OR (entity_type = 'article' AND entity_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
UNION
SELECT child.id FROM article child
WHERE child.parentarticle_id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
)
UNION
SELECT parent.parentarticle_id FROM article parent
WHERE parent.id IN (
SELECT ca.article_id FROM categoryarticle ca WHERE ca.category_id IN (SELECT id FROM cats)
) AND parent.parentarticle_id IS NOT NULL
))
)`;
/** /**
* Assigns a category to a posterminal (turns it on for that category). * Assigns a category to a posterminal (turns it on for that category).

View File

@@ -11,6 +11,14 @@ import ledgerAdd from '../ledger/add.js';
import makeRunner from '../runner.js'; import makeRunner from '../runner.js';
import { parseDate } from '../manifestLines.js'; import { parseDate } from '../manifestLines.js';
import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js'; import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js';
import {
TOUCH_UPDATED_AT as MSSQL_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
} from '../../providers/mssql/queries/article.js';
import {
TOUCH_UPDATED_AT as SQLITE_TOUCH,
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
} from '../../providers/sqlite3/queries/article.js';
const ADJUSTMENT_TYPE = 'adjustment'; const ADJUSTMENT_TYPE = 'adjustment';
@@ -160,6 +168,11 @@ export default function adjust(provider) {
warehouseId, warehouseId,
}); });
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
await runner.query(touchSql, { articleId: art.id });
await runner.query(touchStuecklisteSql, { articleId: art.id });
// Write warehouse history for tracked article // Write warehouse history for tracked article
if (manifestArticle.manifest_type === 'delivery') { if (manifestArticle.manifest_type === 'delivery') {
await recordWarehouseHistory(runner, { await recordWarehouseHistory(runner, {
@@ -369,6 +382,10 @@ export default function adjust(provider) {
// Write article history for the adjusted article // Write article history for the adjusted article
if (item.articleId) { if (item.articleId) {
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
await runner.query(touchSql, { articleId: item.articleId });
await runner.query(touchStuecklisteSql, { articleId: item.articleId });
await recordStockAdjustmentHistory(runner, { await recordStockAdjustmentHistory(runner, {
articleId: item.articleId, articleId: item.articleId,
companyuserId, companyuserId,