delivery seemds to work.
This commit is contained in:
@@ -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
666
delivery.md
Normal 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
341
order.md
@@ -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
|
||||
@@ -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(`
|
||||
|
||||
34
src/queries/delivery/commit.js
Normal file
34
src/queries/delivery/commit.js
Normal 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;
|
||||
`);
|
||||
}
|
||||
56
src/queries/delivery/deliver.js
Normal file
56
src/queries/delivery/deliver.js
Normal 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;
|
||||
}
|
||||
45
src/queries/delivery/index.js
Normal file
45
src/queries/delivery/index.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/queries/delivery/reserve.js
Normal file
54
src/queries/delivery/reserve.js
Normal 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;
|
||||
`);
|
||||
}
|
||||
46
src/queries/delivery/session.js
Normal file
46
src/queries/delivery/session.js
Normal 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');
|
||||
}
|
||||
38
src/queries/delivery/warehouse.js
Normal file
38
src/queries/delivery/warehouse.js
Normal 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;
|
||||
}
|
||||
38
src/queries/delivery/xml.js
Normal file
38
src/queries/delivery/xml.js
Normal 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 '<';
|
||||
case '>':
|
||||
return '>';
|
||||
case '&':
|
||||
return '&';
|
||||
case "'":
|
||||
return ''';
|
||||
case '"':
|
||||
return '"';
|
||||
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}>`;
|
||||
}
|
||||
130
test-client.js
130
test-client.js
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user