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:
@@ -1,7 +1,14 @@
|
||||
# HTTPS POS server
|
||||
PORT=4443
|
||||
|
||||
# Plain HTTP POS server (no TLS) - same API, unencrypted
|
||||
HTTP_PORT=8087
|
||||
|
||||
AUTH_TOKEN=df40ad2067954646abb0499548a52241
|
||||
PAIRING_CODE=307018
|
||||
|
||||
# Allow generating fresh pairing codes on demand via GET /v1/newpin (default: false)
|
||||
NEWPIN_ENABLED=false
|
||||
LOG_FILE=logs/requests.log
|
||||
ORDER_LOG_FILE=logs/orders.log
|
||||
|
||||
|
||||
32
API.md
32
API.md
@@ -79,6 +79,38 @@ Discovers the server and completes pairing with a 6-digit code.
|
||||
|
||||
---
|
||||
|
||||
## `GET /v1/newpin` — Generate a pairing code
|
||||
|
||||
Creates a fresh, usable 6-digit pairing code on demand. Useful when the static `PAIRING_CODE` has already been consumed by a previous pair.
|
||||
|
||||
### Enabling
|
||||
|
||||
Disabled by default for security. Enable it by setting `NEWPIN_ENABLED=true` in the environment (`.env`). When disabled, the endpoint returns `403`.
|
||||
|
||||
### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"pin": "482913",
|
||||
"name": "JTL-POS",
|
||||
"serverTimestamp": "2026-08-13 12:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `pin` | Fresh 6-digit code; valid until consumed by `GET /v1/client` pairing |
|
||||
| `name` | Device name registered on pair (fixed `JTL-POS`) |
|
||||
| `serverTimestamp` | Server time at generation |
|
||||
|
||||
### Errors
|
||||
|
||||
| Status | When |
|
||||
|---|---|
|
||||
| 403 | `NEWPIN_ENABLED` is not `true` |
|
||||
|
||||
---
|
||||
|
||||
## `GET /v1/init` — Sync status
|
||||
|
||||
Returns how many entities changed since each cursor. Poll this; only fetch list endpoints when the matching `*_count` is greater than `"0"`.
|
||||
|
||||
51
nginx.conf.example
Normal file
51
nginx.conf.example
Normal file
@@ -0,0 +1,51 @@
|
||||
# nginx example: terminate TLS in nginx and proxy to the JTL POS plain-HTTP listener.
|
||||
#
|
||||
# The Node server listens on:
|
||||
# PORT -> HTTPS (e.g. 4447)
|
||||
# HTTP_PORT -> plain HTTP (e.g. 8087)
|
||||
#
|
||||
# With this setup you only need to expose port 443 (or 4443) through nginx.
|
||||
# Copy this file to your nginx site config and adjust paths/ports as needed.
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name pos.example.com;
|
||||
|
||||
# Same certificate pair the Node server uses (certs/key.pem + certs/cert.pem).
|
||||
# For a real deployment, replace with your own cert/chain.
|
||||
ssl_certificate /home/strainz/src/jtlPosSync/certs/cert.pem;
|
||||
ssl_certificate_key /home/strainz/src/jtlPosSync/certs/key.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
# The JTL POS API can send large request bodies (product/category sync).
|
||||
client_max_body_size 0;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8087;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# Keep alive to the Node backend.
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Longer timeouts for long-running sync requests.
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
}
|
||||
}
|
||||
|
||||
# Optional: redirect all plain-HTTP traffic to HTTPS.
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name pos.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -15,6 +16,7 @@ import { fetchActiveShop } from './src/shop.js';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const PORT = Number(process.env.PORT) || 4443;
|
||||
const HTTP_PORT = Number(process.env.HTTP_PORT) || 8080;
|
||||
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241';
|
||||
const PAIRING_CODE = process.env.PAIRING_CODE || '307018';
|
||||
|
||||
@@ -112,6 +114,8 @@ const httpsServer = https.createServer(
|
||||
loggedJtlHandler
|
||||
);
|
||||
|
||||
const httpServer = http.createServer(loggedJtlHandler);
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
const pool = await connectDb();
|
||||
@@ -129,11 +133,16 @@ async function start() {
|
||||
logger.info(`Pairing code: ${PAIRING_CODE}`);
|
||||
logger.info(`Auth token: ${AUTH_TOKEN}`);
|
||||
});
|
||||
|
||||
httpServer.listen(HTTP_PORT, '0.0.0.0', () => {
|
||||
logger.success(`HTTP POS server listening on http://0.0.0.0:${HTTP_PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function shutdown() {
|
||||
logger.info('Shutting down...');
|
||||
httpsServer.close();
|
||||
httpServer.close();
|
||||
await closeDb();
|
||||
closeRequestLog();
|
||||
closeOrderLog();
|
||||
|
||||
@@ -4,9 +4,10 @@ import * as client from './client.js';
|
||||
import * as customergroup from './customergroup.js';
|
||||
import * as deletedEntity from './deleted-entity.js';
|
||||
import * as init from './init.js';
|
||||
import * as newpin from './newpin.js';
|
||||
import * as order from './order.js';
|
||||
import * as pimage from './pimage.js';
|
||||
import * as product from './product.js';
|
||||
import * as productcomposite from './productcomposite.js';
|
||||
|
||||
export const endpoints = [client, init, category, product, productcomposite, deletedEntity, pimage, cimage, customergroup, order];
|
||||
export const endpoints = [client, newpin, init, category, product, productcomposite, deletedEntity, pimage, cimage, customergroup, order];
|
||||
|
||||
27
src/endpoints/newpin.js
Normal file
27
src/endpoints/newpin.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { sendJson, serverTimestamp } from '../http.js';
|
||||
|
||||
export const method = 'GET';
|
||||
export const path = '/v1/newpin';
|
||||
|
||||
// 6-digit code in [100000, 999999] so it always has exactly 6 digits (no leading zeros)
|
||||
function generatePin() {
|
||||
return String(crypto.randomInt(100000, 1000000));
|
||||
}
|
||||
|
||||
export function handle(_req, res, { pairingStore, config }) {
|
||||
if (!config.newPinEnabled) {
|
||||
return sendJson(res, 403, {
|
||||
Message: 'This endpoint is disabled. Set NEWPIN_ENABLED=true to enable it.',
|
||||
});
|
||||
}
|
||||
|
||||
const pin = generatePin();
|
||||
pairingStore.setPairingCode(pin, 'JTL-POS');
|
||||
|
||||
return sendJson(res, 200, {
|
||||
pin,
|
||||
name: 'JTL-POS',
|
||||
serverTimestamp: serverTimestamp(),
|
||||
});
|
||||
}
|
||||
@@ -19,6 +19,7 @@ function buildConfig(config = {}) {
|
||||
mandantId: config.mandantId || process.env.MANDANT_ID || '1',
|
||||
mandantName: config.mandantName || process.env.MANDANT_NAME || 'eB-Standard',
|
||||
mandantDatabase: config.mandantDatabase || process.env.MANDANT_DATABASE || 'eazybusiness',
|
||||
newPinEnabled: config.newPinEnabled ?? process.env.NEWPIN_ENABLED === 'true',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sql from 'mssql';
|
||||
import { xmlTag, xmlElement } from './xml.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
/**
|
||||
* Commits this session's reservations into "real" Picklisten (bumps
|
||||
@@ -16,6 +17,8 @@ import { xmlTag, xmlElement } from './xml.js';
|
||||
export async function commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag) {
|
||||
const bestellungen = xmlElement('Bestellung', [xmlTag('kBestellung', kAuftrag)]);
|
||||
|
||||
logger.info(`commit: kSessionId=${kSessionId} kAuftrag=${kAuftrag} nTeillieferung=false bestellungen=${bestellungen}`);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('Bestellungen', sql.NVarChar(sql.MAX), bestellungen)
|
||||
.input('kBenutzer', sql.Int, kBenutzer)
|
||||
@@ -31,4 +34,6 @@ export async function commitPicklists(transaction, kBenutzer, kSessionId, kAuftr
|
||||
@kSessionId = @kSessionId,
|
||||
@xResult = @xResult OUTPUT;
|
||||
`);
|
||||
|
||||
logger.info(`commit: done kSessionId=${kSessionId} kAuftrag=${kAuftrag}`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sql from 'mssql';
|
||||
import { xmlTag, xmlElement } from './xml.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
// PicklistenAusliefernOptionen.VersandSetzen (0x0002). DeliveryStep.Run always sets
|
||||
// AuslieferungAusliefernContext.Optionen.VersandSetzen = true for POS/API orders
|
||||
@@ -34,6 +35,8 @@ export async function deliverPicklists(transaction, kBenutzer, kSessionId, kAuft
|
||||
xmlTag('fGewicht', 0), // NOT NULL column; real weight is recomputed from the order's articles further below in the proc
|
||||
]);
|
||||
|
||||
logger.info(`deliver: kSessionId=${kSessionId} kAuftrag=${kAuftrag} kVersandArt=${kVersandArt} pakete=${pakete}`);
|
||||
|
||||
const result = await new sql.Request(transaction)
|
||||
.input('Pakete', sql.NVarChar(sql.MAX), pakete)
|
||||
.input('nOptions', sql.Int, AUSLIEFERN_OPTIONS)
|
||||
@@ -52,5 +55,8 @@ export async function deliverPicklists(transaction, kBenutzer, kSessionId, kAuft
|
||||
@xResult = @xResult OUTPUT;
|
||||
SELECT @xResult AS xResult;
|
||||
`);
|
||||
return result.recordset[0]?.xResult ?? null;
|
||||
|
||||
const xResult = result.recordset[0]?.xResult ?? null;
|
||||
logger.info(`deliver: done kSessionId=${kSessionId} kAuftrag=${kAuftrag} xResult=${String(xResult).slice(0, 2000)}`);
|
||||
return xResult;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { reservePositions } from './reserve.js';
|
||||
import { bookStockShortfallsAndRereserve } from './stock-shortage.js';
|
||||
import { commitPicklists } from './commit.js';
|
||||
import { deliverPicklists } from './deliver.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
/**
|
||||
* Delivers the full ordered quantity of every position of an order, exactly
|
||||
@@ -23,8 +24,11 @@ export async function deliverOrder(transaction, kBenutzer, kAuftrag, kVersandArt
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`deliverOrder: kAuftrag=${kAuftrag} kBenutzer=${kBenutzer} kVersandArt=${kVersandArt} deliveredItems=${JSON.stringify(deliveredItems)}`);
|
||||
|
||||
const kWarenLager = await getOutgoingWarehouse(transaction);
|
||||
const kWarenLagerPlatz = await getWarehousePlace(transaction, kWarenLager);
|
||||
logger.info(`deliverOrder: kWarenLager=${kWarenLager} kWarenLagerPlatz=${kWarenLagerPlatz}`);
|
||||
const kSessionId = await openSession(transaction, kBenutzer);
|
||||
|
||||
try {
|
||||
@@ -39,6 +43,10 @@ export async function deliverOrder(transaction, kBenutzer, kAuftrag, kVersandArt
|
||||
);
|
||||
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 {
|
||||
// Safe to run unconditionally (success or error): spPicklistenVerwerfen only
|
||||
// ever removes not-yet-delivered (nStatus < 10) Picklisten for this session.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sql from 'mssql';
|
||||
import { xmlTag, xmlElement } from './xml.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
// ReserviereBestellpositionenOptionen: ChargenVorbelegen (0x0002) | Stücklistenkorrektur (0x0100).
|
||||
// Matches jtlCore's AuslieferungContext ctor + the StoredProcedures.cs wrapper for
|
||||
@@ -34,6 +35,10 @@ export async function reservePositions(transaction, kBenutzer, kSessionId, kWare
|
||||
xmlTag('kAnsprechpartner', 0),
|
||||
]);
|
||||
|
||||
logger.info(`reserve: kSessionId=${kSessionId} kWarenLager=${kWarenLager} nOptions=0x${RESERVIERE_OPTIONS.toString(16)} positions=${JSON.stringify(positions)}`);
|
||||
logger.info(`reserve: XML @Bestellpositionen=${bestellpositionen}`);
|
||||
logger.info(`reserve: XML @Laeger=${laeger}`);
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('Bestellpositionen', sql.NVarChar(sql.MAX), bestellpositionen)
|
||||
.input('Laeger', sql.NVarChar(sql.MAX), laeger)
|
||||
@@ -51,4 +56,6 @@ export async function reservePositions(transaction, kBenutzer, kSessionId, kWare
|
||||
@kBenutzer = @kBenutzer,
|
||||
@kSessionId = @kSessionId;
|
||||
`);
|
||||
|
||||
logger.info(`reserve: done kSessionId=${kSessionId} positions=${JSON.stringify(positions)}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import sql from 'mssql';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
/**
|
||||
* Opens a JTL "Auslieferung" session by inserting a row into dbo.tSessionId,
|
||||
@@ -17,7 +18,9 @@ export async function openSession(transaction, kBenutzer, hostname = 'jtlsrv') {
|
||||
VALUES (@cRechnername, @kBenutzer, DATEADD(day, 10, GETDATE()));
|
||||
SELECT kSessionId FROM @t;
|
||||
`);
|
||||
return result.recordset[0].kSessionId;
|
||||
const kSessionId = result.recordset[0].kSessionId;
|
||||
logger.info(`session: opened kSessionId=${kSessionId} kBenutzer=${kBenutzer}`);
|
||||
return kSessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,6 +31,7 @@ export async function openSession(transaction, kBenutzer, hostname = 'jtlsrv') {
|
||||
* a successful delivery and on the error path.
|
||||
*/
|
||||
export async function discardSession(transaction, kBenutzer, kSessionId) {
|
||||
logger.info(`session: discard kSessionId=${kSessionId} kBenutzer=${kBenutzer}`);
|
||||
await new sql.Request(transaction)
|
||||
.input('kBenutzer', sql.Int, kBenutzer)
|
||||
.input('kSessionId', sql.Int, kSessionId)
|
||||
@@ -40,6 +44,7 @@ export async function discardSession(transaction, kBenutzer, kSessionId) {
|
||||
* Wawi GUI's "who's editing what" bookkeeping), but keeps the table tidy.
|
||||
*/
|
||||
export async function closeSession(transaction, kSessionId) {
|
||||
logger.info(`session: close kSessionId=${kSessionId}`);
|
||||
await new sql.Request(transaction)
|
||||
.input('kSessionId', sql.Int, kSessionId)
|
||||
.query('DELETE FROM dbo.tSessionId WHERE kSessionId = @kSessionId');
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import sql from 'mssql';
|
||||
import { reservePositions } from './reserve.js';
|
||||
import { logger } from '../../logger.js';
|
||||
|
||||
const POS_SHORTAGE_COMMENT = 'Korrekturbuchung erstellt durch POS-Abgleich';
|
||||
const BUCHUNGSART_WARENEINGANG = 10;
|
||||
@@ -15,18 +16,23 @@ async function getReservedQuantity(transaction, kSessionId, kAuftragPosition) {
|
||||
WHERE p.kSessionId = @kSessionId
|
||||
AND pp.kBestellPos = @kBestellPos
|
||||
`);
|
||||
return Number(result.recordset[0]?.reserved ?? 0);
|
||||
const reserved = Number(result.recordset[0]?.reserved ?? 0);
|
||||
logger.info(`stockShortage: getReservedQuantity kSessionId=${kSessionId} kBestellPos=${kAuftragPosition} reserved=${reserved}`);
|
||||
return reserved;
|
||||
}
|
||||
|
||||
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
|
||||
SELECT ap.kArtikel, ap.nType, ap.nReserviert, a.cLagerAktiv, a.cLagerArtikel, a.cArtNr
|
||||
FROM Verkauf.tAuftragPosition ap
|
||||
LEFT JOIN dbo.tArtikel a ON a.kArtikel = ap.kArtikel
|
||||
WHERE ap.kAuftragPosition = @kAuftragPosition
|
||||
`);
|
||||
return result.recordset[0]?.kArtikel ?? 0;
|
||||
const row = result.recordset[0];
|
||||
logger.info(`stockShortage: getPositionArtikel kBestellPos=${kAuftragPosition} row=${JSON.stringify(row)}`);
|
||||
return row?.kArtikel ?? 0;
|
||||
}
|
||||
|
||||
async function bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtikel, fehlmenge) {
|
||||
@@ -74,30 +80,41 @@ export async function bookStockShortfallsAndRereserve(
|
||||
kWarenLagerPlatz,
|
||||
positions,
|
||||
) {
|
||||
logger.info(`stockShortage: bookStockShortfallsAndRereserve kBenutzer=${kBenutzer} kSessionId=${kSessionId} kWarenLager=${kWarenLager} kWarenLagerPlatz=${kWarenLagerPlatz} positions=${JSON.stringify(positions)}`);
|
||||
|
||||
const rereserve = [];
|
||||
|
||||
for (const { kAuftragPosition, quantity } of positions) {
|
||||
if (!kAuftragPosition || quantity <= 0) {
|
||||
logger.info(`stockShortage: skip position kBestellPos=${kAuftragPosition} (no kAuftragPosition or qty<=0)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const reserved = await getReservedQuantity(transaction, kSessionId, kAuftragPosition);
|
||||
const shortage = quantity - reserved;
|
||||
logger.info(`stockShortage: kBestellPos=${kAuftragPosition} quantity=${quantity} reserved=${reserved} shortage=${shortage}`);
|
||||
if (shortage <= 0.0001) {
|
||||
logger.info(`stockShortage: kBestellPos=${kAuftragPosition} no shortage, skip`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const kArtikel = await getPositionArtikel(transaction, kAuftragPosition);
|
||||
logger.info(`stockShortage: kBestellPos=${kAuftragPosition} kArtikel=${kArtikel}`);
|
||||
if (!kArtikel) {
|
||||
logger.info(`stockShortage: kBestellPos=${kAuftragPosition} no kArtikel (free position / Pfand?), skip shortage booking`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await bookWareneingang(transaction, kBenutzer, kWarenLagerPlatz, kArtikel, shortage);
|
||||
logger.info(`stockShortage: booked Wareneingang kArtikel=${kArtikel} kWarenLagerPlatz=${kWarenLagerPlatz} fehlmenge=${shortage}`);
|
||||
rereserve.push({ kAuftragPosition, quantity: shortage });
|
||||
}
|
||||
|
||||
if (rereserve.length) {
|
||||
logger.info(`stockShortage: re-reserving ${JSON.stringify(rereserve)}`);
|
||||
await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, rereserve);
|
||||
} else {
|
||||
logger.info('stockShortage: no re-reserve needed');
|
||||
}
|
||||
|
||||
for (const { kAuftragPosition, quantity } of positions) {
|
||||
@@ -105,8 +122,12 @@ export async function bookStockShortfallsAndRereserve(
|
||||
continue;
|
||||
}
|
||||
const reserved = await getReservedQuantity(transaction, kSessionId, kAuftragPosition);
|
||||
logger.info(`stockShortage: final check kBestellPos=${kAuftragPosition} need=${quantity} reserved=${reserved}`);
|
||||
if (reserved + 0.0001 < quantity) {
|
||||
logger.error(`stockShortage: FAIL kBestellPos=${kAuftragPosition} need=${quantity} reserved=${reserved}`);
|
||||
throw new Error(`insufficient stock after POS shortage booking for kBestellPos=${kAuftragPosition}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('stockShortage: all positions fully reserved after shortage booking');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user