Compare commits

...

2 Commits

Author SHA1 Message Date
seb
fe0d2e43f4 u 2026-08-23 04:12:14 +02:00
seb
5caedbe345 u 2026-08-23 03:53:08 +02:00
3 changed files with 94 additions and 32 deletions

View File

@@ -1,7 +1,7 @@
import { openSession, discardSession, closeSession } from './session.js'; import { openSession, discardSession, closeSession } from './session.js';
import { getOutgoingWarehouse, getWarehousePlace } from './warehouse.js'; import { getOutgoingWarehouse, getWarehousePlace } from './warehouse.js';
import { reservePositions } from './reserve.js'; import { reservePositions } from './reserve.js';
import { bookStockShortfallsAndRereserve } from './stock-shortage.js'; import { bookStockShortfallsAndRereserve, bookReturnPositions } from './stock-shortage.js';
import { commitPicklists } from './commit.js'; import { commitPicklists } from './commit.js';
import { deliverPicklists } from './deliver.js'; import { deliverPicklists } from './deliver.js';
import { logger } from '../../logger.js'; import { logger } from '../../logger.js';
@@ -29,35 +29,42 @@ export async function deliverOrder(transaction, kBenutzer, kAuftrag, kVersandArt
const kWarenLager = await getOutgoingWarehouse(transaction); const kWarenLager = await getOutgoingWarehouse(transaction);
const kWarenLagerPlatz = await getWarehousePlace(transaction, kWarenLager); const kWarenLagerPlatz = await getWarehousePlace(transaction, kWarenLager);
logger.info(`deliverOrder: kWarenLager=${kWarenLager} kWarenLagerPlatz=${kWarenLagerPlatz}`); logger.info(`deliverOrder: kWarenLager=${kWarenLager} kWarenLagerPlatz=${kWarenLagerPlatz}`);
const kSessionId = await openSession(transaction, kBenutzer);
try { const positiveItems = deliveredItems.filter((i) => i.quantity > 0);
await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, deliveredItems); const returnItems = deliveredItems.filter((i) => i.quantity < 0);
await bookStockShortfallsAndRereserve(
transaction, // 1. Process returned items: book them back into warehouse stock via Wareneingang
kBenutzer, if (returnItems.length) {
kSessionId, await bookReturnPositions(transaction, kBenutzer, kWarenLagerPlatz, returnItems);
kWarenLager, }
kWarenLagerPlatz,
deliveredItems, // 2. Process positive items: standard JTL picklist/delivery fulfillment
); if (positiveItems.length) {
await commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag); const kSessionId = await openSession(transaction, kBenutzer);
await deliverPicklists(transaction, kBenutzer, kSessionId, kAuftrag, kVersandArt);
logger.info(`deliverOrder: delivered kAuftrag=${kAuftrag} kSessionId=${kSessionId}`);
} catch (err) {
logger.error(`deliverOrder: FAILED kAuftrag=${kAuftrag} kSessionId=${kSessionId}: ${err.message}`);
throw err;
} finally {
// Safe to run unconditionally (success or error): spPicklistenVerwerfen only
// ever removes not-yet-delivered (nStatus < 10) Picklisten for this session.
// Swallow cleanup errors so they never mask an earlier, more relevant error
// (the outer transaction rollback in create-order.js is what actually matters
// on failure).
try { try {
await discardSession(transaction, kBenutzer, kSessionId); await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, positiveItems);
await closeSession(transaction, kSessionId); await bookStockShortfallsAndRereserve(
} catch { transaction,
// best-effort cleanup only kBenutzer,
kSessionId,
kWarenLager,
kWarenLagerPlatz,
positiveItems,
);
await commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag);
await deliverPicklists(transaction, kBenutzer, kSessionId, kAuftrag, kVersandArt);
logger.info(`deliverOrder: delivered kAuftrag=${kAuftrag} kSessionId=${kSessionId}`);
} catch (err) {
logger.error(`deliverOrder: FAILED kAuftrag=${kAuftrag} kSessionId=${kSessionId}: ${err.message}`);
throw err;
} finally {
try {
await discardSession(transaction, kBenutzer, kSessionId);
await closeSession(transaction, kSessionId);
} catch {
// best-effort cleanup only
}
} }
} }
} }

View File

