delivery seemds to work.

This commit is contained in:
seb
2026-07-13 03:40:14 +02:00
parent 03dde8eff8
commit 955fe2c164
12 changed files with 985 additions and 573 deletions

View File

@@ -1,52 +0,0 @@
import sql from 'mssql';
import 'dotenv/config';
const config = {
server: process.env.MSSQL_SERVER || 'localhost',
port: Number(process.env.MSSQL_PORT) || 1433,
database: process.env.MSSQL_DATABASE || 'Mandant_3',
user: process.env.MSSQL_USER,
password: process.env.MSSQL_PASSWORD,
options: {
encrypt: process.env.MSSQL_ENCRYPT !== 'false',
trustServerCertificate: process.env.MSSQL_TRUST_SERVER_CERTIFICATE !== 'false',
},
};
async function main() {
try {
const pool = await sql.connect(config);
const result = await pool.request().query(`
SELECT
a.kArtikel as id, a.cArtNr as sku,
CONVERT(BIGINT, a.bRowversion) as articleRV,
MAX(CONVERT(BIGINT, abp.bRowversion)) as maxImageRV,
CASE
WHEN MAX(CONVERT(BIGINT, abp.bRowversion)) IS NOT NULL
AND MAX(CONVERT(BIGINT, abp.bRowversion)) > CONVERT(BIGINT, a.bRowversion)
THEN MAX(CONVERT(BIGINT, abp.bRowversion))
ELSE CONVERT(BIGINT, a.bRowversion)
END as effectiveLastChanged
FROM dbo.tArtikel a
INNER JOIN dbo.tArtikelBeschreibung ab ON ab.kArtikel = a.kArtikel AND ab.kSprache = 1
LEFT JOIN dbo.tArtikelbildPlattform abp ON abp.kArtikel = a.kArtikel AND abp.kShop = 1
WHERE a.cAktiv = 'Y'
GROUP BY a.kArtikel, a.cArtNr, a.bRowversion
ORDER BY a.kArtikel
`);
console.log('Product effective lastChanged (with image RV for shop 1):');
for (const row of result.recordset) {
const diff = row.maxImageRV && row.maxImageRV > row.articleRV ? ' (IMAGE RV HIGHER!)' : '';
console.log(' ID: ' + row.id + ', SKU: ' + row.sku + ', articleRV: ' + row.articleRV + ', maxImageRV: ' + (row.maxImageRV || 'none') + ', effective: ' + row.effectiveLastChanged + diff);
}
await pool.close();
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();

666
delivery.md Normal file
View File

@@ -0,0 +1,666 @@
# Delivery investigation
## Problem
Orders created via `jtlsrv` (`AU-202607-10025`) get `nKomplettAusgeliefert = 1` and a
`dbo.tLieferschein` / `dbo.tLieferscheinPos` row, but **no** `dbo.tPickliste` /
`dbo.tPicklistePos` rows and no real warehouse stock booking. Orders created by the
real JTL-Wawi/POS stack (`AU-202607-10026`) get all of that correctly (Pickliste with
proper status progression 5 → 10 → 40, a numbered Lieferschein like
`AU-202607-10026-001`, etc).
Root cause: our code (`src/queries/create-order.js`) calls the low-level
`Versand.spLieferscheinErstellen` / `Versand.spLieferscheinPosErstellen` procedures
directly. These only insert the Lieferschein rows and call
`Verkauf.spAuftragEckdatenBerechnen` (which derives `nKomplettAusgeliefert`). They
never touch the Pickliste/warehouse-reservation subsystem, so nothing is actually
picked/booked out of stock. The real JTL code path is much deeper and goes through
the same "Auslieferung" (fulfillment) engine used by the Wawi GUI's "Auftrag
ausliefern" feature. This document records that engine in full, down to exact SQL
parameter names/types and XML schemas, gathered from:
- Decompiled C# (`ilspycmd`) of `jtlCore.dll` (obfuscated string literals, but
control flow / call graph is readable) and `jtlDatabase.dll` (**not** obfuscated —
this is where the actual ADO.NET parameter wiring lives).
- `sp_helptext` of the actual stored procedures in `eazybusiness` (SQL Server ground
truth — these fully document their own XML input schemas in comments and in the
`.nodes()`/`.value()` shredding code).
## 0. Step-by-step SQL to issue for one delivery (ready to run/adapt)
This is the literal sequence of statements to execute, in order, inside the same
transaction as the rest of order creation, to deliver one order
(`@kAuftrag`) for one user (`@kBenutzer`). Everything here is derived 1:1 from the
stored-procedure signatures and XML schemas documented in §3; treat this section as
the concrete checklist, §3 as the "why"/evidence.
Placeholders to fill in per order: `@kAuftrag`, `@kBenutzer`, `@kWarenLager` (see
§7 — outgoing warehouse, still unresolved), and one `<Bestellposition>`/`<Lager>`
pair per order position (only positions with real, stock-tracked articles need to go
through the reservation dance below at all — see §7 for free positions).
```sql
-- Step 1: open a session (mirrors jtlCore's SessionManager / dbo.tSessionId row)
DECLARE @kSessionId INT;
INSERT INTO dbo.tSessionId (cRechnername, kBenutzer, dLastAction)
VALUES ('jtlsrv', @kBenutzer, DATEADD(day, 10, GETDATE()));
SET @kSessionId = SCOPE_IDENTITY();
-- Step 2: reserve every order position against the outgoing warehouse
-- (one call per position, or batch them all into @Bestellpositionen and call
-- Auslieferung.spReserviereBestellpositionen once for the whole order — same effect)
DECLARE @Bestellpositionen XML = '
<Bestellposition><kBestellPos>{kAuftragPosition}</kBestellPos><fAnzahl>{menge}</fAnzahl></Bestellposition>
<!-- repeat one <Bestellposition> per order position -->
';
DECLARE @Laeger XML = '
<Lager><kWarenlager>{kWarenLager}</kWarenlager><nPrio>0</nPrio><kLieferant>0</kLieferant><kAnsprechpartner>0</kAnsprechpartner></Lager>
';
EXEC Auslieferung.spReserviereBestellpositionen
@Bestellpositionen = @Bestellpositionen,
@Laeger = @Laeger,
@Warenlagereingaenge = NULL,
@nOptions = 0x0102, -- ChargenVorbelegen (0x0002) | Stücklistenkorrektur (0x0100)
@kBenutzer = @kBenutzer,
@kSessionId = @kSessionId;
-- -> creates dbo.tPickliste (kSessionId = @kSessionId) + dbo.tPicklistePos rows, status 0 then 5
-- Step 3: commit the session's reservations into "real" Picklisten
DECLARE @Bestellungen XML = '<Bestellung><kBestellung>{kAuftrag}</kBestellung></Bestellung>';
DECLARE @xResultUebernehmen XML;
EXEC Auslieferung.spPicklistenUebernehmen
@Bestellungen = @Bestellungen,
@kBenutzer = @kBenutzer,
@nTeillieferung = 0, -- 0 = partial delivery forbidden (we always deliver in full)
@kSessionId = @kSessionId,
@xResult = @xResultUebernehmen OUTPUT;
-- -> bumps dbo.tPicklistePos.nStatus from <10 to 10 (or 20 for position types 18/20) for this session
-- Step 4: actually deliver — creates the numbered Lieferschein, decrements real
-- stock, marks positions delivered
--
-- IMPORTANT: @Pakete must NOT be NULL/empty if you want dbo.tVersand populated.
-- The nested Auslieferung.spPicklistenAusliefern_PaketeErzeugen builds a temp
-- table straight from @Pakete.nodes('/Paket'); if @Pakete is NULL that temp
-- table stays empty and NOT A SINGLE dbo.tVersand row gets written for ANY
-- Lieferschein in the session (not even the "no Paket given" fallback row,
-- because even that fallback branch is keyed off a per-kBestellung JOIN
-- against the temp table). So pass one <Paket> per order, at minimum with
-- <kBestellung>/<kVersandart>/<fGewicht> (fGewicht=0 is the sentinel the proc
-- itself later replaces with an article-weight-based value; the column is
-- NOT NULL so it can't be omitted).
DECLARE @Pakete XML = '<Paket><kBestellung>{kAuftrag}</kBestellung><kVersandart>{kVersandArt}</kVersandart><fGewicht>0</fGewicht></Paket>';
DECLARE @xResultAusliefern XML;
EXEC Auslieferung.spPicklistenAusliefern
@xHinweise = NULL, -- optional <Hinweis><kBestellPos>/<kBestellung></kBestellung><cHinweis>...
@Pakete = @Pakete,
@nOptions = 0x0002, -- VersandSetzen (DeliveryStep always sets this); OR 0x0001 for one Lieferschein per warehouse
@kBenutzer = @kBenutzer,
@kSessionId = @kSessionId,
@xResult = @xResultAusliefern OUTPUT;
-- -> creates dbo.tLieferschein/tLieferscheinPos (properly numbered "<AuftragsNr>-NNN"),
-- links kLieferscheinPos back onto tPicklistePos, calls dbo.spWarenlagerAusgangPicklistePos
-- (real stock decrement), bumps tPicklistePos.nStatus to 40, AND (via
-- spPicklistenAusliefern_PaketeErzeugen) inserts a dbo.tVersand row per
-- Lieferschein carrying {kVersandArt} and, since VersandSetzen is set and this
-- is a local (non-fulfillment) warehouse, dVersendet = GETDATE()
-- Step 5: recalculate order eckdaten (nKomplettAusgeliefert etc.) — already implemented today
DECLARE @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen;
INSERT INTO @p1 VALUES (@kAuftrag);
EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @p1;
-- Step 6: discard anything left in this session that never got committed/delivered.
-- Safe to call unconditionally (success AND error path) — its body is just
-- `DELETE FROM dbo.tPickliste WHERE kSessionId = @kSessionId AND nStatus < 10`,
-- so it never touches already-delivered (nStatus > 10) Picklisten. On the error
-- path it also cleans up whatever Step 2 managed to reserve before the failure.
EXEC Auslieferung.spPicklistenVerwerfen @kBenutzer = @kBenutzer, @kSessionId = @kSessionId;
-- Step 7 (optional cleanup, mirrors SessionManager.Dispose()):
DELETE FROM dbo.tSessionId WHERE kSessionId = @kSessionId;
```
Notes on the XML construction above (see §3 for full schema/behavior detail):
- `{kAuftragPosition}` = `Verkauf.tAuftragPosition.kAuftragPosition` (what our code
calls `kAuftragPosition`), `{menge}` = ordered quantity, `{kWarenLager}` = the
resolved outgoing warehouse (§7, open question), `{kAuftrag}` =
`Verkauf.tAuftrag.kAuftrag`.
- `@Bestellpositionen`/`@Laeger` are XML *fragments* — SQL Server's `xml` type
happily holds multiple top-level elements, so just concatenate one
`<Bestellposition>...</Bestellposition>` per position (same for `<Lager>` if ever
passing multiple candidate warehouses).
- Steps 2-4 must run inside the *same* transaction/connection context as order
creation so `@kSessionId` and the temp Pickliste rows are visible to each other and
everything rolls back together on failure.
## 0.1 Cross-check against another (incomplete) implementation
A second, independently-written `deliverOrder()` implementation was reviewed. It was
apparently built by SQL-tracing manual "Auftrag ausliefern" actions in the Wawi GUI
(comments like `// Based on Statement 30 from JTL dump`). Cross-checking it against
the decompiled C#/`sp_helptext` ground truth above:
**Confirms our findings:**
- Same overall call order: `Auslieferung.spReserviereBestellungen`/`spReserviereBestellpositionen`
`Auslieferung.spPicklistenUebernehmen``Auslieferung.spPicklistenAusliefern`
`Verkauf.spAuftragEckdatenBerechnen`.
- Session handling via a `dbo.tSessionId` row (`kSessionId`), matching §2 exactly.
- `@nOptions = 258` (`0x102`) for `Auslieferung.spReserviereBestellpositionen` — exact
match with our decompiled-C#-derived value in §3.1.
- `@nOptions = 2` (no `Stücklistenkorrektur` bit) for `Auslieferung.spReserviereBestellungen`
— matches the decompiled C# wrapper for that specific overload, which (unlike the
`...Bestellpositionen` wrapper) does **not** OR in `Stücklistenkorrektur`.
- Real example `@Laeger` XML with multiple prioritised warehouses (`kWarenlager` 1,
4, 5, 6, 7) plus dropshipping-supplier fallback rows (`kLieferant` 3/1/2/`n`,
i.e. default/cheapest/fastest/specific supplier) — confirms the magic-value scheme
documented in §3.1 with a concrete real-world example, and confirms
`@Laeger` supports arbitrarily many `<Lager>` candidates, not just one.
- After delivery, Picklisten/`tPicklistePos` rows are deliberately **kept** (not
deleted) — their `cleanupSessions()` only ever deletes `dbo.tSessionId` (the
`picklistId` branch that would delete `tPickliste`/`tPicklistePos` is called with
`null` and never actually runs). Matches expectations: the Pickliste is the audit
trail the Wawi GUI still shows after delivery, so it must survive.
- Calls `Auslieferung.spPicklistenVerwerfen` again at the very end, even after a
successful `spPicklistenAusliefern`. Checked its SQL body
(`Auslieferung.spPicklistenVerwerfen`, `/tmp/sp_verwerfen.sql`):
```sql
DELETE FROM dbo.tPickliste WHERE kSessionId = @kSessionId AND nStatus < 10
```
It only ever removes *uncommitted* (`nStatus < 10`) Picklisten for the session, so
calling it unconditionally at the end of a successful delivery is harmless/a good
defensive habit (cleans up any leftover reservation that didn't make it into the
delivery, e.g. a position that failed stock checks), not just an error-path call
as §6 step 6 assumed. **Recommendation: call it unconditionally after step 4/5,
not only on error.**
**Contradicts/corrects our findings — resolved in favor of the ground truth:**
- It resolves order positions through legacy `dbo.tBestellung`/`dbo.tBestellPos`
tables, joined back to `Verkauf.tAuftragPosition` by **matching `kArtikel`**
(`ap.kArtikel = bp.tArtikel_kArtikel AND ap.kAuftrag = a.kAuftrag`) rather than by
ID. This is unnecessary and fragile (breaks for two order lines with the same
SKU/article). Checked directly: `Auslieferung.vBestellPos`
(`/tmp/vbestellpos.sql`) —
```sql
CREATE VIEW [Auslieferung].[vBestellPos] AS
SELECT tAuftragPosition.kAuftragPosition AS kBestellPos,
tAuftrag.kAuftrag AS kBestellung, ...
```
confirms **`kBestellPos` *is* `Verkauf.tAuftragPosition.kAuftragPosition` and
`kBestellung` *is* `Verkauf.tAuftrag.kAuftrag`, directly** — "Bestellung"/"BestellPos"
is just the historical/legacy parameter naming carried over from pre-3.0 JTL, not a
separate table to join through. Our `create-order.js` already has these ids
directly (`kAuftrag`, `kAuftragPosition`) — no extra lookup needed, use them as-is
in all the `@Bestellpositionen`/`@Bestellungen`/`@Laeger` XML.
- It calls `Auslieferung.spPicklistenAusliefern` with **`@nOptions = 0`** (no
`VersandSetzen`). This contradicts `DeliveryStep.cs` (POS/API order path), which
explicitly does:
```csharp
auslieferungAusliefernContext.Optionen.VersandSetzen = true;
```
(`decompiledReference/decompiled/jtlCore/jtlCore.Classes.Sync.PosOrderSteps/DeliveryStep.cs:213`,
confirmed present, not just inferred). The other implementation was likely traced
from a manual GUI delivery where the "Versand setzen" checkbox happened to be
unchecked. **For reproducing POS/API-style immediate delivery, use `@nOptions = 2`
(`VersandSetzen`), not `0`, per DeliveryStep's actual behavior — keep §0/§3.3 as
documented.**
- It also creates a `dbo.tUserSession` row (`createJTLSessions`) alongside
`dbo.tSessionId`, mirroring a full GUI login. No stored procedure body read so far
(§3, plus `spWarenlagerAusgangPicklistePos`, `spPicklistenVerwerfen`,
`vBestellPos`) references or validates against `tUserSession` — the only
server-side check seen is `kBenutzer` existing in `dbo.tbenutzer` (a plain FK-style
check, e.g. inside `spWarenlagerAusgangPicklistePos`). **Recommendation: skip
`tUserSession` entirely** unless a not-yet-encountered procedure turns out to
require it.
- Its partial-delivery path (multiple `spReserviereBestellungen` calls interleaved
with manual `DELETE FROM tPicklistePos`/quantity-adjustment dances) is real
evidence of how the Wawi GUI implements *partial* delivery, but is unnecessary
complexity for us — `create-order.js` always delivers the full ordered quantity
(mirrors `DeliveryStep`'s non-voucher path), so the single-pass §0 sequence
(reserve once, commit with `nTeillieferung = 0`, deliver) is sufficient and matches
what `DeliveryStep.Run` itself does for a new POS order.
## 1. C# call chain: HTTP request → delivery commit
1. **`OrderController.Put`**
(`decompiledReference/JTL.Wawi.PosServer.Controller/OrderController.cs:33-67`)
Receives `PUT /api/v1/order`, delegates to `Q5hG5CfaiXW` (line 101-127), which
builds a `PosFrontend<...>` and calls `.ExecuteAsync(new PosOrderCommand {
Origin = OriginType.Bestellung, ... })`.
2. **`PosFrontend.ExecuteAsync` → `PosFrontend.HandleOrders`**
(`decompiledReference/JTL.Wawi.Sync.Core.decompiled.cs:6168-6122`)
Calls `new PosOrderImportService(...).ImportOrders(...)`.
3. **`PosOrderImportService.ImportOrders`**
(`decompiledReference/JTL.Wawi.Sync.Core.decompiled.cs:6231-6281`)
For a brand-new order (no existing `OrderNumber`/`OrderId` match — our case):
`CreateOrUpdatePosAuftrag(order, list, shopSubShopId)` →
`SyncCoreService.Service.CreateOrUpdatePosAuftrag(...)`.
4. **`SyncCoreService.CreateOrUpdatePosAuftrag`**
(`decompiledReference/decompiled/jtlCore/jtlCore.Classes.Sync/SyncCoreService.cs:77-80`)
Delegates to `new PosOrderCreationService(...).CreateOrUpdatePosAuftrag(...)`.
5. **`PosOrderCreationService.CreateOrUpdatePosAuftrag`**
(`decompiledReference/decompiled/jtlCore/jtlCore.Classes.Sync.Pos/PosOrderCreationService.cs:102-221`)
Creates the order via `importer.TrySaveOrder(...)`, then runs a step pipeline:
```
new RoundingErrorStep(...).Run(jtlAuftrag, orderEntity);
new DeliveryStep(...).Run(jtlAuftrag, orderEntity);
new InvoicePrintStep(...).Run(jtlAuftrag, orderEntity);
new VoucherPrintStep(...).Run(jtlAuftrag, orderEntity);
```
6. **`DeliveryStep.Run`**
(`decompiledReference/decompiled/jtlCore/jtlCore.Classes.Sync.PosOrderSteps/DeliveryStep.cs:184-273`)
`bool flag = orderEntity.Settings?.Deliver ?? true;` — this is where `deliver`
from the POS payload is consumed. When true:
- `PosWarehouseService.FindeWarenlagerFürAusgang()` — resolves the outgoing
warehouse (`kWarenLager`) to use. **Still unresolved in this doc — see "Open
questions" below.**
- `AuslieferungAusliefernContext.CreateForAufträge([kAuftrag], ...)` — opens a
delivery context (constructs an `AuslieferungContext`, which immediately opens
a `SessionManager`/`kSessionId`, see §2).
- `auslieferungContext.LadeBestände()` — loads current stock (read-only, for UI).
- For every position with a real `ArtikelId`:
`PosBookPositionService.BookPosition(...)` →
`auslieferungContext.NeuReservieren(positions, warehouseDetails)` → calls
`Auslieferung.spReserviereBestellpositionen` (§3.1).
- For positions without an `ArtikelId` (shipping, deposit/Pfand lines):
`auslieferungContext.NeuReservieren(...)` directly with the full quantity (same
stored procedure).
- If the order is paid (or the payment method allows delivery-before-payment):
`posStockPositionService.FehlbestandEinbuchen(...)` — books any stock shortfall
so delivery isn't blocked by missing stock.
- **`auslieferungAusliefernContext.Commit();`** — persists everything (§2).
- If backdated: `ApTVuv7PTPl.SetVersanddatum(auftrag.kAuftrag, orderEntity.CreationDate)`.
7. **`AuslieferungAusliefernContext.Commit()`**
(`decompiledReference/decompiled/jtlCore/jtlCore.Classes.Versand.Auslieferung/AuslieferungAusliefernContext.cs:223-321`)
```csharp
Validiere();
PicklistenCommitten(); // -> Auslieferung.spPicklistenUebernehmen (§3.2)
...
auslieferungSpResult = IScXmooPhGf(); // -> Auslieferung.spPicklistenAusliefern (§3.3)
...
kHtXmjSeHbm(T5jXmkcZPKf()); // -> Verkauf.spAuftragEckdatenBerechnen (already replicated)
ExecuteWorkflows(auslieferungSpResult); // UI/event notifications only, no DB writes
```
## 2. The session concept (`kSessionId`)
Every reservation/commit call is scoped by a `kSessionId`. This comes from
`jtlCore.Classes.SessionManager` (`decompiledReference/decompiled/jtlCore/jtlCore.Classes/SessionManager.cs`),
which lazily does, on first use:
```csharp
var s = new jtlSessionid {
cRechnername = Environment.MachineName,
kBenutzer = BenutzerManager.Current.kAngemeldeterBenutzer,
dLastAction = DateTime.Now + TimeSpan.FromDays(10.0)
};
s.Save(); // INSERT INTO dbo.tSessionId (...)
s.Touch();
```
`jtlSessionidBase` (`jtlDatabase.dll`, decompiled) maps to table **`dbo.tSessionId`**:
| Column | Type | Notes |
|---------------|-----------------|-----------------------------------------|
| `kSessionId` | `int` identity | PK, returned via `SCOPE_IDENTITY()` |
| `cRechnername`| `nvarchar(255)` | machine name (cosmetic, can be any string) |
| `kBenutzer` | `int` | FK `dbo.tBenutzer` |
| `dLastAction` | `datetime` | kept alive by a 60s "touch" timer; only relevant for GUI housekeeping/timeouts |
So reproducing this is just: **insert one row into `dbo.tSessionId`, read back
`kSessionId`**, use it for the whole delivery operation, nothing else needed (no need
to periodically "touch" it for a short-lived one-shot API request).
On `Dispose()`, if the context was constructed for "new reservation" mode
(`BQ9XLnEpso5 == true`, i.e. our case, an `AuslieferungAusliefernContext`), and
`Commit()` was never called, it rolls back via `Auslieferung.spPicklistenVerwerfen`
instead. We don't need this if we always commit; keep it in mind for error handling
(wrap in try/catch and call `spPicklistenVerwerfen` on failure to avoid leaving orphan
Picklisten in that session).
## 3. The stored procedures, in call order
All four procedures below live in `jtlDatabase.classes.jtlDBClasses.StoredProcedures`
(decompiled with `ilspycmd -t jtlDatabase.classes.jtlDBClasses.StoredProcedures
jtlDatabase.dll`) and their exact ADO.NET parameter wiring is unobfuscated. Their SQL
bodies were read directly via `sp_helptext` against `eazybusiness`.
### 3.1 `Auslieferung.spReserviereBestellpositionen` — soft-reserve positions
C# wrapper (`StoredProcedures.cs:1413-1440`):
```csharp
private static int? Auslieferung_spReserviereBestellpositionen(
string Bestellpositionen, string Laeger, string Warenlagereingaenge,
int? nOptions, int? kBenutzer, int? kSessionId, IDbConnection connection = null)
```
SQL parameters (`@Bestellpositionen XML, @Laeger XML, @Warenlagereingaenge XML,
@nOptions INT, @kBenutzer INT, @kSessionId INT`):
```xml
<!-- @Bestellpositionen: one per order position to reserve -->
<Bestellposition>
<kBestellPos>1</kBestellPos> <!-- = Verkauf.tAuftragPosition.kAuftragPosition -->
<fAnzahl>1.234</fAnzahl> <!-- quantity to reserve -->
</Bestellposition>
<!-- @Laeger: candidate warehouses, tried in nPrio order -->
<Lager>
<kWarenlager>1</kWarenlager>
<nPrio>0</nPrio>
<kLieferant>0</kLieferant> <!-- 0 = normal; -1/-2/-3 = cheapest/fastest/default supplier (dropshipping) -->
<kAnsprechpartner>0</kAnsprechpartner>
</Lager>
<!-- @Warenlagereingaenge: optional, specific goods-receipt rows (FIFO/batch/serial) — leave empty/NULL for "any" -->
<Warenlagereingang>
<kWarenlagereingang>17</kWarenlagereingang>
<fAnzahl>2.0</fAnzahl>
<nPrio>0</nPrio>
</Warenlagereingang>
```
`nOptions` bitmask (`StoredProcedures.ReserviereBestellpositionenOptionen`):
`0x0001 PicklisteProBestellung`, `0x0002 ChargenVorbelegen`, `0x0100
Stücklistenkorrektur`. The C# `AuslieferungContext` ctor always sets
`ChargenVorbelegen` (`0x0002`), and the low-level SP wrapper always OR's in
`Stücklistenkorrektur` (`0x0100`) — so the effective default is **`0x0102` (258)**
unless "one Pickliste per order" is also wanted (`+0x0001`).
Behavior (from `Auslieferung.spReserviereBestellposition`, called once per position
in a cursor loop, `/tmp/sp_reserviere_single.sql`):
- Resolves candidate stock sources from `@Laeger`/`@Warenlagereingaenge` into a temp
table (`#WARENLAGEREINGANG`), ordered by `nPrio`, `dMHD`, `cCharge`,
`kWarenlagereingang`.
- For each source, in order, until `@fAnzahl` is exhausted:
- Finds or creates a `dbo.tPickliste` row (`kSessionId = @kSessionId`, `nStatus =
0`, `cPicklisteNr` from `dbo.spGetNextNummer('Pickliste', ...)`). Reused across
positions of the same session/warehouse/supplier (one Pickliste per
warehouse+supplier+session, unless `PicklisteProBestellung` requested).
- Inserts a `dbo.tPicklistePos` row: `(kPickliste, kWarenLager, kWarenLagerEingang,
fAnzahl, kBestellPos, kPicklistePosStatus=0, kArtikel, kWarenlagerPlatz,
kPicklistePos_Ursprung=0, kLieferscheinPos=0, kBestellung)`.
- After the loop: deletes zero-quantity `tPicklistePos` rows for this
`kBestellPos`/session, then inserts a `dbo.tPicklistePosStatus` row with
**`nStatus = 5`** ("angelegt"/created) for every newly created `tPicklistePos`
(`kPicklistePosStatus = 0`).
This is the step that's entirely missing from our code — **this is why no Pickliste
exists for jtlsrv orders.**
### 3.2 `Auslieferung.spPicklistenUebernehmen` — commit reservations into "real" Picklisten
C# wrapper (`StoredProcedures.cs:1358-1386`):
```csharp
private static int? Auslieferung_spPicklistenUebernehmen(
string Bestellungen, int? kBenutzer, bool? nTeillieferung, int? kSessionId,
ref string xResult, IDbConnection connection = null)
```
SQL parameters (`@Bestellungen XML, @kBenutzer INT, @nTeillieferung BIT, @kSessionId
INT, @xResult XML OUTPUT`):
```xml
<Bestellung>
<kBestellung>7</kBestellung> <!-- Verkauf.tAuftrag.kAuftrag, one element per order -->
</Bestellung>
```
`@nTeillieferung`: `0` = partial delivery forbidden (raises an error if any reserved
order has both open and already-picked quantity outstanding — not our concern for a
"deliver everything now" POS order, so pass `0`); the C# side derives it from
`AuslieferungTeillieferungOptionen.GetCodeForSql() >= 1`.
Behavior (`/tmp/sp_uebernehmen.sql`):
- If `@nTeillieferung = 0`, verifies no order in `@Bestellungen` would end up
partially delivered; raises `RAISERROR` with an XML error payload otherwise.
- Deletes now-superfluous "Stücklistenvater" pick positions and any now-empty
Picklisten in this session.
- Bumps every `dbo.tPicklistePos` row in this session from status `< 10` to
**`nStatus = 10`** ("übernommen"/committed) via a new `tPicklistePosStatus` row.
- For a subset of position types (`tbestellpos.nType IN (18, 20)`) bumps further to
**`nStatus = 20`**.
- Returns `@xResult`:
```xml
<Result><NewPicklisten><Pickliste><kPickliste>...</kPickliste></Pickliste>...</NewPicklisten></Result>
```
### 3.3 `Auslieferung.spPicklistenAusliefern` — the actual delivery
C# wrapper (`StoredProcedures.cs:1327-1356`) — **this is the procedure the user
correctly identified as missing:**
```csharp
private static int? Auslieferung_spPicklistenAusliefern(
string xHinweise, string Pakete, int? nOptions, int? kBenutzer, int? kSessionId,
ref string xResult, IDbConnection connection = null)
```
SQL parameters (`@xHinweise XML, @Pakete XML, @nOptions INT, @kBenutzer INT,
@kSessionId INT, @xResult XML OUTPUT`):
```xml
<!-- @xHinweise: optional per-position/per-order notes copied onto the Lieferschein(Pos); can be empty/NULL -->
<Hinweis>
<kBestellPos>5</kBestellPos> <!-- OR <kBestellung>7</kBestellung> for the order-level note -->
<cHinweis>blah</cHinweis>
</Hinweis>
<!-- @Pakete: optional shipping/tracking info; can be empty/NULL for "no shipment info yet" -->
<Paket>
<kBestellung>1</kBestellung>
<kVersandart>5</kVersandart>
<dVersanddatum>...</dVersanddatum>
<cTrackingId>trackingid</cTrackingId>
<cEnclosedReturnIdentCode>...</cEnclosedReturnIdentCode>
<cHinweis>...</cHinweis>
</Paket>
```
`@nOptions` bitmask (`StoredProcedures.PicklistenAusliefernOptionen`): `0x0001
LieferscheinProLager` (one Lieferschein per warehouse), `0x0002 VersandSetzen` (also
create a `dbo.tVersandInfo`/package row + set shipped date on local-warehouse
deliveries). `DeliveryStep` always sets `VersandSetzen`, so the effective value is
**`2`** (or `3` if also splitting by warehouse).
Behavior (`/tmp/sp_ausliefern.sql`, delegates most of the work to sub-procedures, all
scoped to `WHERE ... kSessionId = @kSessionId`):
1. Validates (raises on partially-assigned serial numbers / empty serials for
serial-tracked articles — not relevant unless articles use `cLagerArtikel = 'Y'`
serial tracking).
2. **`Auslieferung.spPicklistenAusliefern_LokaleLager`** (`/tmp/sp_lokal.sql`) — the
part that matters for a normal local-warehouse delivery:
- Builds `@xLieferschein` XML for every order that has committed `tPicklistePos`
rows in this session on a local warehouse (`tWarenLager.nFulfillment = 0`), and
calls **`Versand.spLieferscheinErstellen`** (same proc our code already calls,
but now driven from the Pickliste data, with a properly numbered
`cLieferscheinNr` = `<Auftragsnummer>-NNN`).
- Builds `@xLieferscheinPos` XML (`kLieferschein`, `kBestellPos`,
`fAnzahl` summed per position) and calls **`Versand.spLieferscheinPosErstellen`**
(again, same proc, but now the quantities come from the Picklisten, not
directly from the order).
- **Links the new `kLieferscheinPos` back onto the `tPicklistePos` rows**
(`UPDATE dbo.tPicklistePos SET kLieferscheinPos = ...`) — this is the row our
current code never populates, which is presumably one of the visible
differences you saw between order 25 and order 26.
3. `Auslieferung.spPicklistenAusliefern_Fulfillment` /
`..._Dropshipping` — not relevant for local-warehouse orders.
4. Marks serial-numbered stock items (`dbo.tlagerartikel`) with the resulting
`kBestellPos`/`kLieferscheinPos` (only relevant for `cLagerArtikel = 'Y'`
articles).
5. **Real stock decrement**: builds `@xPicklistePos` (`kPicklistePos`, `kBenutzer`,
`cKommentar`) for every committed local-warehouse `tPicklistePos` row and calls
**`dbo.spWarenlagerAusgangPicklistePos`** (`/tmp/sp_ausgang.sql`), which in turn
builds a `WarenAusgang` XML per position (`kWarenLagerEingang`, `kLieferscheinPos`,
`fAnzahl`, `kWarenlagerPlatz`, `kArtikel`, `kBuchungsart = 20`) and calls
**`dbo.spWarenlagerAusgangSchreiben`** — the actual, final stock-decrement
procedure (writes the goods-issue row and reduces
`tWarenLagerEingang.fAnzahlAktuell`/warehouse stock). **This is the step that
explains the "no real stock decrement" symptom.**
6. Bumps `tPicklistePos` status to **`nStatus = 40`** ("ausgeliefert"/delivered).
7. Copies `@xHinweise` notes onto the new `tLieferschein`/`tLieferscheinPos` rows.
8. `Auslieferung.spPicklistenAusliefern_PaketeErzeugen` (`/tmp/sp_paketeerzeugen.sql`) —
creates the `dbo.tVersand` row(s) and, because `VersandSetzen` is set, marks
local-warehouse deliveries as shipped (`dVersendet = GETDATE()`).
- **Gotcha confirmed by testing**: this sub-proc builds a `#XMLPAKET` temp table
straight from `@Pakete.nodes('/Paket')` and then drives *every* `INSERT INTO
dbo.tVersand` off a join against that temp table (keyed by `kBestellung`). If
`@Pakete` is `NULL`/empty, `#XMLPAKET` has zero rows and the join finds
nothing for **any** order in the session — not even the "no explicit Paket"
fallback branch runs, because that branch is also reached only via the
`cLieferschein` cursor which is itself driven by a `JOIN #XMLPAKET`. Net
effect: passing `@Pakete = NULL` silently skips `dbo.tVersand` entirely, no
error, no shipment method recorded — this is exactly the bug the user
reported (rows shown by the user all have `kVersandArt = 2` /
"Selbstabholer" and a real `dVersendet`, which only happens when `@Pakete`
contains a matching `<Paket>` element for that order).
- Fix: always pass at least
`<Paket><kBestellung>{kAuftrag}</kBestellung><kVersandart>{kVersandArt}</kVersandart><fGewicht>0</fGewicht></Paket>`
(using the order's own `Verkauf.tAuftrag.kVersandArt`). `fGewicht` is a
`NOT NULL` column with no default, so it must be present in the XML — `0` is
safe, it's the exact sentinel value this same proc later looks for
(`WHERE dbo.tVersand.fGewicht = 0`) to recompute a real weight from the
order's article weights a few statements later.
- `cIdentCode`/`cTrackingId`, `dVersanddatum`, `cEnclosedReturnIdentCode`,
`cHinweis` are all optional and can be added later (e.g. once real carrier
integration exists) without changing anything else in this flow.
9. `Auslieferung.spPicklistenAusliefern_Umlagerungen` — stock-transfer orders only,
not relevant here.
10. Returns `@xResult`:
```xml
<Result>
<NewDeliveryNotes><DeliveryNote><kLieferschein>...</kLieferschein></DeliveryNote>...</NewDeliveryNotes>
<ProcessedPicklisten><Pickliste><kPickliste>...</kPickliste></Pickliste>...</ProcessedPicklisten>
<NewFulfillmentauftraege>...</NewFulfillmentauftraege>
<NewLieferantenbestellungen>...</NewLieferantenbestellungen>
<DeliveredOrders><Order><kBestellung>...</kBestellung></Order>...</DeliveredOrders>
<DeliveredUmlagerungen>...</DeliveredUmlagerungen>
</Result>
```
(schema confirmed unobfuscated in `StoredProcedures.AuslieferungSpResult`,
`StoredProcedures.cs:52-186`)
### 3.4 `Verkauf.spAuftragEckdatenBerechnen`
Already correctly called by our existing code (`create-order.js`); recalculates
`nKomplettAusgeliefert` and other derived order fields from the (now populated)
Lieferschein/Pickliste data. No change needed here, just needs to run *after* step
3.3 instead of instead of it.
## 4. `tPicklistePos.nStatus` progression (for reference)
| Status | Meaning | Set by |
|--------|---------------------------------------|-------------------------------------------|
| 0 | just inserted | `spReserviereBestellposition` |
| 5 | reserved / "angelegt" | `spReserviereBestellpositionen` (end) |
| 10 | committed / "übernommen" | `spPicklistenUebernehmen` |
| 20 | committed (special position types 18/20) | `spPicklistenUebernehmen` |
| 40 | delivered / "ausgeliefert" | `spPicklistenAusliefern` (via `_LokaleLager`) |
## 5. What our code does today (`src/queries/create-order.js`)
`deliverOrder()` calls `Versand.spLieferscheinErstellen` +
`Versand.spLieferscheinPosErstellen` directly per position — i.e. it starts at
step 3.3's *inner* Lieferschein-creation calls, skipping everything before it
(§3.1 reserve, §3.2 commit) and everything after within 3.3 (Pickliste linking,
real stock decrement, package/ship-date handling). It happens to still flip
`nKomplettAusgeliefert` because `spLieferscheinPosErstellen`/our subsequent call to
`Verkauf.spAuftragEckdatenBerechnen` don't care where the Lieferschein numbers came
from — they just see delivered quantities recorded on `tLieferschein`. Net effect:
- No `dbo.tPickliste` / `dbo.tPicklistePos` rows.
- No real warehouse stock (`dbo.tWarenLagerEingang.fAnzahlAktuell` /
`dbo.tlagerbestand`) decrement.
- `cLieferscheinNr` isn't numbered per-order the way the real flow does it
(`<AuftragsNr>-001`).
## 6. Plan to reproduce exactly
Rewrite `deliverOrder()` in `create-order.js` to, per order:
1. `INSERT INTO dbo.tSessionId (cRechnername, kBenutzer, dLastAction) VALUES
('jtlsrv', @kBenutzer, DATEADD(day, 10, GETDATE()))`, read back `kSessionId`
(`SCOPE_IDENTITY()`).
2. For every order position with a real `kArtikel` (skip pure text/shipping-only
rows if the article resolves to 0 — those go through the "Freiposition" path in
§3.1, which our `@Laeger` XML already supports the same way): build
`@Bestellpositionen` (one `<Bestellposition>` per position, full ordered
quantity) and `@Laeger` (single `<Lager>` with the resolved outgoing
`kWarenlager`, `nPrio=0`, `kLieferant=0`, `kAnsprechpartner=0`), leave
`@Warenlagereingaenge` `NULL`, `@nOptions = 0x0102` (`ChargenVorbelegen |
Stücklistenkorrektur`). Call `Auslieferung.spReserviereBestellpositionen`.
3. Call `Auslieferung.spPicklistenUebernehmen` with `@Bestellungen =
<Bestellung><kBestellung>{kAuftrag}</kBestellung></Bestellung>`,
`@nTeillieferung = 0`, same `@kSessionId`.
4. Call `Auslieferung.spPicklistenAusliefern` with `@xHinweise = NULL`, `@Pakete =
<Paket><kBestellung>{kAuftrag}</kBestellung><kVersandart>{kVersandArt}</kVersandart><fGewicht>0</fGewicht></Paket>`
(using the order's own `Verkauf.tAuftrag.kVersandArt`; **must not be `NULL`** or
`dbo.tVersand` never gets a row for this order at all — see §3.3 step 8),
`@nOptions = 2` (`VersandSetzen`), same `@kSessionId`.
5. Call `Verkauf.spAuftragEckdatenBerechnen(@kAuftrag)` (already implemented).
6. On any failure in 2-4, call `Auslieferung.spPicklistenVerwerfen(@kBenutzer,
@kSessionId)` to discard the session's half-finished Picklisten before
re-throwing.
7. Optionally `DELETE FROM dbo.tSessionId WHERE kSessionId = @kSessionId` at the
end (mirrors `SessionManager.Dispose()`); not strictly required for
correctness since it only affects the GUI's "who's editing what" bookkeeping,
but keeps `tSessionId` from accumulating rows.
## 7. Open questions / remaining unknowns
- **Outgoing warehouse resolution** (`PosWarehouseService.FindeWarenlagerFürAusgang()`,
`jtlCore`, obfuscated): need to determine which `dbo.tWarenLager.kWarenLager` to
put in `@Laeger`. Likely candidates: the single active local warehouse
(`nFulfillment = 0`) if there's only one, or a per-shop/subshop default warehouse
setting somewhere in `dbo.tShopSubshop`/shop config. Needs to be resolved (e.g. by
decompiling `PosWarehouseService` from `jtlCore.dll` with `ilspycmd`, or reading
`sp_helptext` of whatever proc/view it ultimately queries) before step 2 above can
pick the right warehouse automatically instead of hard-coding it.
- **Article resolution / stock articles vs. free positions**: `spReserviereBestellposition`
branches heavily on `tArtikel.cLagerVariation`/`cLagerAktiv`/`cLagerArtikel` and
dropshipping supplier data; our current order items are simple retail SKUs, so the
"Artikel mit Lagerbestand (Warenläger)" branch should apply, but this needs
verification against a real low/zero-stock article to see how shortfalls are
handled (`PosStockPositionService.FehlbestandEinbuchen` in `DeliveryStep`, not yet
traced).
- **Serial-number / batch tracked articles**: out of scope for now (our catalog
doesn't seem to use `cLagerArtikel = 'Y'` serial tracking), but `spPicklistenAusliefern`
will raise an error (`RAISERROR(..., 18, 3)` / `(..., 18, 6)`) if it ever does and we
don't supply serial numbers via `Auslieferung.spReserviereSeriennummern` first.
## 8. Implementation status
Implemented in `src/queries/delivery/` (`session.js`, `warehouse.js`, `reserve.js`,
`commit.js`, `deliver.js`, `index.js`), wired into `create-order.js` in place of the
old `Versand.spLieferscheinErstellen`/`spLieferscheinPosErstellen` shortcut. See
those files' doc comments for the mapping back to the steps in §0/§6.
Verified end-to-end against a live test order in `Mandant_3`: real stock decrement,
`dbo.tPickliste`/`tPicklistePos` created with `nStatus = 40` and a real
`kLieferscheinPos` link, properly numbered `Lieferschein` (`<AuftragsNr>-001`),
`nKomplettAusgeliefert = 1`, and the session row cleaned up afterward.
One gotcha found only through this live testing, not visible from the decompiled
C# alone (**`AuslieferungAusliefernContext`/`PosBookPositionService` build their own
`Paket`-equivalent state in-memory before calling the stored procedure, so this
particular pitfall is specific to calling the raw SQL procedure directly**): passing
`@Pakete = NULL` to `Auslieferung.spPicklistenAusliefern` silently skips `dbo.tVersand`
row creation entirely (see §3.3 step 8 for the full mechanism). Fixed by always
passing a `<Paket>` element with the order's `kVersandArt` (and a `fGewicht = 0`
sentinel, since the column is `NOT NULL`). This is what actually makes
`dbo.tVersand` end up with a row like `kVersandArt = 2` ("Selbstabholer") and a real
`dVersendet`, matching native JTL-Wawi/POS orders.
A second, unrelated tedious/mssql driver limitation was hit and fixed: the `xml` SQL
type does not reliably round-trip through `sql.Xml` typed parameters/output
parameters for these particular procedures (`Implicit conversion from data type xml
to nvarchar is not allowed`). Worked around by passing XML as `NVarChar(MAX)` and
`CONVERT(XML, @param)`-ing it inside the SQL batch itself, and reading `@xResult
OUTPUT` back via a trailing `SELECT` instead of a driver-level output parameter.

341
order.md
View File

@@ -1,341 +0,0 @@
socket.on("sendOrder", async (customerOrder, paymentNote, customerNote, delivery, employee, article, articleAmount, customerId, orderType, callback) => {
logger.info([socket.id, 'sendOrder', JSON.stringify(customerOrder, null, 2), paymentNote, customerNote, delivery, employee, article, articleAmount, customerId, orderType]);
if (article) {
paymentNote = "[AZ] " + paymentNote;
}
let transaction;
let rolledBack = false;
try {
transaction = new sql.Transaction(pool);
await transaction.begin(sql.ISOLATION_LEVEL.READ_UNCOMMITTED);
transaction.on("rollback", () => {
rolledBack = true;
});
let kKunde;
const resultNum = await new sql.Request(transaction).input("cKundenNr", customerId).query("SELECT 1 as collision,tKunde.kKunde FROM dbo.tKunde WHERE cKundenNr = @cKundenNr");
if (resultNum.recordset[0] && resultNum.recordset[0].collision == 1) {
kKunde = resultNum.recordset[0].kKunde;
} else {
throw new Error('falsche Kundennummer');
}
if (orderType != 'offer') orderType = 'order';
const resultNumA = await new sql.Request(transaction).query("DECLARE @Number int; " + "UPDATE dbo.tLaufendeNummern SET nNummer = nNummer + 1 , @Number = nNummer WHERE kLaufendeNummer = " + ((orderType == 'order') ? 3 : 4) + "; " + "SELECT @Number as auftragNummer;");
const auftragNummer = resultNumA.recordset[0].auftragNummer;
const resultAuftrag = await new sql.Request(transaction)
.input("cAuftragsNr", ((orderType == 'order') ? 'A-' : 'AN-') + auftragNummer)
.input("kKunde", kKunde)
.input("nType", (orderType == 'order') ? 1 : 0)
.input("cKundenNr", customerId)
.input("dVoraussichtlichesLieferdatum", sql.DateTime2, new Date(delivery))
.query(
`DECLARE @t table([kAuftrag] int); INSERT INTO [eazybusiness].[Verkauf].[tAuftrag]
(cAuftragsNr,dVoraussichtlichesLieferdatum,nKomplettAusgeliefert,kBenutzer,kKunde,kBenutzerErstellt,nType,fFaktor,kFirmaHistory,kSprache,cVersandlandWaehrung,fVersandlandWaehrungFaktor,fFinanzierungskosten,cWaehrung,kPlattform,cKundenNr,cVersandlandISO,kVersandArt,kZahlungsart,kKundengruppe)
OUTPUT inserted.kAuftrag INTO @t values (@cAuftragsNr,@dVoraussichtlichesLieferdatum,0,1,@kKunde,1,@nType,1.0,5,1,'EUR',1.0,0.0,'EUR',1,@cKundenNr,'DE',2,11,1);SELECT * FROM @t`
);
const kAuftrag = resultAuftrag.recordset[0].kAuftrag;
await new sql.Request(transaction).input("kAuftrag", kAuftrag).input("kKunde", kKunde).query(
`insert into Verkauf.tAuftragAdresse (kAuftrag,kKunde,cFirma,cAnrede,cTitel,cVorname,cName,cStrasse,cPLZ,cOrt,cLand,cTel,cZusatz,cAdressZusatz,cMobil,cMail,cFax,cBundesland,cISO,nTyp,nZolldokumenteErforderlich)
values (@kAuftrag,@kKunde,'','','','','-','-','','-','Deutschland','','','','','','','','DE',0,0)`
);
await new sql.Request(transaction).input("kAuftrag", kAuftrag).input("kKunde", kKunde).query(
`insert into Verkauf.tAuftragAdresse (kAuftrag,kKunde,cFirma,cAnrede,cTitel,cVorname,cName,cStrasse,cPLZ,cOrt,cLand,cTel,cZusatz,cAdressZusatz,cMobil,cMail,cFax,cBundesland,cISO,nTyp,nZolldokumenteErforderlich)
values (@kAuftrag,@kKunde,'','','','','-','-','','-','Deutschland','','','','','','','','DE',1,0)`
);
for (const item of customerOrder) {
if (item.price == null) item.price = 0;
const mwst = ((parseInt(item.kSteuerklasse) === 1) ? 19 : ((parseInt(item.kSteuerklasse) === 2) ? 7 : 0));
if (item.type == "free") {
const res_a = await new sql.Request(transaction)
.input("kAuftrag", kAuftrag)
.input("fVkNetto", parseFloat(item.price) / ((mwst + 100) / 100))
.input("cName", item.cName)
.input("fRabatt", parseFloat(item.rabatt.toString().replace(/,/, '.')))
.input("fAnzahl", item.count)
.input("fMwSt", mwst)
.input("kSteuerklasse", item.kSteuerklasse)
.query(
`INSERT INTO Verkauf.tAuftragPosition (kArtikel,kAuftrag,cArtNr,nReserviert,cName,cHinweis,fAnzahl,fVkNetto,fMwSt,cNameStandard,kSteuerklasse,nType,cEinheit,fFaktor,kSteuerschluessel,fRabatt)
VALUES (NULL,@kAuftrag,NULL,1,@cName,'',@fAnzahl,@fVkNetto,@fMwSt,@cName,@kSteuerklasse,0,NULL,1.0,3,@fRabatt); SELECT SCOPE_IDENTITY() AS kAuftragPosition`
);
await new sql.Request(transaction)
.input("kRef1", kAuftrag)
.input("cRef1", 'kAuftrag')
.input("kRef2", res_a.recordset[0].kAuftragPosition)
.input("cRef2", 'kAuftragPosition')
.input("cType", "handle_freePos_order")
.input("cState", "new")
.query(
`INSERT INTO dbo.tIncident (cType,cState,cRef1,kRef1,cRef2,kRef2)
VALUES (@cType,@cState,@cRef1,@kRef1,@cRef2,@kRef2)`
);
} else if (item.type == "article") {
await new sql.Request(transaction)
.input("kAuftrag", kAuftrag)
.input("fVkNetto", parseFloat(item.price) / ((mwst + 100) / 100))
.input("fAnzahl", item.count)
.input("cName", item.cName)
.input("fRabatt", parseFloat(item.rabatt.toString().replace(/,/, '.')))
.input("cArtNr", item.cArtNr)
.input("fMwSt", mwst)
.input("kSteuerklasse", item.kSteuerklasse)
.input("kArtikel", item.kArtikel)
.query(
`INSERT INTO Verkauf.tAuftragPosition (kArtikel,kAuftrag,cArtNr,nReserviert,cName,cHinweis,fAnzahl,fVkNetto,fMwSt,cNameStandard,kSteuerklasse,nType,cEinheit,fFaktor,kSteuerschluessel,fRabatt)
VALUES (@kArtikel,@kAuftrag,@cArtNr,1,@cName,'',@fAnzahl,@fVkNetto,@fMwSt,@cName,@kSteuerklasse,1,'',1.0,3,@fRabatt)`
);
}
}
await pool.request().input("kAuftrag", kAuftrag).query(
`declare @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen
insert into @p1 values(@kAuftrag)
exec Verkauf.spAuftragEckdatenBerechnen @auftrag=@p1`
);
await transaction.commit();
SELECT TOP (1000) [kZahlungsart]
,[cName]
,[cPrtString]
,[nLastschrift]
,[cPrtStringVor]
,[cPaymentOption]
,[cKonto]
,[nAusliefernVorZahlung]
,[nPrioritaet]
,[nMahnwesenAktiv]
,[fSkontoWert]
,[nSkontoZeitraum]
,[bRowversion]
,[nMatchingOptionen]
,[nIstStandard]
,[nAktiv]
FROM [eazybusiness].[dbo].[tZahlungsart]
kZahlungsart cName cPrtString nLastschrift cPrtStringVor cPaymentOption cKonto nAusliefernVorZahlung nPrioritaet nMahnwesenAktiv fSkontoWert nSkontoZeitraum bRowversion nMatchingOptionen nIstStandard nAktiv
1 Bar NULL NULL NULL 0 0 0 NULL NULL 0x00000000000019D8 0 1 1
2 Überweisung NULL NULL NULL 0 0 0 NULL NULL 0x00000000000019D9 0 0 1
3 Scheck NULL NULL NULL 0 0 0 NULL NULL 0x00000000000019DA 0 0 1
4 Kreditkarte NULL NULL NULL 0 0 0 NULL NULL 0x00000000000019DB 0 0 1
5 eBay Rechnungskauf NULL 0 NULL NULL NULL 0 0 NULL NULL 0x00000000000019DC 0 0 1
6 eBay Managed Payments NULL NULL NULL NULL NULL NULL 0 0 NULL NULL 0x00000000000019DD 0 0 1
7 Karte 0 0 0 0 0.0000000000000 0 0x00000000000387CA 0 0 0
8 k2 0 0 0 0 0.0000000000000 0 0x0000000000038ABD 0 0 0
SELECT TOP (1000) [kZahlung]
,[cName]
,[dDatum]
,[fBetrag]
,[kBestellung]
,[kBenutzer]
,[nAnzahlung]
,[cHinweis]
,[kZahlungsart]
,[nKeinExport]
,[cSKRManuell]
,[cExternalTransactionId]
,[kZahlungsabgleichUmsatz]
,[nZuweisungstyp]
,[nZahlungstyp]
,[cZuweisungsinfo]
,[nZuweisungswertung]
,[kRechnung]
,[bRowversion]
,[kGutschrift]
,[kEingangsrechnung]
FROM [eazybusiness].[dbo].[tZahlung]
kZahlung cName dDatum fBetrag kBestellung kBenutzer nAnzahlung cHinweis kZahlungsart nKeinExport cSKRManuell cExternalTransactionId kZahlungsabgleichUmsatz nZuweisungstyp nZahlungstyp cZuweisungsinfo nZuweisungswertung kRechnung bRowversion kGutschrift kEingangsrechnung
187 Bar 2026-07-11 19:08:10.000 14.3000000000000 1152 1 0 1 0 NULL R00178 NULL 0 0 0 NULL 0x0000000000045A6A NULL NULL
188 Bar 2026-07-11 19:10:07.000 2.2000000000000 1153 1 0 1 0 NULL R00179 NULL 0 0 0 NULL 0x0000000000045B1E NULL NULL
189 Karte 2026-07-11 19:26:52.000 9.9500000000000 1154 1 0 7 0 R00180 NULL 0 0 0 NULL 0x0000000000045C55 NULL NULL
190 Bar 2026-07-11 19:45:29.000 12.0000000000000 1155 1 0 1 0 NULL R00181 NULL 0 0 0 NULL 0x0000000000045DB2 NULL NULL
191 Bar 2026-07-11 19:47:45.000 12.7500000000000 1156 1 0 1 0 NULL R00182 NULL 0 0 0 NULL 0x0000000000045E8B NULL NULL
192 Karte 2026-07-11 19:59:29.000 18.4500000000000 1157 1 0 7 0 R00183 NULL 0 0 0 NULL 0x0000000000045F62 NULL NULL
193 Bar 2026-07-11 20:01:38.000 19.0000000000000 1158 1 0 1 0 NULL R00184 NULL 0 0 0 NULL 0x0000000000045FF7 NULL NULL
194 Karte 2026-07-11 20:02:56.000 2.3000000000000 1159 1 0 7 0 R00185 NULL 0 0 0 NULL 0x0000000000046068 NULL NULL
195 Karte 2026-07-11 20:03:35.000 1.0000000000000 1160 1 0 7 0 R00186 NULL 0 0 0 NULL 0x00000000000460D9 NULL NULL
196 Bar 2026-07-11 20:15:51.000 33.6000000000000 1161 1 0 1 0 NULL R00187 NULL 0 0 0 NULL 0x00000000000461DB NULL NULL
197 Karte 2026-07-11 20:25:56.000 14.0000000000000 1162 1 0 7 0 R00188 NULL 0 0 0 NULL 0x0000000000046299 NULL NULL
198 Karte 2026-07-11 20:27:25.000 6.7000000000000 1163 1 0 7 0 R00189 NULL 0 0 0 NULL 0x0000000000046307 NULL NULL
199 Karte 2026-07-11 20:28:02.000 6.9500000000000 1164 1 0 7 0 R00190 NULL 0 0 0 NULL 0x000000000004635F NULL NULL
SELECT TOP (1000) [cName]
,[nummer]
,[dChanged]
,[bRowversion]
FROM [eazybusiness].[dbo].[tpk]
cName nummer dChanged bRowversion
tZahlung 200 2026-07-11 0x0000000000046358
cName nummer dChanged bRowversion
tZahlungsart 9 2026-07-07 0x0000000000038ABC
SELECT TOP (1000) [kVersand]
,[kLieferschein]
,[kBenutzer]
,[kLogistik]
,[cIdentCode]
,[dErstellt]
,[cHinweis]
,[fGewicht]
,[kVersandArt]
,[cLogistiker]
,[cFulfillmentCenter]
,[dAnkunftszeit]
,[nVerpackZeitSek]
,[dVersendet]
,[nStatus]
,[cShipmentId]
,[cReference]
,[cShipmentOrderId]
,[kKartonAuftragPos]
,[bRowversion]
,[cEnclosedReturnIdentCode]
,[nViaAmazonMWS]
,[kReturnVersandart]
FROM [eazybusiness].[dbo].[tVersand]
kVersand kLieferschein kBenutzer kLogistik cIdentCode dErstellt cHinweis fGewicht kVersandArt cLogistiker cFulfillmentCenter dAnkunftszeit nVerpackZeitSek dVersendet nStatus cShipmentId cReference cShipmentOrderId kKartonAuftragPos bRowversion cEnclosedReturnIdentCode nViaAmazonMWS
1149 1149 1 0 2026-07-11 16:45:43.380 0.0000000000000 2 NULL 0 2026-07-11 16:45:43.380 0 NULL NULL NULL NULL 0x00000000000458FB 0
1150 1150 1 0 2026-07-11 17:07:56.030 0.0000000000000 2 NULL 0 2026-07-11 17:07:56.030 0 NULL NULL NULL NULL 0x0000000000045A19 0
1151 1151 1 0 2026-07-11 17:10:04.430 0.0000000000000 2 NULL 0 2026-07-11 17:10:04.430 0 NULL NULL NULL NULL 0x0000000000045AD4 0
1152 1152 1 0 2026-07-11 17:11:14.023 0.0000000000000 2 NULL 0 2026-07-11 17:11:14.023 0 NULL NULL NULL NULL 0x0000000000045B43 0
1153 1153 1 0 2026-07-11 17:27:14.687 0.0000000000000 2 NULL 0 2026-07-11 17:27:14.687 0 NULL NULL NULL NULL 0x0000000000045C7A 0
1154 1154 1 0 2026-07-11 17:47:24.573 0.0000000000000 2 NULL 0 2026-07-11 17:47:24.570 0 NULL NULL NULL NULL 0x0000000000045E1C 0
1155 1155 1 0 2026-07-11 17:52:59.990 0.0000000000000 2 NULL 0 2026-07-11 17:52:59.990 0 NULL NULL NULL NULL 0x0000000000045ECE 0
1156 1156 1 0 2026-07-11 18:01:31.267 0.0000000000000 2 NULL 0 2026-07-11 18:01:31.267 0 NULL NULL NULL NULL 0x0000000000045FAF 0
1157 1157 1 0 2026-07-11 18:02:07.393 0.0000000000000 2 NULL 0 2026-07-11 18:02:07.393 0 NULL NULL NULL NULL 0x0000000000046026 0
1158 1158 1 0 2026-07-11 18:03:15.873 0.0000000000000 2 NULL 0 2026-07-11 18:03:15.873 0 NULL NULL NULL NULL 0x000000000004609F 0
1159 1159 1 0 2026-07-11 18:03:50.530 0.0000000000000 2 NULL 0 2026-07-11 18:03:50.530 0 NULL NULL NULL NULL 0x0000000000046102 0
1160 1160 1 0 2026-07-11 18:19:57.820 0.0000000000000 2 NULL 0 2026-07-11 18:19:57.817 0 NULL NULL NULL NULL 0x0000000000046233 0
1161 1161 1 0 2026-07-11 18:26:14.487 0.0000000000000 2 NULL 0 2026-07-11 18:26:14.487 0 NULL NULL NULL NULL 0x00000000000462C7 0
1162 1162 1 0 2026-07-11 18:27:55.337 0.0000000000000 2 NULL 0 2026-07-11 18:27:55.337 0 NULL NULL NULL NULL 0x000000000004632C 0
1163 1163 1 0 2026-07-11 18:28:26.020 0.0000000000000 2 NULL 0 2026-07-11 18:28:26.017 0 NULL NULL NULL NULL 0x0000000000046384 0
SELECT TOP (1000) [kAuftrag]
,[kBenutzer]
,[kKunde]
,[cAuftragsNr]
,[nType]
,[dErstellt]
,[kShopauftrag]
,[nBeschreibung]
,[cInet]
,[cWaehrung]
,[fFaktor]
,[kShop]
,[kFirmaHistory]
,[kPlattform]
,[kSprache]
,[cExterneAuftragsnummer]
,[nSteuereinstellung]
,[cEbayUsername]
,[cShopZahlungsmodul]
,[nHatUpload]
,[fZusatzGewicht]
,[cVersandlandISO]
,[cVersandlandBundeslandKuerzel]
,[cVersandlandWaehrung]
,[fVersandlandWaehrungFaktor]
,[kVersandArt]
,[nZahlungszielTage]
,[dVoraussichtlichesLieferdatum]
,[dAuslieferdatum]
,[fSkonto]
,[nSkontoTage]
,[kVorgangsstatus]
,[nStorno]
,[nKomplettAusgeliefert]
,[nLieferPrioritaet]
,[nPremiumVersand]
,[kRueckhaltegrund]
,[kZahlungsart]
,[kWarenlager]
,[nIstExterneRechnung]
,[nIstReadOnly]
,[nMaxLiefertage]
,[cOutboundId]
,[kFulfillmentLieferant]
,[cUstId]
,[nArchiv]
,[nReserviert]
,[nDebitorennr]
,[nAuftragStatus]
,[cKundenAuftragsnummer]
,[cAmazonServiceLevel]
,[dExternesErstelldatum]
,[kFarbe]
,[fFinanzierungskosten]
,[kAuftragQuelle]
,[nAuftragQuelleAktion]
,[kArtikelKarton]
,[bRowversion]
,[cKundenNr]
,[kKundengruppe]
,[dErstelltWawi]
,[dAuslieferungAb]
,[kAmazonUser]
,[cKundeUstId]
,[nPending]
,[kBenutzerErstellt]
,[nSteuersonderbehandlung]
FROM [eazybusiness].[Verkauf].[tAuftrag]
kAuftrag kBenutzer kKunde cAuftragsNr nType dErstellt kShopauftrag nBeschreibung cInet cWaehrung fFaktor kShop kFirmaHistory kPlattform kSprache cExterneAuftragsnummer nSteuereinstellung cEbayUsername cShopZahlungsmodul nHatUpload fZusatzGewicht cVersandlandISO cVersandlandBundeslandKuerzel cVersandlandWaehrung fVersandlandWaehrungFaktor kVersandArt nZahlungszielTage dVoraussichtlichesLieferdatum dAuslieferdatum fSkonto nSkontoTage kVorgangsstatus nStorno nKomplettAusgeliefert nLieferPrioritaet nPremiumVersand kRueckhaltegrund kZahlungsart kWarenlager nIstExterneRechnung nIstReadOnly nMaxLiefertage cOutboundId kFulfillmentLieferant cUstId nArchiv nReserviert nDebitorennr nAuftragStatus cKundenAuftragsnummer cAmazonServiceLevel dExternesErstelldatum kFarbe fFinanzierungskosten kAuftragQuelle nAuftragQuelleAktion kArtikelKarton bRowversion cKundenNr kKundengruppe dErstelltWawi dAuslieferungAb kAmazonUser cKundeUstId nPending kBenutzerErstellt nSteuersonderbehandlung
1148 1 1 A10182 1 2026-07-11 18:39:30.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00174 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045814 0 1 2026-07-11 16:39:55.640 NULL NULL 0 0 0
1149 1 1 A10183 1 2026-07-11 18:43:51.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00175 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045894 0 1 2026-07-11 16:44:08.557 NULL NULL 0 0 0
1150 1 1 A10184 1 2026-07-11 18:45:08.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00176 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x000000000004590B 0 1 2026-07-11 16:45:42.850 NULL NULL 0 0 0
1151 1 1 A10185 1 2026-07-11 18:47:54.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00177 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045A2A 0 1 2026-07-11 17:07:55.803 NULL NULL 0 0 0
1152 1 1 A10186 1 2026-07-11 19:08:10.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00178 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045AF1 0 1 2026-07-11 17:10:04.070 NULL NULL 0 0 0
1153 1 1 A10187 1 2026-07-11 19:10:07.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00179 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045B51 0 1 2026-07-11 17:11:13.527 NULL NULL 0 0 0
1154 1 1 A10188 1 2026-07-11 19:26:52.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00180 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045C88 0 1 2026-07-11 17:27:14.490 NULL NULL 0 0 0
1155 1 1 A10189 1 2026-07-11 19:45:29.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00181 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045E2D 0 1 2026-07-11 17:47:23.770 NULL NULL 0 0 0
1156 1 1 A10190 1 2026-07-11 19:47:45.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00182 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045EE2 0 1 2026-07-11 17:52:59.733 NULL NULL 0 0 0
1157 1 1 A10191 1 2026-07-11 19:59:29.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00183 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000045FC3 0 1 2026-07-11 18:01:30.943 NULL NULL 0 0 0
1158 1 1 A10192 1 2026-07-11 20:01:38.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00184 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000046034 0 1 2026-07-11 18:02:07.167 NULL NULL 0 0 0
1159 1 1 A10193 1 2026-07-11 20:02:56.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00185 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x00000000000460B0 0 1 2026-07-11 18:03:15.633 NULL NULL 0 0 0
1160 1 1 A10194 1 2026-07-11 20:03:35.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00186 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000046110 0 1 2026-07-11 18:03:50.343 NULL NULL 0 0 0
1161 1 1 A10195 1 2026-07-11 20:15:51.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00187 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 1 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x000000000004624A 0 1 2026-07-11 18:19:57.537 NULL NULL 0 0 0
1162 1 1 A10196 1 2026-07-11 20:25:56.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00188 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x00000000000462D5 0 1 2026-07-11 18:26:14.297 NULL NULL 0 0 0
1163 1 1 A10197 1 2026-07-11 20:27:25.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00189 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x000000000004633A 0 1 2026-07-11 18:27:55.140 NULL NULL 0 0 0
1164 1 1 A10198 1 2026-07-11 20:28:02.000 0 0 Y EUR 1.0000000000000 2 2 7 1 R00190 0 0 0.0000000000000 DE EUR 1.0000000000000 2 0 NULL NULL 0.0000000000000 NULL NULL 0 1 10 0 NULL 7 NULL 1 2 NULL NULL DE453945359 0 1 NULL 0 NULL NULL 0.0000000000000 NULL NULL NULL 0x0000000000046392 0 1 2026-07-11 18:28:25.833 NULL NULL 0 0 0

View File

@@ -1,6 +1,7 @@
import sql from 'mssql';
import { getPool } from '../db.js';
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
import { deliverOrder } from './delivery/index.js';
const config = {
kBenutzer: Number(process.env.JTL_KBENUTZER) || 1,
@@ -389,13 +390,12 @@ async function insertPosOrderPositionMapping(transaction, kAuftragPosition, kPos
* order is created, `Settings.Deliver` (default true, see decompiled
* jtlCore.Classes.Sync.PosOrderSteps.DeliveryStep:199,
* `bool flag = orderEntity.Settings?.Deliver ?? true;`) decides whether the
* order is delivered immediately. JTL's real implementation books stock via
* the full Auslieferung/warehouse engine; we reproduce its DB end-state
* directly via the same stored procedures it ultimately relies on
* (Versand.spLieferscheinErstellen / spLieferscheinPosErstellen), which also
* call Verkauf.spAuftragEckdatenBerechnen and are what actually derive
* nKomplettAusgeliefert. Digital-voucher partial delivery (IstGutscheinDigital)
* is intentionally not reproduced.
* order is delivered immediately. Digital-voucher partial delivery
* (IstGutscheinDigital) is intentionally not reproduced.
*
* The actual delivery mechanics (Pickliste creation, real stock decrement,
* numbered Lieferschein) live in ./delivery/ - see delivery.md for the full
* investigation behind it.
*/
function isOrderDelivered(order) {
const deliver = order.settings?.deliver;
@@ -405,48 +405,6 @@ function isOrderDelivered(order) {
return deliver === true || String(deliver) === '1' || String(deliver).toLowerCase() === 'true';
}
async function createLieferschein(transaction, kAuftrag) {
const request = new sql.Request(transaction);
request.input('kBestellung', sql.Int, kAuftrag);
request.input('kBenutzer', sql.Int, config.kBenutzer);
request.input('cLieferscheinNr', sql.NVarChar(50), null);
request.input('cHinweis', sql.NVarChar(255), null);
request.input('dMailVersand', sql.DateTime, null);
request.input('dGedruckt', sql.DateTime, null);
request.input('nFulfillment', sql.Int, null);
request.input('kLieferantenBestellung', sql.Int, null);
request.input('kSessionId', sql.Int, null);
request.output('kLieferschein', sql.Int);
const result = await request.execute('Versand.spLieferscheinErstellen');
return result.output.kLieferschein;
}
async function createLieferscheinPos(transaction, kLieferschein, kAuftragPosition, quantity) {
const request = new sql.Request(transaction);
request.input('kLieferschein', sql.Int, kLieferschein);
request.input('kBestellPos', sql.Int, kAuftragPosition);
request.input('fAnzahl', sql.Decimal(25, 13), quantity);
request.input('cHinweis', sql.NVarChar(4000), '');
request.output('kLieferscheinPos', sql.Int);
await request.execute('Versand.spLieferscheinPosErstellen');
}
/**
* Delivers the full ordered quantity of every position (no partial delivery),
* mirroring DeliveryStep's non-voucher path. Uses the server's current time
* throughout (see createOrder), so there is never a past order date to
* backdate Versanddatum against.
*/
async function deliverOrder(transaction, kAuftrag, deliveredItems) {
if (!deliveredItems.length) {
return;
}
const kLieferschein = await createLieferschein(transaction, kAuftrag);
for (const { kAuftragPosition, quantity } of deliveredItems) {
await createLieferscheinPos(transaction, kLieferschein, kAuftragPosition, quantity);
}
}
async function insertPayment(transaction, kAuftrag, payment, order, orderDate, zahlungsartCache) {
const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache);
const kZahlung = await allocatePk(transaction, 'tZahlung');
@@ -539,7 +497,7 @@ export async function createOrder(order) {
}
if (isOrderDelivered(order)) {
await deliverOrder(transaction, kAuftrag, deliveredItems);
await deliverOrder(transaction, config.kBenutzer, kAuftrag, defaults.kVersandArt, deliveredItems);
}
await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(`

View File

@@ -0,0 +1,34 @@
import sql from 'mssql';
import { xmlTag, xmlElement } from './xml.js';
/**
* Commits this session's reservations into "real" Picklisten (bumps
* dbo.tPicklistePos.nStatus from <10 to 10/20 for this session). Mirrors
* AuslieferungContext.PicklistenCommitten -> Auslieferung.spPicklistenUebernehmen.
*
* nTeillieferung = 0: partial delivery is forbidden, matching create-order.js
* always delivering the full ordered quantity (see delivery.md §6).
*
* XML is passed as NVarChar and CONVERT()ed to xml inside the batch, and
* @xResult is read back via SELECT rather than a true XML OUTPUT parameter
* (tedious does not reliably round-trip xml OUTPUT params for this proc).
*/
export async function commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag) {
const bestellungen = xmlElement('Bestellung', [xmlTag('kBestellung', kAuftrag)]);
await new sql.Request(transaction)
.input('Bestellungen', sql.NVarChar(sql.MAX), bestellungen)
.input('kBenutzer', sql.Int, kBenutzer)
.input('nTeillieferung', sql.Bit, false)
.input('kSessionId', sql.Int, kSessionId)
.query(`
DECLARE @xBestellungen XML = CONVERT(XML, @Bestellungen);
DECLARE @xResult XML;
EXEC Auslieferung.spPicklistenUebernehmen
@Bestellungen = @xBestellungen,
@kBenutzer = @kBenutzer,
@nTeillieferung = @nTeillieferung,
@kSessionId = @kSessionId,
@xResult = @xResult OUTPUT;
`);
}

View File

@@ -0,0 +1,56 @@
import sql from 'mssql';
import { xmlTag, xmlElement } from './xml.js';
// PicklistenAusliefernOptionen.VersandSetzen (0x0002). DeliveryStep.Run always sets
// AuslieferungAusliefernContext.Optionen.VersandSetzen = true for POS/API orders
// (decompiled jtlCore.Classes.Sync.PosOrderSteps.DeliveryStep.cs:213), so we do too
// -- see delivery.md §0.1 for the cross-check that settled this.
const AUSLIEFERN_OPTIONS = 0x002;
/**
* Actually delivers the session's committed Picklisten: creates the properly
* numbered dbo.tLieferschein/tLieferscheinPos rows, links them back onto
* tPicklistePos, decrements real warehouse stock (dbo.spWarenlagerAusgangSchreiben),
* and bumps tPicklistePos.nStatus to 40. Mirrors
* AuslieferungAusliefernContext.Commit -> Auslieferung.spPicklistenAusliefern.
*
* @Pakete matters even for local (non-fulfillment) warehouses: the nested
* Auslieferung.spPicklistenAusliefern_PaketeErzeugen only creates a
* dbo.tVersand row for a Lieferschein if #XMLPAKET (built straight from
* @Pakete) contains at least one row for its kBestellung - if @Pakete is
* NULL, #XMLPAKET stays empty and NO tVersand row is written for ANY
* Lieferschein in the session, at all. So we always pass one <Paket> per
* order carrying its kVersandArt (e.g. "Selbstabholer"), even though we
* don't have real tracking/weight info to report yet.
*
* Returns the raw @xResult XML (NewDeliveryNotes/ProcessedPicklisten/... - see
* delivery.md §3.3), read back via SELECT since tedious does not reliably
* round-trip xml OUTPUT params for this proc.
*/
export async function deliverPicklists(transaction, kBenutzer, kSessionId, kAuftrag, kVersandArt) {
const pakete = xmlElement('Paket', [
xmlTag('kBestellung', kAuftrag),
xmlTag('kVersandart', kVersandArt),
xmlTag('fGewicht', 0), // NOT NULL column; real weight is recomputed from the order's articles further below in the proc
]);
const result = await new sql.Request(transaction)
.input('Pakete', sql.NVarChar(sql.MAX), pakete)
.input('nOptions', sql.Int, AUSLIEFERN_OPTIONS)
.input('kBenutzer', sql.Int, kBenutzer)
.input('kSessionId', sql.Int, kSessionId)
.query(`
DECLARE @xHinweise XML = NULL;
DECLARE @xPakete XML = CONVERT(XML, @Pakete);
DECLARE @xResult XML;
EXEC Auslieferung.spPicklistenAusliefern
@xHinweise = @xHinweise,
@Pakete = @xPakete,
@nOptions = @nOptions,
@kBenutzer = @kBenutzer,
@kSessionId = @kSessionId,
@xResult = @xResult OUTPUT;
SELECT @xResult AS xResult;
`);
return result.recordset[0]?.xResult ?? null;
}

View File

@@ -0,0 +1,45 @@
import { openSession, discardSession, closeSession } from './session.js';
import { getOutgoingWarehouse } from './warehouse.js';
import { reservePositions } from './reserve.js';
import { commitPicklists } from './commit.js';
import { deliverPicklists } from './deliver.js';
/**
* Delivers the full ordered quantity of every position of an order, exactly
* reproducing JTL-Wawi's own fulfillment engine (jtlCore's
* PosOrderCreationService -> DeliveryStep.Run -> AuslieferungAusliefernContext.Commit())
* instead of the old shortcut of calling Versand.spLieferscheinErstellen /
* spLieferscheinPosErstellen directly. See ../../../delivery.md for the full
* investigation (§0 has the literal SQL sequence this function implements).
*
* Creates real dbo.tPickliste/tPicklistePos rows, decrements real warehouse
* stock, produces a properly numbered Lieferschein ("<AuftragsNr>-NNN"), and
* a dbo.tVersand row carrying the order's Versandart (e.g. "Selbstabholer"),
* matching what the Wawi GUI / native POS sync produce.
*/
export async function deliverOrder(transaction, kBenutzer, kAuftrag, kVersandArt, deliveredItems) {
if (!deliveredItems.length) {
return;
}
const kWarenLager = await getOutgoingWarehouse(transaction);
const kSessionId = await openSession(transaction, kBenutzer);
try {
await reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, deliveredItems);
await commitPicklists(transaction, kBenutzer, kSessionId, kAuftrag);
await deliverPicklists(transaction, kBenutzer, kSessionId, kAuftrag, kVersandArt);
} finally {
// Safe to run unconditionally (success or error): spPicklistenVerwerfen only
// ever removes not-yet-delivered (nStatus < 10) Picklisten for this session.
// Swallow cleanup errors so they never mask an earlier, more relevant error
// (the outer transaction rollback in create-order.js is what actually matters
// on failure).
try {
await discardSession(transaction, kBenutzer, kSessionId);
await closeSession(transaction, kSessionId);
} catch {
// best-effort cleanup only
}
}
}

View File

@@ -0,0 +1,54 @@
import sql from 'mssql';
import { xmlTag, xmlElement } from './xml.js';
// ReserviereBestellpositionenOptionen: ChargenVorbelegen (0x0002) | Stücklistenkorrektur (0x0100).
// Matches jtlCore's AuslieferungContext ctor + the StoredProcedures.cs wrapper for
// Auslieferung_spReserviereBestellpositionen (see delivery.md §3.1).
const RESERVIERE_OPTIONS = 0x102;
/**
* Reserves every order position against the outgoing warehouse, creating
* dbo.tPickliste/tPicklistePos rows (status 0 -> 5) for this session.
* Mirrors PosBookPositionService.BookPosition -> AuslieferungContext.NeuReservieren
* -> Auslieferung.spReserviereBestellpositionen.
*
* XML parameters are passed as NVarChar and CONVERT()ed to xml in the SQL
* batch itself (rather than using sql.Xml directly), because tedious does
* not reliably round-trip the `xml` SQL type for these procedures.
*/
export async function reservePositions(transaction, kBenutzer, kSessionId, kWarenLager, positions) {
if (!positions.length) {
return;
}
const bestellpositionen = positions
.map(({ kAuftragPosition, quantity }) =>
xmlElement('Bestellposition', [xmlTag('kBestellPos', kAuftragPosition), xmlTag('fAnzahl', quantity)])
)
.join('');
const laeger = xmlElement('Lager', [
xmlTag('kWarenlager', kWarenLager),
xmlTag('nPrio', 0),
xmlTag('kLieferant', 0),
xmlTag('kAnsprechpartner', 0),
]);
await new sql.Request(transaction)
.input('Bestellpositionen', sql.NVarChar(sql.MAX), bestellpositionen)
.input('Laeger', sql.NVarChar(sql.MAX), laeger)
.input('nOptions', sql.Int, RESERVIERE_OPTIONS)
.input('kBenutzer', sql.Int, kBenutzer)
.input('kSessionId', sql.Int, kSessionId)
.query(`
DECLARE @xBestellpositionen XML = CONVERT(XML, @Bestellpositionen);
DECLARE @xLaeger XML = CONVERT(XML, @Laeger);
EXEC Auslieferung.spReserviereBestellpositionen
@Bestellpositionen = @xBestellpositionen,
@Laeger = @xLaeger,
@Warenlagereingaenge = NULL,
@nOptions = @nOptions,
@kBenutzer = @kBenutzer,
@kSessionId = @kSessionId;
`);
}

View File

@@ -0,0 +1,46 @@
import sql from 'mssql';
/**
* Opens a JTL "Auslieferung" session by inserting a row into dbo.tSessionId,
* mirroring jtlCore.Classes.SessionManager.GetSessionId(). All of the
* Auslieferung.sp* calls in this delivery/ folder are scoped to this
* kSessionId (see ../../../delivery.md §2).
*/
export async function openSession(transaction, kBenutzer, hostname = 'jtlsrv') {
const result = await new sql.Request(transaction)
.input('cRechnername', sql.NVarChar(255), hostname)
.input('kBenutzer', sql.Int, kBenutzer)
.query(`
DECLARE @t TABLE ([kSessionId] INT);
INSERT INTO dbo.tSessionId (cRechnername, kBenutzer, dLastAction)
OUTPUT inserted.kSessionId INTO @t
VALUES (@cRechnername, @kBenutzer, DATEADD(day, 10, GETDATE()));
SELECT kSessionId FROM @t;
`);
return result.recordset[0].kSessionId;
}
/**
* Discards any reservation belonging to this session that never made it into
* a delivered Pickliste. Auslieferung.spPicklistenVerwerfen only ever deletes
* dbo.tPickliste rows with nStatus < 10 for this session, so it's safe (and
* recommended, see delivery.md §0.1) to call this unconditionally, both after
* a successful delivery and on the error path.
*/
export async function discardSession(transaction, kBenutzer, kSessionId) {
await new sql.Request(transaction)
.input('kBenutzer', sql.Int, kBenutzer)
.input('kSessionId', sql.Int, kSessionId)
.execute('Auslieferung.spPicklistenVerwerfen');
}
/**
* Mirrors SessionManager.Dispose(): removes the housekeeping row from
* dbo.tSessionId. Not strictly required for correctness (it only affects the
* Wawi GUI's "who's editing what" bookkeeping), but keeps the table tidy.
*/
export async function closeSession(transaction, kSessionId) {
await new sql.Request(transaction)
.input('kSessionId', sql.Int, kSessionId)
.query('DELETE FROM dbo.tSessionId WHERE kSessionId = @kSessionId');
}

View File

@@ -0,0 +1,38 @@
import sql from 'mssql';
/** DB-resolved (or env-overridden) outgoing warehouse, cached after first lookup. */
let cachedWarenLager = null;
/**
* Resolves the local warehouse (dbo.tWarenLager.nFulfillment = 0) to book
* stock out of. JTL's own PosWarehouseService.FindeWarenlagerFuerAusgang()
* (jtlCore, obfuscated) is not yet decompiled/traced (see delivery.md §7), so
* we approximate it: use JTL_KWARENLAGER if configured, otherwise pick the
* highest-priority active local warehouse. This is correct for the common
* single-warehouse setup; multi-warehouse setups should set JTL_KWARENLAGER
* explicitly until FindeWarenlagerFuerAusgang() is fully traced.
*/
export async function getOutgoingWarehouse(transaction) {
if (cachedWarenLager != null) {
return cachedWarenLager;
}
const configured = Number(process.env.JTL_KWARENLAGER);
if (Number.isInteger(configured) && configured > 0) {
cachedWarenLager = configured;
return cachedWarenLager;
}
const result = await new sql.Request(transaction).query(`
SELECT TOP 1 kWarenLager
FROM dbo.tWarenLager
WHERE nFulfillment = 0 AND ISNULL(nAktiv, 1) = 1
ORDER BY nAuslieferungsPrio, kWarenLager
`);
const row = result.recordset[0];
if (!row) {
throw new Error('No local warehouse (dbo.tWarenLager.nFulfillment = 0) found; set JTL_KWARENLAGER explicitly.');
}
cachedWarenLager = row.kWarenLager;
return cachedWarenLager;
}

View File

@@ -0,0 +1,38 @@
/**
* Minimal helpers for building the small, numeric-only XML fragments the
* Auslieferung.sp* procedures expect (see delivery.md §3 for the exact
* schemas). SQL Server's `xml` type happily holds multiple top-level
* elements, so a "fragment" here is just concatenated <Tag>...</Tag> blocks.
*/
function escapeXml(value) {
return String(value).replace(/[<>&'"]/g, (c) => {
switch (c) {
case '<':
return '&lt;';
case '>':
return '&gt;';
case '&':
return '&amp;';
case "'":
return '&apos;';
case '"':
return '&quot;';
default:
return c;
}
});
}
/** Builds `<tag>value</tag>`, or `<tag/>` when value is null/undefined. */
export function xmlTag(tag, value) {
if (value == null) {
return `<${tag}/>`;
}
return `<${tag}>${escapeXml(value)}</${tag}>`;
}
/** Wraps a set of child tags (already-built strings) in `<tag>...</tag>`. */
export function xmlElement(tag, childrenXml) {
return `<${tag}>${childrenXml.join('')}</${tag}>`;
}

View File

@@ -1,130 +0,0 @@
import https from 'node:https';
import { gunzipSync } from 'node:zlib';
const HOST = process.env.JTL_HOST || '127.0.0.1';
const PORT = Number(process.env.JTL_PORT) || Number(process.env.PORT) || 4443;
const INIT_QUERY =
'mandantId=1&lastChangedCategory=0&lastChangedCustomer=0&lastChangedCustomerGroup=0&lastChangedProduct=0&lastChangedConfigurationGroup=0&lastChangedConfigurationItem=0&lastChangedCompositeProduct=0&lastChangedDeletedEntity=0';
function usage() {
console.error('Usage:');
console.error(' node test-client.js <authCode> [name] # 6-digit: step1+2, 4-digit: step1 only');
console.error(' node test-client.js init <authToken>');
process.exit(1);
}
function posHeaders(authToken) {
const headers = {
accept: 'application/json',
'content-type': 'application/json',
'cache-control': 'no-cache',
version: '1.0.11.14',
system: 'JTL-POS',
charset: 'utf-8',
connection: 'Keep-Alive',
'user-agent': 'Dalvik/2.1.0 (Linux; U; Android 13; SM-T970 Build/TP1A.220624.014)',
'accept-encoding': 'gzip',
};
if (authToken) {
headers.authorization = `Bearer ${authToken}`;
}
return headers;
}
function decodeBody(buffer, headers) {
if (headers['content-encoding'] === 'gzip') {
return gunzipSync(buffer);
}
return buffer;
}
function request(path, authToken) {
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: HOST,
port: PORT,
path,
method: 'GET',
headers: posHeaders(authToken),
rejectUnauthorized: false,
},
(res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks);
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: decodeBody(raw, res.headers),
});
});
}
);
req.on('error', reject);
req.end();
});
}
function printResponse(res) {
console.log(`status: ${res.statusCode}`);
console.log('headers:', JSON.stringify(res.headers, null, 2));
console.log('body:');
try {
console.log(JSON.stringify(JSON.parse(res.body.toString('utf8')), null, 2));
} catch {
console.log(res.body.toString('utf8'));
}
}
async function clientRequest(authCode, name, label) {
const path = `/api/v1/client?authCode=${encodeURIComponent(authCode)}&name=${encodeURIComponent(name)}`;
console.log(`${label} GET https://${HOST}:${PORT}${path}\n`);
const res = await request(path);
printResponse(res);
console.log();
return res;
}
async function main() {
const mode = process.argv[2];
if (!mode) {
usage();
}
if (mode === 'init') {
const authToken = process.argv[3] || process.env.AUTH_TOKEN;
if (!authToken) {
usage();
}
const path = `/v1/init?${INIT_QUERY}`;
console.log(`GET https://${HOST}:${PORT}${path}\n`);
const res = await request(path, authToken);
printResponse(res);
return;
}
const name = process.argv[3] || process.env.CLIENT_NAME || '001';
const fullCode = mode;
if (fullCode.length === 6) {
await clientRequest(fullCode.slice(0, 4), name, '=== step 1 (4 digits) ===');
await clientRequest(fullCode, name, '=== step 2 (6 digits) ===');
return;
}
await clientRequest(fullCode, name, '=== step 1 (4 digits) ===');
}
main().catch((err) => {
console.error('request failed:', err.message);
process.exit(1);
});