close
This commit is contained in:
586
src/queries/create-order.js
Normal file
586
src/queries/create-order.js
Normal file
@@ -0,0 +1,586 @@
|
||||
import sql from 'mssql';
|
||||
import { getPool } from '../db.js';
|
||||
import { getActiveShopId, getActiveShopSubshopId } from '../shop.js';
|
||||
|
||||
const config = {
|
||||
kBenutzer: Number(process.env.JTL_KBENUTZER) || 1,
|
||||
kFirmaHistory: Number(process.env.JTL_KFIRMAHISTORY) || 0,
|
||||
kSprache: Number(process.env.JTL_KSPRACHE) || 1,
|
||||
kPlattform: Number(process.env.JTL_KPLATTFORM) || 7,
|
||||
kVersandArt: Number(process.env.JTL_KVERSANDART) || 0,
|
||||
kKundengruppe: Number(process.env.JTL_KKUNDENGRUPPE) || 0,
|
||||
// kLaufendeNummer 3 = Auftrag, 6 = Kunde in dbo.tLaufendeNummern
|
||||
orderNumberSequence: Number(process.env.JTL_ORDER_NUMBER_SEQUENCE) || 3,
|
||||
customerNumberSequence: Number(process.env.JTL_CUSTOMER_NUMBER_SEQUENCE) || 6,
|
||||
};
|
||||
|
||||
/** DB-dependent foreign keys, resolved once on first order. */
|
||||
let resolvedDefaults = null;
|
||||
|
||||
async function getDefaults() {
|
||||
if (resolvedDefaults) {
|
||||
return resolvedDefaults;
|
||||
}
|
||||
|
||||
const result = await getPool().request().input('kPlattform', sql.Int, config.kPlattform).query(`
|
||||
SELECT
|
||||
(SELECT MAX(kFirmaHistory) FROM dbo.tFirmaHistory) AS kFirmaHistory,
|
||||
(SELECT MIN(kVersandArt) FROM dbo.tVersandArt) AS kVersandArt,
|
||||
(SELECT TOP 1 kKundenGruppe FROM dbo.tKundenGruppe ORDER BY nStandard DESC, kKundenGruppe) AS kKundengruppe,
|
||||
(SELECT CASE WHEN EXISTS (SELECT 1 FROM dbo.tPlattform WHERE nPlattform = @kPlattform) THEN @kPlattform ELSE 1 END) AS kPlattform;
|
||||
`);
|
||||
const row = result.recordset[0] || {};
|
||||
|
||||
resolvedDefaults = {
|
||||
kFirmaHistory: config.kFirmaHistory || row.kFirmaHistory || 1,
|
||||
kVersandArt: config.kVersandArt || row.kVersandArt || 1,
|
||||
kKundengruppe: config.kKundengruppe || row.kKundengruppe || 1,
|
||||
kPlattform: row.kPlattform || 1,
|
||||
};
|
||||
return resolvedDefaults;
|
||||
}
|
||||
|
||||
const COUNTRY_NAMES = {
|
||||
DE: 'Deutschland',
|
||||
AT: 'Österreich',
|
||||
CH: 'Schweiz',
|
||||
};
|
||||
|
||||
function toNumber(value, fallback = 0) {
|
||||
const n = parseFloat(String(value ?? '').replace(',', '.'));
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function parseCreationDate(value) {
|
||||
if (value) {
|
||||
const d = new Date(String(value).replace(' ', 'T'));
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return new Date();
|
||||
}
|
||||
|
||||
// JTL maps VAT rates to Steuerklassen: 19% => 1 (voller Satz), 7% => 2 (ermäßigt)
|
||||
function steuerklasseForVat(vat) {
|
||||
if (vat >= 15) return 1;
|
||||
if (vat > 0) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
function isoWeek(date) {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the Admin > Nummernkreise placeholders used in tLaufendeNummern's
|
||||
* cPrefix/cSuffix columns: J = Jahr, M = Monat, T = Tag, K = Kalenderwoche.
|
||||
*/
|
||||
function formatNumberPlaceholders(template, date) {
|
||||
if (!template) return '';
|
||||
const pad2 = (n) => String(n).padStart(2, '0');
|
||||
return template
|
||||
.replace(/<J>/g, String(date.getFullYear()))
|
||||
.replace(/<M>/g, pad2(date.getMonth() + 1))
|
||||
.replace(/<T>/g, pad2(date.getDate()))
|
||||
.replace(/<K>/g, pad2(isoWeek(date)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Atomically increments nNummer for a dbo.tLaufendeNummern row and returns
|
||||
* the fully formatted number (cPrefix + nNummer + cSuffix, with placeholders
|
||||
* resolved against `date`).
|
||||
*/
|
||||
async function nextNumberFromSequence(transaction, kLaufendeNummer, date) {
|
||||
const result = await new sql.Request(transaction).input('kLaufendeNummer', sql.Int, kLaufendeNummer).query(`
|
||||
DECLARE @n INT, @cPrefix NVARCHAR(50), @cSuffix NVARCHAR(50);
|
||||
UPDATE dbo.tLaufendeNummern
|
||||
SET @n = nNummer = nNummer + 1, @cPrefix = cPrefix, @cSuffix = cSuffix
|
||||
WHERE kLaufendeNummer = @kLaufendeNummer;
|
||||
SELECT @n AS nNummer, @cPrefix AS cPrefix, @cSuffix AS cSuffix;
|
||||
`);
|
||||
const row = result.recordset[0];
|
||||
if (!row || row.nNummer == null) {
|
||||
throw new Error(`dbo.tLaufendeNummern has no row ${kLaufendeNummer}`);
|
||||
}
|
||||
const prefix = formatNumberPlaceholders(row.cPrefix, date);
|
||||
const suffix = formatNumberPlaceholders(row.cSuffix, date);
|
||||
return `${prefix}${row.nNummer}${suffix}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates the next primary key for a table via dbo.tpk
|
||||
* (JTL tables like tZahlung/tZahlungsart have no identity column).
|
||||
*/
|
||||
async function allocatePk(transaction, tableName) {
|
||||
const result = await new sql.Request(transaction).input('cName', sql.NVarChar, tableName).query(`
|
||||
DECLARE @pk INT;
|
||||
UPDATE dbo.tpk SET @pk = nummer, nummer = nummer + 1, dChanged = GETDATE() WHERE cName = @cName;
|
||||
SELECT @pk AS pk;
|
||||
`);
|
||||
const pk = result.recordset[0]?.pk;
|
||||
if (pk == null) {
|
||||
throw new Error(`dbo.tpk has no row for table '${tableName}'`);
|
||||
}
|
||||
return pk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Zahlungsart by name; creates it on the fly (including its PK
|
||||
* from dbo.tpk) when it does not exist yet.
|
||||
*/
|
||||
async function resolveZahlungsart(transaction, name, cache) {
|
||||
const key = String(name || 'Bar');
|
||||
const cached = cache.get(key.toLowerCase());
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const existing = await new sql.Request(transaction)
|
||||
.input('cName', sql.NVarChar, key)
|
||||
.query('SELECT TOP 1 kZahlungsart, cName FROM dbo.tZahlungsart WHERE cName = @cName');
|
||||
|
||||
let zahlungsart;
|
||||
if (existing.recordset[0]) {
|
||||
zahlungsart = {
|
||||
kZahlungsart: existing.recordset[0].kZahlungsart,
|
||||
cName: existing.recordset[0].cName,
|
||||
};
|
||||
} else {
|
||||
const kZahlungsart = await allocatePk(transaction, 'tZahlungsart');
|
||||
await new sql.Request(transaction)
|
||||
.input('kZahlungsart', sql.Int, kZahlungsart)
|
||||
.input('cName', sql.NVarChar, key)
|
||||
.query(`
|
||||
INSERT INTO dbo.tZahlungsart
|
||||
(kZahlungsart, cName, cPrtString, nLastschrift, cPrtStringVor, cPaymentOption, cKonto,
|
||||
nAusliefernVorZahlung, nPrioritaet, nMahnwesenAktiv, fSkontoWert, nSkontoZeitraum,
|
||||
nMatchingOptionen, nIstStandard, nAktiv)
|
||||
VALUES (@kZahlungsart, @cName, '', 0, '', '', '', 0, 0, 0, 0, 0, 0, 0, 1)
|
||||
`);
|
||||
zahlungsart = { kZahlungsart, cName: key };
|
||||
}
|
||||
|
||||
cache.set(key.toLowerCase(), zahlungsart);
|
||||
return zahlungsart;
|
||||
}
|
||||
|
||||
async function nextCustomerNumber(transaction) {
|
||||
return nextNumberFromSequence(transaction, config.customerNumberSequence, new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Kunde via Kunde.spKundeInsert (which fills tKunde, tAdresse and
|
||||
* the search tables). Columns of the table type are named explicitly because
|
||||
* their order differs between JTL versions.
|
||||
*/
|
||||
async function createCustomer(transaction, { customerNumber, address, isPosCustomer, defaults }) {
|
||||
const a = address || {};
|
||||
const iso = (a.countryIso || 'DE').toUpperCase();
|
||||
const kKundengruppe = Number(a.customerGroupId) || defaults.kKundengruppe;
|
||||
|
||||
const result = await new sql.Request(transaction)
|
||||
.input('cKundenNr', sql.NVarChar, customerNumber)
|
||||
.input('cFirma', sql.NVarChar, a.company || '')
|
||||
.input('cAnrede', sql.NVarChar, a.salutation || '')
|
||||
.input('cTitel', sql.NVarChar, a.title || '')
|
||||
.input('cVorname', sql.NVarChar, a.firstName || '')
|
||||
.input('cName', sql.NVarChar, a.lastName || 'Laufkunde')
|
||||
.input('cStrasse', sql.NVarChar, a.street || '-')
|
||||
.input('cPLZ', sql.NVarChar, a.zipCode || '')
|
||||
.input('cOrt', sql.NVarChar, a.city || '-')
|
||||
.input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso)
|
||||
.input('cTel', sql.NVarChar, a.phone || '')
|
||||
.input('cFax', sql.NVarChar, a.fax || '')
|
||||
.input('cEMail', sql.NVarChar, a.email || '')
|
||||
.input('cMobil', sql.NVarChar, a.mobile || '')
|
||||
.input('fRabatt', sql.Decimal(18, 13), toNumber(a.discount, 0))
|
||||
.input('cAdressZusatz', sql.NVarChar, a.addressAddition || '')
|
||||
.input('cGeburtstag', sql.NVarChar, a.birthday || '')
|
||||
.input('kKundenGruppe', sql.Int, kKundengruppe)
|
||||
.input('kSprache', sql.Int, config.kSprache)
|
||||
.input('cISO', sql.NVarChar, iso)
|
||||
.input('cBundesland', sql.NVarChar, a.state || '')
|
||||
.input('cHerkunft', sql.NVarChar, isPosCustomer ? 'Kasse' : 'POS-API')
|
||||
.input('cKassenKunde', sql.Char(1), isPosCustomer ? 'Y' : 'N')
|
||||
.input('nDebitorennr', sql.Int, Number(a.debtorNumber) || 0)
|
||||
.query(`
|
||||
DECLARE @returnValue INT;
|
||||
DECLARE @p1 dbo.TYPE_spkundeInsert;
|
||||
INSERT INTO @p1
|
||||
(kInetKunde, kKundenKategorie, cKundenNr, cFirma, cAnrede, cTitel, cVorname, cName,
|
||||
cStrasse, cPLZ, cOrt, cLand, cTel, cFax, cEMail, dErstellt, cMobil, fRabatt, cUSTID, cNewsletter,
|
||||
cZusatz, cEbayName, kBuyer, cAdressZusatz, cGeburtstag, cWWW, cSperre, cPostID, kKundenGruppe,
|
||||
nZahlungsziel, kSprache, cISO, cBundesland, cHerkunft, cKassenKunde, cHRNr, kZahlungsart,
|
||||
nDebitorennr, cSteuerNr, nKreditlimit, kKundenDrucktext, nMahnstopp, nMahnrhythmus, kFirma,
|
||||
fProvision, nVertreter, fSkonto, nSkontoInTagen)
|
||||
VALUES
|
||||
(0, 0, @cKundenNr, @cFirma, @cAnrede, @cTitel, @cVorname, @cName,
|
||||
@cStrasse, @cPLZ, @cOrt, @cLand, @cTel, @cFax, @cEMail, GETDATE(), @cMobil, @fRabatt, NULL, 'N',
|
||||
N'', N'', 0, @cAdressZusatz, @cGeburtstag, N'', 'N', NULL, @kKundenGruppe,
|
||||
0, @kSprache, @cISO, @cBundesland, @cHerkunft, @cKassenKunde, N'', 0,
|
||||
@nDebitorennr, N'', 0, 0, 0, 0, 0,
|
||||
NULL, 0, 0, 0);
|
||||
EXEC @returnValue = Kunde.spKundeInsert @daten = @p1;
|
||||
SELECT @returnValue AS kKunde;
|
||||
`);
|
||||
|
||||
const kKunde = result.recordset[0]?.kKunde;
|
||||
if (!kKunde || kKunde <= 0) {
|
||||
throw new Error(`Kunde.spKundeInsert failed for '${customerNumber}'`);
|
||||
}
|
||||
return { kKunde, kKundengruppe };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the customer by cKundenNr; creates the Kunde on the fly when it
|
||||
* does not exist. Orders without a customer number use (or create) the
|
||||
* Kassenkunde (walk-in customer).
|
||||
*/
|
||||
async function resolveCustomer(transaction, order, defaults) {
|
||||
const customerNumber = String(order.customerNumber || '').trim();
|
||||
|
||||
if (customerNumber) {
|
||||
const result = await new sql.Request(transaction)
|
||||
.input('cKundenNr', sql.NVarChar, customerNumber)
|
||||
.query('SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKundenNr = @cKundenNr');
|
||||
if (result.recordset[0]) {
|
||||
return {
|
||||
kKunde: result.recordset[0].kKunde,
|
||||
kKundengruppe: result.recordset[0].kKundenGruppe || defaults.kKundengruppe,
|
||||
};
|
||||
}
|
||||
return createCustomer(transaction, {
|
||||
customerNumber,
|
||||
address: order.billingAddress,
|
||||
isPosCustomer: false,
|
||||
defaults,
|
||||
});
|
||||
}
|
||||
|
||||
const kassenKunde = await new sql.Request(transaction).query(
|
||||
"SELECT TOP 1 kKunde, kKundenGruppe FROM dbo.tKunde WHERE cKassenKunde = 'Y' ORDER BY kKunde"
|
||||
);
|
||||
if (kassenKunde.recordset[0]) {
|
||||
return {
|
||||
kKunde: kassenKunde.recordset[0].kKunde,
|
||||
kKundengruppe: kassenKunde.recordset[0].kKundenGruppe || defaults.kKundengruppe,
|
||||
};
|
||||
}
|
||||
|
||||
return createCustomer(transaction, {
|
||||
customerNumber: await nextCustomerNumber(transaction),
|
||||
address: order.billingAddress,
|
||||
isPosCustomer: true,
|
||||
defaults,
|
||||
});
|
||||
}
|
||||
|
||||
async function nextOrderNumber(transaction, orderDate) {
|
||||
return nextNumberFromSequence(transaction, config.orderNumberSequence, orderDate);
|
||||
}
|
||||
|
||||
async function insertOrderAddress(transaction, kAuftrag, kKunde, address, nTyp) {
|
||||
const a = address || {};
|
||||
const iso = (a.countryIso || 'DE').toUpperCase();
|
||||
await new sql.Request(transaction)
|
||||
.input('kAuftrag', sql.Int, kAuftrag)
|
||||
.input('kKunde', sql.Int, kKunde)
|
||||
.input('cFirma', sql.NVarChar, a.company || '')
|
||||
.input('cAnrede', sql.NVarChar, a.salutation || '')
|
||||
.input('cTitel', sql.NVarChar, a.title || '')
|
||||
.input('cVorname', sql.NVarChar, a.firstName || '')
|
||||
.input('cName', sql.NVarChar, a.lastName || '-')
|
||||
.input('cStrasse', sql.NVarChar, a.street || '-')
|
||||
.input('cPLZ', sql.NVarChar, a.zipCode || '')
|
||||
.input('cOrt', sql.NVarChar, a.city || '-')
|
||||
.input('cLand', sql.NVarChar, COUNTRY_NAMES[iso] || iso)
|
||||
.input('cTel', sql.NVarChar, a.phone || '')
|
||||
.input('cZusatz', sql.NVarChar, a.extraAddressLine || '')
|
||||
.input('cAdressZusatz', sql.NVarChar, a.addressAddition || '')
|
||||
.input('cMobil', sql.NVarChar, a.mobile || '')
|
||||
.input('cMail', sql.NVarChar, a.email || '')
|
||||
.input('cFax', sql.NVarChar, a.fax || '')
|
||||
.input('cBundesland', sql.NVarChar, a.state || '')
|
||||
.input('cISO', sql.NVarChar, iso)
|
||||
.input('nTyp', sql.Int, nTyp)
|
||||
.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, @cFirma, @cAnrede, @cTitel, @cVorname, @cName, @cStrasse, @cPLZ, @cOrt, @cLand,
|
||||
@cTel, @cZusatz, @cAdressZusatz, @cMobil, @cMail, @cFax, @cBundesland, @cISO, @nTyp, 0)
|
||||
`);
|
||||
}
|
||||
|
||||
async function insertOrderItem(transaction, kAuftrag, item) {
|
||||
const vat = toNumber(item.vat, 19);
|
||||
const quantity = toNumber(item.quantity, 1);
|
||||
const priceNet = toNumber(item.priceNet, toNumber(item.priceGross) / (1 + vat / 100));
|
||||
const discount = toNumber(item.discountPercent, 0);
|
||||
const kSteuerklasse = steuerklasseForVat(vat);
|
||||
const sku = String(item.sku || '').trim();
|
||||
|
||||
let kArtikel = null;
|
||||
if (sku) {
|
||||
const result = await new sql.Request(transaction)
|
||||
.input('cArtNr', sql.NVarChar, sku)
|
||||
.query('SELECT TOP 1 kArtikel FROM dbo.tArtikel WHERE cArtNr = @cArtNr');
|
||||
kArtikel = result.recordset[0]?.kArtikel ?? null;
|
||||
}
|
||||
|
||||
const result = await new sql.Request(transaction)
|
||||
.input('kArtikel', sql.Int, kArtikel)
|
||||
.input('kAuftrag', sql.Int, kAuftrag)
|
||||
.input('cArtNr', sql.NVarChar, kArtikel ? sku : null)
|
||||
.input('cName', sql.NVarChar, item.name || sku || 'Position')
|
||||
.input('cHinweis', sql.NVarChar, item.note || '')
|
||||
.input('fAnzahl', sql.Float, quantity)
|
||||
.input('fVkNetto', sql.Float, priceNet)
|
||||
.input('fMwSt', sql.Float, vat)
|
||||
.input('kSteuerklasse', sql.Int, kSteuerklasse)
|
||||
.input('nType', sql.Int, kArtikel ? 1 : 0)
|
||||
.input('cEinheit', sql.NVarChar, item.unit || '')
|
||||
.input('fRabatt', sql.Float, discount)
|
||||
.query(`
|
||||
DECLARE @t TABLE ([kAuftragPosition] INT);
|
||||
INSERT INTO Verkauf.tAuftragPosition
|
||||
(kArtikel, kAuftrag, cArtNr, nReserviert, cName, cHinweis, fAnzahl, fVkNetto, fMwSt,
|
||||
cNameStandard, kSteuerklasse, nType, cEinheit, fFaktor, kSteuerschluessel, fRabatt)
|
||||
OUTPUT inserted.kAuftragPosition INTO @t
|
||||
VALUES (@kArtikel, @kAuftrag, @cArtNr, 1, @cName, @cHinweis, @fAnzahl, @fVkNetto, @fMwSt,
|
||||
@cName, @kSteuerklasse, @nType, @cEinheit, 1.0, 3, @fRabatt);
|
||||
SELECT kAuftragPosition FROM @t;
|
||||
`);
|
||||
return result.recordset[0]?.kAuftragPosition ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Links a JTL-Wawi order (and its positions) back to the originating
|
||||
* POS order/position ids via Pos.tAuftragMapping / Pos.tAuftragPositionMapping,
|
||||
* so the POS system can recognise orders it already pushed into JTL-Wawi.
|
||||
*/
|
||||
async function insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag) {
|
||||
const kShopSubShop = getActiveShopSubshopId();
|
||||
if (!Number.isInteger(kPosAuftrag) || !kShopSubShop) {
|
||||
return;
|
||||
}
|
||||
await new sql.Request(transaction)
|
||||
.input('kAuftrag', sql.Int, kAuftrag)
|
||||
.input('kPosAuftrag', sql.Int, kPosAuftrag)
|
||||
.input('kShopSubShop', sql.Int, kShopSubShop)
|
||||
.query(`
|
||||
INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop)
|
||||
VALUES (@kAuftrag, @kPosAuftrag, @kShopSubShop)
|
||||
`);
|
||||
}
|
||||
|
||||
async function insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition) {
|
||||
const kShopSubShop = getActiveShopSubshopId();
|
||||
if (!Number.isInteger(kAuftragPosition) || !Number.isInteger(kPosAuftragPosition) || !kShopSubShop) {
|
||||
return;
|
||||
}
|
||||
await new sql.Request(transaction)
|
||||
.input('kAuftragPosition', sql.Int, kAuftragPosition)
|
||||
.input('kPosAuftragPosition', sql.Int, kPosAuftragPosition)
|
||||
.input('kShopSubShop', sql.Int, kShopSubShop)
|
||||
.query(`
|
||||
INSERT INTO Pos.tAuftragPositionMapping (kAuftragPosition, kPosAuftragPosition, kShopSubShop)
|
||||
VALUES (@kAuftragPosition, @kPosAuftragPosition, @kShopSubShop)
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors JTL's own PosOrderCreationService -> DeliveryStep.Run(): after a POS
|
||||
* 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.
|
||||
*/
|
||||
function isOrderDelivered(order) {
|
||||
const deliver = order.settings?.deliver;
|
||||
if (deliver === undefined || deliver === null) {
|
||||
return true;
|
||||
}
|
||||
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');
|
||||
}
|
||||
|
||||
async function setVersanddatum(transaction, kAuftrag, orderDate) {
|
||||
await new sql.Request(transaction)
|
||||
.input('kBestellung', sql.Int, kAuftrag)
|
||||
.input('dVersanddatum', sql.DateTime, orderDate)
|
||||
.query(`
|
||||
MERGE dbo.tVersandInfo AS target
|
||||
USING (SELECT @kBestellung AS kBestellung) AS src
|
||||
ON target.kBestellung = src.kBestellung
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET dVersanddatum = @dVersanddatum
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (kBestellung, dVersanddatum, nAnzahlVersendetePakete, nAnzahlPakete)
|
||||
VALUES (@kBestellung, @dVersanddatum, 0, 0);
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers the full ordered quantity of every position (no partial delivery),
|
||||
* mirroring DeliveryStep's non-voucher path.
|
||||
*/
|
||||
async function deliverOrder(transaction, kAuftrag, orderDate, deliveredItems) {
|
||||
if (!deliveredItems.length) {
|
||||
return;
|
||||
}
|
||||
const kLieferschein = await createLieferschein(transaction, kAuftrag);
|
||||
for (const { kAuftragPosition, quantity } of deliveredItems) {
|
||||
await createLieferscheinPos(transaction, kLieferschein, kAuftragPosition, quantity);
|
||||
}
|
||||
if (orderDate < new Date(new Date().toDateString())) {
|
||||
await setVersanddatum(transaction, kAuftrag, orderDate);
|
||||
}
|
||||
}
|
||||
|
||||
async function insertPayment(transaction, kAuftrag, payment, order, zahlungsartCache) {
|
||||
const zahlungsart = await resolveZahlungsart(transaction, payment.paymentMethodName || order.paymentMethodName, zahlungsartCache);
|
||||
const kZahlung = await allocatePk(transaction, 'tZahlung');
|
||||
|
||||
await new sql.Request(transaction)
|
||||
.input('kZahlung', sql.Int, kZahlung)
|
||||
.input('cName', sql.NVarChar, zahlungsart.cName)
|
||||
.input('dDatum', sql.DateTime, parseCreationDate(order.creationDate))
|
||||
.input('fBetrag', sql.Float, toNumber(payment.amount, 0))
|
||||
.input('kBestellung', sql.Int, kAuftrag)
|
||||
.input('kBenutzer', sql.Int, config.kBenutzer)
|
||||
.input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
|
||||
.input('cExternalTransactionId', sql.NVarChar, order.externalOrderNumber || '')
|
||||
.query(`
|
||||
INSERT INTO dbo.tZahlung
|
||||
(kZahlung, cName, dDatum, fBetrag, kBestellung, kBenutzer, nAnzahlung, cHinweis, kZahlungsart,
|
||||
nKeinExport, cExternalTransactionId, nZuweisungstyp, nZahlungstyp, cZuweisungsinfo, nZuweisungswertung)
|
||||
VALUES (@kZahlung, @cName, @dDatum, @fBetrag, @kBestellung, @kBenutzer, 0, '', @kZahlungsart,
|
||||
0, @cExternalTransactionId, 0, 0, '', 0)
|
||||
`);
|
||||
}
|
||||
|
||||
export async function createOrder(order) {
|
||||
const defaults = await getDefaults();
|
||||
const transaction = new sql.Transaction(getPool());
|
||||
await transaction.begin(sql.ISOLATION_LEVEL.READ_COMMITTED);
|
||||
|
||||
try {
|
||||
const zahlungsartCache = new Map();
|
||||
|
||||
const orderDate = parseCreationDate(order.creationDate);
|
||||
const { kKunde, kKundengruppe } = await resolveCustomer(transaction, order, defaults);
|
||||
const zahlungsart = await resolveZahlungsart(transaction, order.paymentMethodName, zahlungsartCache);
|
||||
const cAuftragsNr = await nextOrderNumber(transaction, orderDate);
|
||||
|
||||
const resultAuftrag = await new sql.Request(transaction)
|
||||
.input('cAuftragsNr', sql.NVarChar, cAuftragsNr)
|
||||
.input('dErstellt', sql.DateTime, orderDate)
|
||||
.input('kBenutzer', sql.Int, config.kBenutzer)
|
||||
.input('kKunde', sql.Int, kKunde)
|
||||
.input('kFirmaHistory', sql.Int, defaults.kFirmaHistory)
|
||||
.input('kSprache', sql.Int, config.kSprache)
|
||||
.input('cWaehrung', sql.NVarChar, order.currencyIso || 'EUR')
|
||||
.input('kPlattform', sql.Int, defaults.kPlattform)
|
||||
.input('kShop', sql.Int, getActiveShopId() || null)
|
||||
.input('cKundenNr', sql.NVarChar, String(order.customerNumber || ''))
|
||||
.input('cVersandlandISO', sql.NVarChar, (order.shippingAddress?.countryIso || 'DE').toUpperCase())
|
||||
.input('kVersandArt', sql.Int, defaults.kVersandArt)
|
||||
.input('kZahlungsart', sql.Int, zahlungsart.kZahlungsart)
|
||||
.input('kKundengruppe', sql.Int, kKundengruppe)
|
||||
.input('cExterneAuftragsnummer', sql.NVarChar, order.externalOrderNumber || '')
|
||||
.query(`
|
||||
DECLARE @t TABLE ([kAuftrag] INT);
|
||||
INSERT INTO Verkauf.tAuftrag
|
||||
(cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor,
|
||||
kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten,
|
||||
cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe,
|
||||
cExterneAuftragsnummer)
|
||||
OUTPUT inserted.kAuftrag INTO @t
|
||||
VALUES (@cAuftragsNr, @dErstellt, 0, @kBenutzer, @kKunde, @kBenutzer, 1, 1.0,
|
||||
@kFirmaHistory, @kSprache, @cWaehrung, 1.0, 0.0,
|
||||
@cWaehrung, @kPlattform, @kShop, @cKundenNr, @cVersandlandISO, @kVersandArt, @kZahlungsart, @kKundengruppe,
|
||||
@cExterneAuftragsnummer);
|
||||
SELECT kAuftrag FROM @t;
|
||||
`);
|
||||
|
||||
const kAuftrag = resultAuftrag.recordset[0].kAuftrag;
|
||||
|
||||
const kPosAuftrag = Number.parseInt(order.externalId, 10);
|
||||
await insertPosOrderMapping(transaction, kAuftrag, kPosAuftrag);
|
||||
|
||||
await insertOrderAddress(transaction, kAuftrag, kKunde, order.shippingAddress, 0);
|
||||
await insertOrderAddress(transaction, kAuftrag, kKunde, order.billingAddress, 1);
|
||||
|
||||
const deliveredItems = [];
|
||||
for (const item of order.orderItems || []) {
|
||||
const kAuftragPosition = await insertOrderItem(transaction, kAuftrag, item);
|
||||
const kPosAuftragPosition = Number.parseInt(item.externalId, 10);
|
||||
await insertPosOrderPositionMapping(transaction, kAuftragPosition, kPosAuftragPosition);
|
||||
if (kAuftragPosition != null) {
|
||||
deliveredItems.push({ kAuftragPosition, quantity: toNumber(item.quantity, 1) });
|
||||
}
|
||||
}
|
||||
|
||||
for (const payment of order.payments || []) {
|
||||
await insertPayment(transaction, kAuftrag, payment, order, zahlungsartCache);
|
||||
}
|
||||
|
||||
if (isOrderDelivered(order)) {
|
||||
await deliverOrder(transaction, kAuftrag, orderDate, deliveredItems);
|
||||
}
|
||||
|
||||
await new sql.Request(transaction).input('kAuftrag', sql.Int, kAuftrag).query(`
|
||||
DECLARE @p1 Verkauf.TYPE_spAuftragEckdatenBerechnen;
|
||||
INSERT INTO @p1 VALUES (@kAuftrag);
|
||||
EXEC Verkauf.spAuftragEckdatenBerechnen @auftrag = @p1;
|
||||
`);
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
return { orderId: String(kAuftrag), orderNumber: cAuftragsNr };
|
||||
} catch (err) {
|
||||
try {
|
||||
await transaction.rollback();
|
||||
} catch {
|
||||
// transaction may already be aborted by the server
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user