fix(delivery): deliver orders with father-articles and no-payment orders

- Treat an article that has become a father (nIstVater=1) as a free
  position (kArtikel=0) with the name kept: VersandIntern.vBestellPosOffen
  joins tArtikel with nIstVater=0, so such positions could never be
  reserved and failed with "insufficient stock after POS shortage
  booking" (e.g. Becks 42000030).
- Recalculate tAuftragPositionEckdaten before deliverOrder so orders
  without payments get their positions reserved too (previously only
  insertPayment triggered the recalculation).

Also adds debug logging across the delivery pipeline (reserve, commit,
deliver, session, stock-shortage, create-order).
This commit is contained in:
seb
2026-08-13 13:19:47 +02:00
parent 538ecf7a80
commit 5951e10ea5
14 changed files with 211 additions and 10 deletions

View File

@@ -1,6 +1,7 @@
import sql from 'mssql';
import { getPool } from '../db.js';
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
import { logger } from '../logger.js';
import { deliverOrder } from './delivery/index.js';
const config = {
@@ -449,8 +450,18 @@ async function insertOrderItem(transaction, kAuftrag, item) {
if (sku && positionType !== VERSANDPOSITION_TYPE) {
const result = await new sql.Request(transaction)
.input('cArtNr', sql.NVarChar, sku)
.query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr');
kArtikel = result.recordset[0]?.kArtikel ?? null;
.query('SELECT TOP 1 kArtikel, nIstVater FROM dbo.tArtikel WHERE cArtNr = @cArtNr');
const row = result.recordset[0];
kArtikel = row?.kArtikel ?? null;
// An article that has become a father (nIstVater = 1) can never be picked/delivered
// as a normal article: VersandIntern.vBestellPosOffen joins dbo.tArtikel with
// Art.nIstVater = 0, so the reservation SP can never reserve it and the order
// fails with "insufficient stock after POS shortage booking". Since the POS still
// sells it as a plain SKU, fall back to a free position (kArtikel = 0) with the
// name kept, which reserves/delivers like the Pfand lines do.
if (row?.nIstVater === 1) {
kArtikel = null;
}
}
const nType = positionType === VERSANDPOSITION_TYPE ? VERSANDPOSITION_TYPE : (kArtikel ? 1 : 0);
@@ -742,6 +753,7 @@ export async function createOrder(order) {
if (kAuftragPosition != null && !isVersandposition(item)) {
deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) });
}
logger.info(`createOrder: item sku="${String(item.sku ?? '').trim()}" type=${item.type} kAuftragPosition=${kAuftragPosition} qty=${toNumber(item.quantity, 1)} -> delivered=${kAuftragPosition != null && !isVersandposition(item)}`);
}
for (const payment of order.payments || []) {
@@ -749,6 +761,15 @@ export async function createOrder(order) {
await insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache);
}
// Positions must be reflected in tAuftragPositionEckdaten before the delivery
// reservation runs: VersandIntern.vBestellPosOffen joins tAuftragPositionEckdaten,
// and without a populated row the reservation SP sees fAnzahlZuPicken = 0 and
// reserves nothing ("insufficient stock after POS shortage booking"). insertPayment
// recalculates for paid orders; do it unconditionally here so no-payment orders
// (e.g. externalId=264) are delivered correctly too.
await recalculateAuftragEckdaten(transaction, kAuftrag);
logger.info(`createOrder: kAuftrag=${kAuftrag} isOrderDelivered=${isOrderDelivered(order)} deliveredItems=${JSON.stringify(deliveredItems)}`);
if (isOrderDelivered(order)) {
await deliverOrder(transaction, config.kBenutzer, kAuftrag, kVersandArt, deliveredItems);
}