This commit is contained in:
seb
2026-07-15 01:25:38 +02:00
parent 7fa839da4d
commit 056c9e18cd
21 changed files with 1181 additions and 64 deletions

View File

@@ -43,12 +43,22 @@ export async function handle(req, res) {
try {
const created = await createOrder(order);
logger.success(`order ${created.orderNumber} (kAuftrag=${created.orderId}) created for externalId=${externalOrderId}`);
results.push({
status: 'OK',
externalOrderId,
message: '',
});
if (created.alreadyExists) {
const msg = `order already mapped, skipped save (kAuftrag=${created.orderId}, ${created.orderNumber})`;
logger.error(`order externalId=${externalOrderId} skipped: ${msg}`);
results.push({
status: 'ERROR',
externalOrderId,
message: msg,
});
} else {
logger.success(`order ${created.orderNumber} (kAuftrag=${created.orderId}) created for externalId=${externalOrderId}`);
results.push({
status: 'OK',
externalOrderId,
message: '',
});
}
} catch (err) {
logger.error(`order externalId=${externalOrderId} failed: ${err.message}`);
results.push({
@@ -59,5 +69,7 @@ export async function handle(req, res) {
}
}
return sendJson(res, 200, results);
const failed = results.filter((r) => r.status === 'ERROR').length;
const httpStatus = failed > 0 ? 500 : 200;
return sendJson(res, httpStatus, results);
}

View File

@@ -248,6 +248,18 @@ function isWalkInOrder(order) {
return (billing.lastName || '') === 'Laufkunde' && !(billing.firstName || '').trim();
}
function resolveAuftragCKundenNr(order) {
return isWalkInOrder(order) ? '0' : String(order.customerNumber || '');
}
/** POS settings.invoiceSetting: 0 = external/POS invoice (nIstExterneRechnung=1). */
function resolveNIstExterneRechnung(order) {
const inv = order.settings?.invoiceSetting;
if (inv === undefined || inv === null) return 1;
if (inv === true || inv === 1 || inv === '1' || String(inv).toLowerCase() === 'true') return 0;
return 1;
}
async function lookupKassenkunde(transaction, defaults) {
const result = await new sql.Request(transaction).query(
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
@@ -374,9 +386,15 @@ async function insertOrderItem(transaction, kAuftrag, item) {
/**
* JTL PosOrderCreationService.CheckIfOrderExists — skip re-import when the POS
* order id is already mapped to a Wawi order for this subshop.
* order id is already mapped to a Wawi order for this subshop with the same
* external receipt number.
*/
async function findExistingPosOrderMapping(kPosAuftrag) {
function externalOrderNumbersMatch(mapped, incoming) {
if (!incoming) return true;
return String(mapped || '').toLowerCase() === String(incoming).toLowerCase();
}
async function findExistingPosOrderMapping(kPosAuftrag, externalOrderNumber) {
const kShopSubShop = getActiveShopSubshopId();
if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
return null;
@@ -387,7 +405,7 @@ async function findExistingPosOrderMapping(kPosAuftrag) {
.input('kPosAuftrag', sql.Int, kPosAuftrag)
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(`
SELECT TOP 1 m.kAuftrag, a.cAuftragsNr
SELECT TOP 1 m.kAuftrag, a.cAuftragsNr, ISNULL(a.cExterneAuftragsnummer, '') AS cExterneAuftragsnummer
FROM Pos.tAuftragMapping m
LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag
WHERE m.kPosAuftrag = @kPosAuftrag
@@ -400,6 +418,9 @@ async function findExistingPosOrderMapping(kPosAuftrag) {
if (!row?.kAuftrag) {
return null;
}
if (!externalOrderNumbersMatch(row.cExterneAuftragsnummer, externalOrderNumber)) {
return null;
}
return row;
}
@@ -412,20 +433,21 @@ async function upsertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
return;
}
await new sql.Request(transaction)
.input('kPosAuftrag', sql.Int, kPosAuftrag)
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(`
DELETE FROM Pos.tAuftragMapping
WHERE kPosAuftrag = @kPosAuftrag
AND kShopSubShop = @kShopSubShop
`);
await new sql.Request(transaction)
.input('kAuftrag', sql.Int, kAuftrag)
.input('kPosAuftrag', sql.Int, kPosAuftrag)
.input('kShopSubShop', sql.Int, kShopSubShop)
.query(`
MERGE INTO Pos.tAuftragMapping WITH (HOLDLOCK) AS Target
USING (SELECT @kAuftrag AS kAuftrag, @kPosAuftrag AS kPosAuftrag, @kShopSubShop AS kShopSubShop) AS Source
ON Target.kPosAuftrag = Source.kPosAuftrag
AND Target.kShopSubShop = Source.kShopSubShop
WHEN MATCHED THEN
UPDATE SET Target.kAuftrag = Source.kAuftrag
WHEN NOT MATCHED BY TARGET THEN
INSERT (kAuftrag, kPosAuftrag, kShopSubShop)
VALUES (Source.kAuftrag, Source.kPosAuftrag, Source.kShopSubShop);
INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop)
VALUES (@kAuftrag, @kPosAuftrag, @kShopSubShop)
`);
}
@@ -488,12 +510,14 @@ async function insertPayment(transaction, kAuftrag, payment, order, orderDate, z
export async function createOrder(order) {
const kPosAuftrag = Number.parseInt(order.externalId, 10);
const externalOrderNumber = order.externalOrderNumber || '';
if (Number.isInteger(kPosAuftrag) && kPosAuftrag > 0) {
const existing = await findExistingPosOrderMapping(kPosAuftrag);
const existing = await findExistingPosOrderMapping(kPosAuftrag, externalOrderNumber);
if (existing) {
return {
orderId: String(existing.kAuftrag),
orderNumber: existing.cAuftragsNr || '',
alreadyExists: true,
};
}
}
@@ -523,24 +547,25 @@ export async function createOrder(order) {
.input('cWaehrung', sql.NVarChar, order.currencyIso || 'EUR')
.input('kPlattform', sql.Int, defaults.kPlattform)
.input('kShop', sql.Int, getActiveShopId() || null)
.input('cKundenNr', sql.NVarChar, String(order.customerNumber || ''))
.input('cKundenNr', sql.NVarChar, resolveAuftragCKundenNr(order))
.input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase())
.input('kVersandArt', sql.Int, defaults.kVersandArt)
.input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
.input('kKundengruppe', sql.Int, kKundengruppe)
.input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '')
.input('nIstExterneRechnung', sql.Int, resolveNIstExterneRechnung(order))
.query(`
DECLARE @t TABLE ([kAuftrag] INT);
INSERT INTO Verkauf.tAuftrag
(cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor,
kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten,
cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe,
cExterneAuftragsnummer)
cExterneAuftragsnummer, nIstExterneRechnung, cInet, nIstReadOnly, kShopauftrag, nLieferPrioritaet)
OUTPUT inserted.kAuftrag INTO @t
VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0,
@kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0,
@cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe,
@cExterneAuftragsnummer);
@cExterneAuftragsnummer, @nIstExterneRechnung, 'Y', 2, 0, 10);
SELECT kAuftrag FROM @t;
`);
@@ -578,7 +603,7 @@ export async function createOrder(order) {
await transaction.commit();
return { orderId: String(kAuftrag), orderNumber: cAuftragsNr };
return { orderId: String(kAuftrag), orderNumber: cAuftragsNr, alreadyExists: false };
} catch (err) {
try {
await transaction.rollback();

View File

@@ -1,6 +1,7 @@
import { openSession, discardSession, closeSession } from './session.js';
import { getOutgoingWarehouse } from './warehouse.js';
import { getOutgoingWarehouse, getWarehousePlace } from './warehouse.js';
import { reservePositions } from './reserve.js';
import { bookStockShortfallsAndRereserve } from './stock-shortage.js';
import { commitPicklists } from './commit.js';
import { deliverPicklists } from './deliver.js';
@@ -23,10 +24,19 @@ export async function deliverOrder(transaction, kBenutzer, kAuftrag, kVersandArt
}
const kWarenLager = await getOutgoingWarehouse(transaction);
const kWarenLagerPlatz = await getWarehousePlace(transaction, kWarenLager);
const kSessionId = await openSession(transaction, kBenutzer);
try {
await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, deliveredItems);
await bookStockShortfallsAndRereserve(
transaction,
kBenutzer,
kSessionId,
kWarenLager,
kWarenLagerPlatz,
deliveredItems,
);
await commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag);
await deliverPicklists(transaction, kBenutzer, kSessionId, kAuftrag, kVersandArt);
} finally {

View File

@@ -0,0 +1,112 @@
import sql from 'mssql';
import { reservePositions } from './reserve.js';
const POS_SHORTAGE_COMMENT = 'Korrekturbuchung erstellt durch POS-Abgleich';
const BUCHUNGSART_WARENEINGANG = 10;
async function getReservedQuantity(transaction, kSessionId, kAuftragPosition) {
const result = await new sql.Request(transaction)
.input('kSessionId', sql.Int, kSessionId)
.input('kBestellPos', sql.Int, kAuftragPosition)
.query(`
SELECT ISNULL(SUM(pp.fAnzahl), 0) AS reserved
FROM dbo.tPicklistePos pp
INNER JOIN dbo.tPickliste p ON p.kPickliste = pp.kPickliste
WHERE p.kSessionId = @kSessionId
AND pp.kBestellPos = @kBestellPos
`);
return Number(result.recordset[0]?.reserved ?? 0);
}
async function getPositionArtikel(transaction, kAuftragPosition) {
const result = await new sql.Request(transaction)
.input('kAuftragPosition', sql.Int, kAuftragPosition)
.query(`
SELECT kArtikel
FROM Verkauf.tAuftragPosition
WHERE kAuftragPosition = @kAuftragPosition
`);
return result.recordset[0]?.kArtikel ?? 0;
}
async function bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtikel, fehlmenge) {
await new sql.Request(transaction)
.input('kArtikel', sql.Int, kArtikel)
.input('kWarenLagerPlatz', sql.Int, kWarenLagerPlatz)
.input('kBenutzer', sql.Int, kBenutzer)
.input('fAnzahl', sql.Float, fehlmenge)
.input('cKommentar', sql.NVarChar, POS_SHORTAGE_COMMENT)
.input('kBuchungsart', sql.Int, BUCHUNGSART_WARENEINGANG)
.query(`
DECLARE @kWarenlagerEingang INT;
EXEC dbo.spWarenlagerEingangSchreiben
@kArtikel = @kArtikel,
@kWarenLagerPlatz = @kWarenLagerPlatz,
@kLieferantenBestellungPos = 0,
@kBenutzer = @kBenutzer,
@fAnzahl = @fAnzahl,
@fEkEinzel = 0,
@cLieferscheinNr = '',
@cChargenNr = NULL,
@dMHD = NULL,
@dGeliefertAm = NULL,
@cKommentar = @cKommentar,
@kGutschriftPos = 0,
@kLHM = 0,
@kSessionId = 0,
@kBuchungsart = @kBuchungsart,
@kBestellPosUmlagerung = 0,
@kRMRetourePos = 0,
@nHistorieNichtSchreiben = 0,
@kWarenlagerEingang = @kWarenlagerEingang OUTPUT;
SELECT @kWarenlagerEingang AS kWarenlagerEingang;
`);
}
/**
* PosStockPositionService.FehlbestandEinbuchen — book missing stock, then re-reserve.
*/
export async function bookStockShortfallsAndRereserve(
transaction,
kBenutzer,
kSessionId,
kWarenLager,
kWarenLagerPlatz,
positions,
) {
const rereserve = [];
for (const { kAuftragPosition, quantity } of positions) {
if (!kAuftragPosition || quantity <= 0) {
continue;
}
const reserved = await getReservedQuantity(transaction, kSessionId, kAuftragPosition);
const shortage = quantity - reserved;
if (shortage <= 0.0001) {
continue;
}
const kArtikel = await getPositionArtikel(transaction, kAuftragPosition);
if (!kArtikel) {
continue;
}
await bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtikel, shortage);
rereserve.push({ kAuftragPosition, quantity: shortage });
}
if (rereserve.length) {
await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, rereserve);
}
for (const { kAuftragPosition, quantity } of positions) {
if (!kAuftragPosition || quantity <= 0) {
continue;
}
const reserved = await getReservedQuantity(transaction, kSessionId, kAuftragPosition);
if (reserved + 0.0001 < quantity) {
throw new Error(`insufficient stock after POS shortage booking for kBestellPos=${kAuftragPosition}`);
}
}
}

View File

@@ -2,6 +2,7 @@ import sql from 'mssql';
/** DB-resolved (or env-overridden) outgoing warehouse, cached after first lookup. */
let cachedWarenLager = null;
let cachedWarenLagerPlatz = null;
/**
* Resolves the local warehouse (dbo.tWarenLager.nFulfillment = 0) to book
@@ -36,3 +37,34 @@ export async function getOutgoingWarehouse(transaction) {
cachedWarenLager = row.kWarenLager;
return cachedWarenLager;
}
/**
* Default pick place for the outgoing warehouse (dbo.tWarenLagerPlatz).
*/
export async function getWarehousePlace(transaction, kWarenLager) {
if (cachedWarenLagerPlatz != null) {
return cachedWarenLagerPlatz;
}
const configured = Number(process.env.JTL_KWARENLAGERPLATZ);
if (Number.isInteger(configured) && configured > 0) {
cachedWarenLagerPlatz = configured;
return cachedWarenLagerPlatz;
}
const result = await new sql.Request(transaction)
.input('kWarenLager', sql.Int, kWarenLager)
.query(`
SELECT TOP 1 kWarenLagerPlatz
FROM dbo.tWarenLagerPlatz
WHERE kWarenLager = @kWarenLager
AND ISNULL(nGesperrt, 0) = 0
ORDER BY nPrio, kWarenLagerPlatz
`);
const row = result.recordset[0];
if (!row) {
throw new Error(`No warehouse place found for kWarenLager=${kWarenLager}; set JTL_KWARENLAGERPLATZ explicitly.`);
}
cachedWarenLagerPlatz = row.kWarenLagerPlatz;
return cachedWarenLagerPlatz;
}