close
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { sendJson } from '../http.js';
|
import { sendJson } from '../http.js';
|
||||||
|
import { logger } from '../logger.js';
|
||||||
import { logOrder } from '../order-log.js';
|
import { logOrder } from '../order-log.js';
|
||||||
|
import { createOrder } from '../queries/create-order.js';
|
||||||
|
|
||||||
export const method = 'POST';
|
export const method = 'POST';
|
||||||
export const path = '/v1/order';
|
export const path = '/v1/order';
|
||||||
@@ -33,16 +35,29 @@ export async function handle(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const orders = getOrders(body);
|
const orders = getOrders(body);
|
||||||
const results = orders.map((order) => {
|
const results = [];
|
||||||
|
|
||||||
|
for (const order of orders) {
|
||||||
logOrder(order);
|
logOrder(order);
|
||||||
const externalOrderId = String(order?.externalId ?? '');
|
const externalOrderId = String(order?.externalId ?? '');
|
||||||
|
|
||||||
return {
|
try {
|
||||||
status: 'OK',
|
const created = await createOrder(order);
|
||||||
externalOrderId,
|
logger.success(`order ${created.orderNumber} (kAuftrag=${created.orderId}) created for externalId=${externalOrderId}`);
|
||||||
message: '',
|
results.push({
|
||||||
};
|
status: 'OK',
|
||||||
});
|
externalOrderId,
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`order externalId=${externalOrderId} failed: ${err.message}`);
|
||||||
|
results.push({
|
||||||
|
status: 'ERROR',
|
||||||
|
externalOrderId,
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sendJson(res, 200, results);
|
return sendJson(res, 200, results);
|
||||||
}
|
}
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/shop.js
12
src/shop.js
@@ -1,6 +1,7 @@
|
|||||||
import sql from 'mssql';
|
import sql from 'mssql';
|
||||||
|
|
||||||
let activeShopId = 0;
|
let activeShopId = 0;
|
||||||
|
let activeShopSubshopId = 0;
|
||||||
|
|
||||||
export function setActiveShop(id) {
|
export function setActiveShop(id) {
|
||||||
activeShopId = id;
|
activeShopId = id;
|
||||||
@@ -10,11 +11,20 @@ export function getActiveShopId() {
|
|||||||
return activeShopId;
|
return activeShopId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setActiveShopSubshop(id) {
|
||||||
|
activeShopSubshopId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveShopSubshopId() {
|
||||||
|
return activeShopSubshopId;
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchActiveShop(pool) {
|
export async function fetchActiveShop(pool) {
|
||||||
const result = await pool.request().query(`
|
const result = await pool.request().query(`
|
||||||
SELECT TOP 1 kShop FROM dbo.tShopSubshop WHERE nGesperrt = 0 ORDER BY kShop
|
SELECT TOP 1 kShop, kShopSubshop FROM dbo.tShopSubshop WHERE nGesperrt = 0 ORDER BY kShop
|
||||||
`);
|
`);
|
||||||
const id = result.recordset[0]?.kShop ?? 0;
|
const id = result.recordset[0]?.kShop ?? 0;
|
||||||
setActiveShop(id);
|
setActiveShop(id);
|
||||||
|
setActiveShopSubshop(result.recordset[0]?.kShopSubshop ?? 0);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user