u
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
112
src/queries/delivery/stock-shortage.js
Normal file
112
src/queries/delivery/stock-shortage.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user