Compare commits
6 Commits
e9b4ac5d79
...
8002ca56e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8002ca56e6 | ||
|
|
8a085986e5 | ||
|
|
919713fa53 | ||
|
|
5fc316461d | ||
|
|
e31566e6ee | ||
|
|
d6e5da93cb |
171
scratch/test-category-subcategories.js
Normal file
171
scratch/test-category-subcategories.js
Normal 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);
|
||||
});
|
||||
179
scratch/test-deposit-options.js
Normal file
179
scratch/test-deposit-options.js
Normal 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);
|
||||
});
|
||||
159
scratch/test-inventory-touch.js
Normal file
159
scratch/test-inventory-touch.js
Normal 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
164
scratch/test-variants.js
Normal 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);
|
||||
});
|
||||
@@ -240,6 +240,47 @@ async function run() {
|
||||
console.log('Obituaries after setOnAll:', obituariesAfterSetOnAll);
|
||||
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');
|
||||
} finally {
|
||||
try {
|
||||
|
||||
@@ -117,7 +117,8 @@ WHERE id = @articleId
|
||||
export const TOGGLE_PARENT = `
|
||||
UPDATE article
|
||||
SET is_parent = @isParent,
|
||||
parentattributename_id = @parentAttributeNameId
|
||||
parentattributename_id = @parentAttributeNameId,
|
||||
updated_at = SYSUTCDATETIME()
|
||||
WHERE id = @articleId
|
||||
`;
|
||||
|
||||
@@ -222,6 +223,33 @@ SET unit_id = @unitId, unitamount = @unitamount, updated_at = SYSUTCDATETIME()
|
||||
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 = `
|
||||
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
|
||||
OUTPUT INSERTED.id
|
||||
@@ -248,6 +276,28 @@ INSERT INTO categoryarticle (category_id, article_id)
|
||||
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 = `
|
||||
SELECT id
|
||||
FROM brand
|
||||
|
||||
@@ -93,20 +93,32 @@ FROM posterminalcategory
|
||||
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 = `
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM posterminalcategory
|
||||
WHERE posterminal_id = @posterminalId AND category_id = @categoryId
|
||||
;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 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 = `
|
||||
;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
|
||||
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. */
|
||||
@@ -170,7 +182,7 @@ WHERE EXISTS (
|
||||
WHERE ca.article_id = a.id
|
||||
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 = `
|
||||
@@ -219,7 +231,7 @@ WHERE a.articlelist_id IS NOT NULL
|
||||
WHERE ca.article_id = a.id
|
||||
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 = `
|
||||
@@ -263,7 +275,7 @@ SELECT TOP (@limit)
|
||||
COALESCE(pr.net, 0) AS netPrice,
|
||||
COALESCE(ptax.rate, ctax.rate, 0) AS taxRate,
|
||||
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(
|
||||
(
|
||||
SELECT TOP 1 p.hash
|
||||
@@ -288,13 +300,15 @@ SELECT TOP (@limit)
|
||||
) AS barcode,
|
||||
a.is_parent AS isParent,
|
||||
a.parentarticle_id AS parentArticleId,
|
||||
a.parentsort AS parentsort,
|
||||
(
|
||||
SELECT TOP 1 CONCAT(fan.name, '|', av.value, '|', fan.sort, '|', a.parentsort)
|
||||
FROM article fa
|
||||
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
|
||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
|
||||
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
|
||||
SELECT TOP 1 av.value
|
||||
FROM articleattributevalue aav
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||
LEFT JOIN article fa ON fa.id = a.parentarticle_id
|
||||
WHERE aav.article_id = a.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,
|
||||
CASE
|
||||
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
||||
@@ -319,6 +333,7 @@ SELECT TOP (@limit)
|
||||
) AS sort,
|
||||
a.track_stock AS trackStock,
|
||||
CASE
|
||||
WHEN a.track_stock = 0 THEN 0
|
||||
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
||||
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id
|
||||
) 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)
|
||||
)
|
||||
WHERE m.company_id = a.company_id
|
||||
AND l.warehouse_id IS NOT NULL
|
||||
GROUP BY ca.id
|
||||
) comp_stock ON comp_stock.comp_id = ala.article_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 manifest m ON m.id = ma.manifest_id
|
||||
WHERE m.company_id = a.company_id
|
||||
AND l.warehouse_id IS NOT NULL
|
||||
AND (
|
||||
ma.sku = a.sku
|
||||
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
|
||||
AND pc.posterminal_id = @posterminalId
|
||||
)
|
||||
AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor
|
||||
ORDER BY DATEDIFF_BIG(second, '1970-01-01', a.updated_at) ASC, a.id ASC
|
||||
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) * 1000000 + (a.id % 1000000)) ASC
|
||||
`;
|
||||
|
||||
export const LIST_POS_PRODUCT_CATEGORIES = `
|
||||
@@ -483,6 +500,40 @@ 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 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 = `
|
||||
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
|
||||
SELECT pt.id, @entityType, @entityId
|
||||
|
||||
@@ -3,13 +3,17 @@ import { translateQueries } from './translate.js';
|
||||
|
||||
const queries = translateQueries(source, 'article');
|
||||
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,
|
||||
GET_BRAND_BY_COMPANY_AND_ID,
|
||||
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_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,
|
||||
} = queries;
|
||||
|
||||
|
||||
@@ -4,18 +4,34 @@ import { translateQueries } from './translate.js';
|
||||
const queries = translateQueries(source, 'posterminal');
|
||||
export const {
|
||||
CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY,
|
||||
LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF, SET_OFF_ALL, UPDATE,
|
||||
LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF_ALL, UPDATE,
|
||||
FIND_BY_PAIRINGPIN, PAIR, SET_MAX_KNOWN_OBITUARY_ID,
|
||||
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
||||
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
||||
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
||||
LIST_POS_MANDATED_OPTIONS, LIST_POS_PARENT_OPTIONS,
|
||||
TOUCH_LASTSEEN, RECORD_OBITUARY, COUNT_DELETED_ENTITIES,
|
||||
} = queries;
|
||||
|
||||
export const SET_ON = `
|
||||
INSERT INTO posterminalcategory (posterminal_id, category_id)
|
||||
VALUES (@posterminalId, @categoryId)
|
||||
ON CONFLICT (posterminal_id, category_id) DO NOTHING
|
||||
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 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 = `
|
||||
@@ -24,9 +40,8 @@ WITH RECURSIVE cats AS (
|
||||
UNION ALL
|
||||
SELECT c.id FROM category c INNER JOIN cats ON c.parent_id = cats.id
|
||||
)
|
||||
INSERT INTO posterminalcategory (posterminal_id, category_id)
|
||||
SELECT @posterminalId, id FROM cats WHERE 1
|
||||
ON CONFLICT (posterminal_id, category_id) DO NOTHING
|
||||
INSERT OR IGNORE INTO posterminalcategory (posterminal_id, category_id)
|
||||
SELECT @posterminalId, id FROM cats
|
||||
`;
|
||||
|
||||
export const LIST_POS_CUSTOMER_GROUPS = `
|
||||
@@ -147,7 +162,7 @@ SELECT
|
||||
COALESCE(pr.net, 0) AS netPrice,
|
||||
COALESCE(ptax.rate, ctax.rate, 0) AS taxRate,
|
||||
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(
|
||||
(
|
||||
SELECT p.hash
|
||||
@@ -175,13 +190,15 @@ SELECT
|
||||
) AS barcode,
|
||||
a.is_parent AS isParent,
|
||||
a.parentarticle_id AS parentArticleId,
|
||||
a.parentsort AS parentsort,
|
||||
(
|
||||
SELECT fan.name || '|' || av.value || '|' || fan.sort || '|' || a.parentsort
|
||||
FROM article fa
|
||||
INNER JOIN attributename fan ON fan.id = fa.parentattributename_id
|
||||
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id AND av.attribute_id = fan.id
|
||||
WHERE a.parentarticle_id IS NOT NULL AND fa.id = a.parentarticle_id
|
||||
SELECT av.value
|
||||
FROM articleattributevalue aav
|
||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||
LEFT JOIN article fa ON fa.id = a.parentarticle_id
|
||||
WHERE aav.article_id = a.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
|
||||
) AS variantName,
|
||||
CASE
|
||||
@@ -207,6 +224,7 @@ SELECT
|
||||
) AS sort,
|
||||
a.track_stock AS trackStock,
|
||||
CASE
|
||||
WHEN a.track_stock = 0 THEN 0
|
||||
WHEN a.articlelist_id IS NOT NULL AND EXISTS (
|
||||
SELECT 1 FROM articlelistarticle ala WHERE ala.articlelist_id = a.articlelist_id
|
||||
) THEN COALESCE((
|
||||
@@ -225,6 +243,7 @@ SELECT
|
||||
OR ma.sku IN (SELECT asup.sku FROM articlesupplier asup WHERE asup.article_id = ca.id)
|
||||
)
|
||||
WHERE m.company_id = a.company_id
|
||||
AND l.warehouse_id IS NOT NULL
|
||||
GROUP BY ca.id
|
||||
) comp_stock ON comp_stock.comp_id = ala.article_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 manifest m ON m.id = ma.manifest_id
|
||||
WHERE m.company_id = a.company_id
|
||||
AND l.warehouse_id IS NOT NULL
|
||||
AND (
|
||||
ma.sku = a.sku
|
||||
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
|
||||
AND pc.posterminal_id = @posterminalId
|
||||
)
|
||||
AND CAST(strftime('%s', a.updated_at) AS INTEGER) > @cursor
|
||||
ORDER BY lastChanged ASC, a.id ASC
|
||||
AND (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) > @cursor
|
||||
ORDER BY (CAST(strftime('%s', a.updated_at) AS INTEGER) * 1000000 + (a.id % 1000000)) ASC
|
||||
LIMIT @limit
|
||||
`;
|
||||
|
||||
|
||||
@@ -271,6 +271,9 @@ export default function addChild(provider) {
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
} from '../../providers/sqlite3/queries/articlepic.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 { 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 newFullHash from '../../util/newFullHash.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 createPicSql = isSqlite ? SQLITE_CREATE_PIC : MSSQL_CREATE_PIC;
|
||||
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.
|
||||
@@ -107,6 +110,8 @@ export default function addImage(provider) {
|
||||
});
|
||||
}
|
||||
|
||||
await runner.query(touchArticleSql, { articleId });
|
||||
|
||||
return {
|
||||
picId,
|
||||
sort: effectiveSort,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -2,6 +2,8 @@ import sharp from 'sharp';
|
||||
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 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 { recordImageDeleteHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
@@ -9,6 +11,7 @@ import makeRunner from '../runner.js';
|
||||
export default function deleteImage(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
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.
|
||||
@@ -95,6 +98,8 @@ export default function deleteImage(provider) {
|
||||
fullHash,
|
||||
skelehash,
|
||||
});
|
||||
|
||||
await runner.query(touchArticleSql, { articleId });
|
||||
};
|
||||
|
||||
if (tx) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import * as mssqlPic from '../../providers/mssql/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 { recordImageOrderHistory } from '../articlehistory/record.js';
|
||||
import makeRunner from '../runner.js';
|
||||
@@ -8,6 +10,7 @@ export default function orderImages(provider) {
|
||||
const resolve = resolveArticleBySku(provider);
|
||||
const isSqlite = provider?.dialect === 'sqlite3';
|
||||
const queries = isSqlite ? sqlitePic : mssqlPic;
|
||||
const touchArticleSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||
|
||||
/**
|
||||
* Reorders the images of an article by its SKU.
|
||||
@@ -69,6 +72,7 @@ export default function orderImages(provider) {
|
||||
companyuserId,
|
||||
fullHashes,
|
||||
});
|
||||
await runner.query(touchArticleSql, { articleId });
|
||||
}
|
||||
|
||||
return { fullHashes };
|
||||
|
||||
@@ -86,6 +86,9 @@ export default function removeChild(provider) {
|
||||
childSku,
|
||||
});
|
||||
|
||||
// Touch parent article updated_at so POS and other consumers refresh parent options
|
||||
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parentSku,
|
||||
@@ -131,6 +134,9 @@ export default function removeChild(provider) {
|
||||
parentSku,
|
||||
});
|
||||
|
||||
// Touch parent article updated_at so POS and other consumers refresh parent options
|
||||
await runner.query(queries.TOUCH_UPDATED_AT, { articleId: parentId });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
parentSku,
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import {
|
||||
ADD_CATEGORY as MSSQL_ADD_CATEGORY,
|
||||
CLEAR_POSTERMINAL_OBITUARY as MSSQL_CLEAR_OBITUARY,
|
||||
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';
|
||||
import {
|
||||
ADD_CATEGORY as SQLITE_ADD_CATEGORY,
|
||||
CLEAR_POSTERMINAL_OBITUARY as SQLITE_CLEAR_OBITUARY,
|
||||
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';
|
||||
import resolveCategoryAccess from '../category/resolveCategoryAccess.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 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.
|
||||
@@ -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 });
|
||||
|
||||
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
|
||||
await recordCategoryChangeHistory(runner, {
|
||||
articleId,
|
||||
@@ -91,6 +148,9 @@ export default function setCategoryIdsBySku(provider) {
|
||||
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);
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
|
||||
@@ -2,11 +2,13 @@ import {
|
||||
ADD_EAN as MSSQL_ADD_EAN,
|
||||
DELETE_EANS_BY_ARTICLE as MSSQL_DELETE_EANS,
|
||||
EAN_EXISTS_FOR_OTHER_ARTICLE as MSSQL_CHECK_CONFLICT,
|
||||
TOUCH_UPDATED_AT as MSSQL_TOUCH,
|
||||
} from '../../providers/mssql/queries/article.js';
|
||||
import {
|
||||
ADD_EAN as SQLITE_ADD_EAN,
|
||||
DELETE_EANS_BY_ARTICLE as SQLITE_DELETE_EANS,
|
||||
EAN_EXISTS_FOR_OTHER_ARTICLE as SQLITE_CHECK_CONFLICT,
|
||||
TOUCH_UPDATED_AT as SQLITE_TOUCH,
|
||||
} from '../../providers/sqlite3/queries/article.js';
|
||||
import { isUniqueConstraintError } from '../../providers/provider.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 deleteEansSql = isSqlite ? SQLITE_DELETE_EANS : MSSQL_DELETE_EANS;
|
||||
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.
|
||||
@@ -98,6 +101,9 @@ export default function setEansBySku(provider) {
|
||||
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);
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
|
||||
@@ -6,6 +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,
|
||||
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';
|
||||
@@ -65,6 +73,8 @@ 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.
|
||||
@@ -141,6 +151,12 @@ export default function setPricesBySku(provider) {
|
||||
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);
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
|
||||
@@ -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);
|
||||
if (!article) {
|
||||
throw new Error('Article not found');
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
@@ -142,6 +151,11 @@ export async function syncArticleWarehouseSummaryForLedger(runner, { companyId,
|
||||
warehouseId,
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,14 @@ import {
|
||||
recordWarehouseHistory,
|
||||
} from '../articlehistory/record.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';
|
||||
|
||||
@@ -202,6 +210,10 @@ export default function add(provider) {
|
||||
|
||||
// Record history for acceptance
|
||||
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, {
|
||||
articleId: match.id,
|
||||
companyuserId,
|
||||
@@ -287,6 +299,10 @@ export default function add(provider) {
|
||||
});
|
||||
|
||||
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, {
|
||||
articleId: match.id,
|
||||
companyuserId,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import makeRunner from '../runner.js';
|
||||
function normalizeCursor(cursor) {
|
||||
const n = Number(cursor) || 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) {
|
||||
|
||||
@@ -5,7 +5,9 @@ import makeRunner from '../runner.js';
|
||||
function normalizeCursor(cursor) {
|
||||
const n = Number(cursor) || 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) {
|
||||
|
||||
@@ -3,19 +3,25 @@ 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,
|
||||
LIST_POS_PARENT_OPTIONS as MSSQL_PARENT_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,
|
||||
LIST_POS_PARENT_OPTIONS as SQLITE_PARENT_OPTIONS,
|
||||
} from '../../providers/sqlite3/queries/posterminal.js';
|
||||
import makeRunner from '../runner.js';
|
||||
|
||||
function normalizeCursor(cursor) {
|
||||
const n = Number(cursor) || 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) {
|
||||
@@ -36,6 +42,8 @@ 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;
|
||||
const parentOptionsSql = isSqlite ? SQLITE_PARENT_OPTIONS : MSSQL_PARENT_OPTIONS;
|
||||
|
||||
/**
|
||||
* 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 pid = Number(posterminalId);
|
||||
|
||||
const [prodRes, catRes, cgRes, priceRes] = await Promise.all([
|
||||
const [prodRes, catRes, cgRes, priceRes, optRes, parentOptRes] = await Promise.all([
|
||||
runner.query(productsSql, {
|
||||
posterminalId: pid,
|
||||
cursor: normalizeCursor(cursor),
|
||||
@@ -61,6 +69,8 @@ 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 }),
|
||||
runner.query(parentOptionsSql, { posterminalId: pid }),
|
||||
]);
|
||||
|
||||
const categoriesByArticle = new Map();
|
||||
@@ -79,6 +89,39 @@ 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 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);
|
||||
|
||||
return prodRes.rows.map((product) => {
|
||||
@@ -100,6 +143,28 @@ 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';
|
||||
|
||||
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 {
|
||||
_id: String(product.id),
|
||||
@@ -112,17 +177,21 @@ export default function getProductList(provider) {
|
||||
price: basePrice,
|
||||
created_at: formatDateTime(product.createdAt),
|
||||
lastChanged: String(product.lastChanged),
|
||||
sort: String(product.sort ?? 0),
|
||||
sort: sortValue,
|
||||
categories_id: categoryIds[0] ?? '0',
|
||||
categories: categoryIds.map((categoryId) => ({ categoryId })),
|
||||
prices,
|
||||
is_parent: product.isParent ? '1' : '0',
|
||||
parent: product.parentArticleId > 0 ? String(product.parentArticleId) : '0',
|
||||
variants: product.variantName ?? '',
|
||||
is_parent: isParent ? '1' : '0',
|
||||
parent: isChild ? String(parentArticleId) : '0',
|
||||
variants: isChild ? (product.variantName ?? '') : '',
|
||||
isCompositeProduct: product.isCompositeProduct,
|
||||
attributes: [],
|
||||
options,
|
||||
use_stock: product.trackStock ? '1' : '0',
|
||||
quantity: String(product.fBestand ?? 0),
|
||||
deposit: hasDeposit ? '1' : '0',
|
||||
d_price: depositPrice,
|
||||
deposit_name: depositName,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -21,25 +21,121 @@ export default function setOff(provider) {
|
||||
? SQLITE_LIST_CATEGORY_IDS
|
||||
: MSSQL_LIST_CATEGORY_IDS;
|
||||
|
||||
const addCategoryObituarySql = `
|
||||
INSERT INTO posterminalobituary (posterminal_id, entity_type, entity_id)
|
||||
VALUES (@posterminalId, 'category', @categoryId)
|
||||
`;
|
||||
|
||||
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
|
||||
const touchSql = 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 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).
|
||||
@@ -84,6 +180,9 @@ export default function setOff(provider) {
|
||||
categoryId,
|
||||
});
|
||||
|
||||
await runner.query(touchSql, { categoryId });
|
||||
await runner.query(touchArticlesSql, { categoryId });
|
||||
|
||||
const result = await runner.query(listCatIdsSql, { posterminalId });
|
||||
|
||||
return {
|
||||
|
||||
@@ -21,30 +21,110 @@ export default function setOn(provider) {
|
||||
? SQLITE_LIST_CATEGORY_IDS
|
||||
: MSSQL_LIST_CATEGORY_IDS;
|
||||
const touchSql = isSqlite
|
||||
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||
const touchArticlesSql = isSqlite
|
||||
? `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 = @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
|
||||
))
|
||||
? `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 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).
|
||||
|
||||
@@ -11,6 +11,14 @@ import ledgerAdd from '../ledger/add.js';
|
||||
import makeRunner from '../runner.js';
|
||||
import { parseDate } from '../manifestLines.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';
|
||||
|
||||
@@ -160,6 +168,11 @@ export default function adjust(provider) {
|
||||
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
|
||||
if (manifestArticle.manifest_type === 'delivery') {
|
||||
await recordWarehouseHistory(runner, {
|
||||
@@ -369,6 +382,10 @@ export default function adjust(provider) {
|
||||
|
||||
// Write article history for the adjusted article
|
||||
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, {
|
||||
articleId: item.articleId,
|
||||
companyuserId,
|
||||
|
||||
Reference in New Issue
Block a user