@@ -21,7 +21,7 @@ async function getReservedQuantity(transaction, kSessionId, kAuftragPosition) {
return reserved; return reserved;
} }
async function getPositionArtikel(transaction, kAuftragPosition) { export async function getPositionArtikel(transaction, kAuftragPosition) {
const result = await new sql.Request(transaction) const result = await new sql.Request(transaction)
.input('kAuftragPosition', sql.Int, kAuftragPosition) .input('kAuftragPosition', sql.Int, kAuftragPosition)
.query(` .query(`
@@ -35,14 +35,22 @@ async function getPositionArtikel(transaction, kAuftragPosition) {
return row?.kArtikel ?? 0; return row?.kArtikel ?? 0;
} }
async function bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtikel, fehlmenge) { export async function bookWareneingang(
transaction,
kBenutzer,
kWarenLagerPlatz,
kArtikel,
fehlmenge,
comment = POS_SHORTAGE_COMMENT,
buchungsart = BUCHUNGSART_WARENEINGANG,
) {
await new sql.Request(transaction) await new sql.Request(transaction)
.input('kArtikel', sql.Int, kArtikel) .input('kArtikel', sql.Int, kArtikel)
.input('kWarenLagerPlatz', sql.Int, kWarenLagerPlatz) .input('kWarenLagerPlatz', sql.Int, kWarenLagerPlatz)
.input('kBenutzer', sql.Int, kBenutzer) .input('kBenutzer', sql.Int, kBenutzer)
.input('fAnzahl', sql.Float, fehlmenge) .input('fAnzahl', sql.Float, fehlmenge)
.input('cKommentar', sql.NVarChar, POS_SHORTAGE_COMMENT) .input('cKommentar', sql.NVarChar, comment)
.input('kBuchungsart', sql.Int, BUCHUNGSART_WARENEINGANG) .input('kBuchungsart', sql.Int, buchungsart)
.query(` .query(`
DECLARE @kWarenlagerEingang INT; DECLARE @kWarenlagerEingang INT;
EXEC dbo.spWarenlagerEingangSchreiben EXEC dbo.spWarenlagerEingangSchreiben
@@ -69,6 +77,37 @@ async function bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtik
`); `);
} }
export async function bookReturnPositions(
transaction,
kBenutzer,
kWarenLagerPlatz,
returnPositions,
) {
logger.info(`returnBooking: processing ${returnPositions.length} return position(s) at kWarenLagerPlatz=${kWarenLagerPlatz}`);
for (const { kAuftragPosition, quantity } of returnPositions) {
if (!kAuftragPosition || quantity >= 0) {
continue;
}
const returnQty = Math.abs(quantity);
const kArtikel = await getPositionArtikel(transaction, kAuftragPosition);
logger.info(`returnBooking: kBestellPos=${kAuftragPosition} kArtikel=${kArtikel} returnQty=${returnQty}`);
if (!kArtikel) {
logger.info(`returnBooking: kBestellPos=${kAuftragPosition} no kArtikel (free position / Pfand?), skip return booking`);
continue;
}
await bookWareneingang(
transaction,
kBenutzer,
kWarenLagerPlatz,
kArtikel,
returnQty,
'Korrekturbuchung erstellt durch POS-Abgleich (Retoure)',
BUCHUNGSART_WARENEINGANG,
);
logger.info(`returnBooking: successfully booked return of ${returnQty}x kArtikel=${kArtikel} into kWarenLagerPlatz=${kWarenLagerPlatz}`);
}
}
/** /**
* PosStockPositionService.FehlbestandEinbuchen — book missing stock, then re-reserve. * PosStockPositionService.FehlbestandEinbuchen — book missing stock, then re-reserve.
*/ */

View File

@@ -36,6 +36,21 @@ SELECT TOP (@limit)
FROM dbo.tkategorieartikel ka FROM dbo.tkategorieartikel ka
WHERE ka.kArtikel = a.kArtikel WHERE ka.kArtikel = a.kArtikel
) AS categoryIds, ) AS categoryIds,
(
SELECT COUNT(*)
FROM dbo.tKategorieArtikel ka2
INNER JOIN dbo.tArtikel a2 ON a2.kArtikel = ka2.kArtikel AND a2.cAktiv = 'Y'
INNER JOIN dbo.tArtikelBeschreibung ab2 ON ab2.kArtikel = a2.kArtikel AND ab2.kSprache = @languageId
WHERE ka2.kKategorie = (
SELECT TOP 1 ka1.kKategorie
FROM dbo.tKategorieArtikel ka1
WHERE ka1.kArtikel = a.kArtikel
)
AND (
ab2.cName < ab.cName
OR (ab2.cName = ab.cName AND a2.kArtikel < a.kArtikel)
)
) AS sort,
a.nIstVater AS isParent, a.nIstVater AS isParent,
a.kVaterArtikel AS parentArticleId, a.kVaterArtikel AS parentArticleId,
CASE WHEN a.kStueckliste <> 0 THEN '1' ELSE '0' END AS isCompositeProduct, CASE WHEN a.kStueckliste <> 0 THEN '1' ELSE '0' END AS isCompositeProduct,
@@ -204,6 +219,7 @@ export async function getProductList({ cursor = 0, limit = 20 } = {}) {
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),
categories_id: categoryIds[0] ?? '0', categories_id: categoryIds[0] ?? '0',
categories: categoryIds.map((categoryId) => ({ categoryId })), categories: categoryIds.map((categoryId) => ({ categoryId })),
prices, prices,