Compare commits
2 Commits
6312eaec48
...
6abe2032e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6abe2032e8 | ||
|
|
5951e10ea5 |
@@ -3,8 +3,15 @@ DEMO_MODE=false
|
||||
|
||||
# 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
@@ -92,6 +92,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';
|
||||
@@ -18,6 +19,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';
|
||||
|
||||
@@ -122,6 +124,8 @@ const httpsServer = https.createServer(
|
||||
loggedJtlHandler
|
||||
);
|
||||
|
||||
const httpServer = http.createServer(loggedJtlHandler);
|
||||
|
||||
async function start() {
|
||||
if (isDemoMode()) {
|
||||
const stats = await loadDemoCatalog();
|
||||
@@ -147,11 +151,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(),
|
||||
});
|
||||
}
|
||||
@@ -10,6 +10,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',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { isDemoMode } from '../demo/mode.js';
|
||||
import { createDemoOrder } from '../demo/store.js';
|
||||
import { getPool } from '../db.js';
|
||||
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { deliverOrder } from './delivery/index.js';
|
||||
|
||||
const config = {
|
||||
@@ -451,8 +452,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);
|
||||
@@ -748,6 +759,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 || []) {
|
||||
@@ -755,6 +767,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