This commit is contained in:
seb
2026-07-15 01:25:38 +02:00
parent 7fa839da4d
commit 056c9e18cd
21 changed files with 1181 additions and 64 deletions

View File

@@ -14,9 +14,11 @@ OdbcPool& get_pool() { return g_pool; }
OdbcPool::~OdbcPool() { disconnect(); }
static void odbc_log_diag(SQLSMALLINT handle_type, SQLHANDLE handle, const char* ctx) {
static void odbc_log_diag(SQLSMALLINT handle_type, SQLHANDLE handle, const char* ctx,
std::string* capture = nullptr) {
if (!handle) {
logc::warn("ODBC %s failed (no handle)", ctx);
if (capture && capture->empty()) *capture = std::string(ctx) + ": no handle";
return;
}
SQLSMALLINT rec = 0;
@@ -30,9 +32,14 @@ static void odbc_log_diag(SQLSMALLINT handle_type, SQLHANDLE handle, const char*
if (diag_rc != SQL_SUCCESS && diag_rc != SQL_SUCCESS_WITH_INFO) break;
any = true;
logc::warn("ODBC %s: %s - %s (%d)", ctx, state, msg, (int)native);
if (capture && capture->empty()) {
*capture = std::string(reinterpret_cast<char*>(state)) + " - "
+ std::string(reinterpret_cast<char*>(msg));
}
}
if (!any) {
logc::warn("ODBC %s failed (no diag)", ctx);
if (capture && capture->empty()) *capture = std::string(ctx) + ": no diagnostic";
}
}
@@ -287,28 +294,34 @@ static bool fetch_results(SQLHSTMT hstmt, ResultSet& out) {
bool OdbcPool::execute(Connection* c, const std::string& sql, const std::vector<Param>& params, ResultSet& out) {
if (!c) {
last_error_ = "no connection";
logc::warn("ODBC execute: no connection");
return false;
}
SQLRETURN rc = SQLPrepare(c->hstmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO) {
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "prepare");
last_error_.clear();
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "prepare", &last_error_);
return false;
}
std::vector<SQLLEN> indicators;
if (!params.empty() && !bind_params(c->hstmt, params, indicators)) {
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "bind");
last_error_.clear();
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "bind", &last_error_);
return false;
}
rc = SQLExecute(c->hstmt);
if (rc != SQL_SUCCESS && rc != SQL_SUCCESS_WITH_INFO && rc != SQL_NO_DATA) {
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "execute");
last_error_.clear();
odbc_log_diag(SQL_HANDLE_STMT, c->hstmt, "execute", &last_error_);
return false;
}
last_error_.clear();
fetch_results(c->hstmt, out);
SQLFreeStmt(c->hstmt, SQL_UNBIND);

View File

@@ -85,10 +85,13 @@ public:
void release(Connection* c);
const std::string& last_error() const { return last_error_; }
private:
SQLHENV henv_ = SQL_NULL_HENV;
std::vector<Connection> conns_;
std::mutex mutex_;
std::string last_error_;
Connection* checkout_raw();
};

View File

@@ -36,16 +36,30 @@ void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
try {
nlohmann::json created = order::create_order(order);
logc::success("order %s (kAuftrag=%s) created for externalId=%s",
created.value("orderNumber", "").c_str(),
created.value("orderId", "").c_str(),
externalOrderId.c_str());
results.push_back({
{"status", "OK"},
{"externalOrderId", externalOrderId},
{"message", ""}
});
++successful;
if (created.value("alreadyExists", false)) {
std::string msg = "order already mapped, skipped save (kAuftrag="
+ created.value("orderId", "") + ", "
+ created.value("orderNumber", "") + ")";
logc::error("order externalId=%s skipped: %s",
externalOrderId.c_str(), msg.c_str());
results.push_back({
{"status", "ERROR"},
{"externalOrderId", externalOrderId},
{"message", msg}
});
++failed;
} else {
logc::success("order %s (kAuftrag=%s) created for externalId=%s",
created.value("orderNumber", "").c_str(),
created.value("orderId", "").c_str(),
externalOrderId.c_str());
results.push_back({
{"status", "OK"},
{"externalOrderId", externalOrderId},
{"message", ""}
});
++successful;
}
} catch (const std::exception& ex) {
logc::error("order externalId=%s failed: %s", externalOrderId.c_str(), ex.what());
results.push_back({
@@ -57,7 +71,8 @@ void handle_order(HttpRequest& req, HttpResponse& resp, RouteContext& /*ctx*/) {
}
}
resp.send_json(200, results);
int http_status = (failed > 0) ? 500 : 200;
resp.send_json(http_status, results);
if (orders.empty()) {
logc::info("POST /v1/order: no orders in body");

View File

@@ -25,7 +25,10 @@ inline void commit_picklists(OdbcPool::Connection* c, int kBenutzer, int kSessio
};
ResultSet rs;
if (!get_pool().execute(c, sql, ps, rs)) {
throw std::runtime_error("commit_picklists failed");
const std::string& detail = get_pool().last_error();
throw std::runtime_error(detail.empty()
? "commit_picklists failed"
: "commit_picklists failed: " + detail);
}
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include <algorithm>
#include <cctype>
#include <cmath>
#include <ctime>
#include <chrono>
@@ -297,6 +298,26 @@ inline bool is_walk_in_order(const nlohmann::json& order) {
&& billing.value("firstName", "").empty();
}
inline std::string resolve_auftrag_c_kunden_nr(const nlohmann::json& order) {
if (is_walk_in_order(order)) return "0";
return order.value("customerNumber", "");
}
// POS settings.invoiceSetting: 0 = external/POS invoice (nIstExterneRechnung=1).
inline int resolve_n_ist_externe_rechnung(const nlohmann::json& order) {
if (!order.contains("settings") || order["settings"].is_null()) return 1;
const auto& settings = order["settings"];
if (!settings.contains("invoiceSetting")) return 1;
const auto& inv = settings["invoiceSetting"];
if (inv.is_boolean()) return inv.get<bool>() ? 0 : 1;
if (inv.is_number_integer()) return inv.get<int>() == 1 ? 0 : 1;
if (inv.is_string()) {
std::string s = inv.get<std::string>();
return (s == "1" || s == "true") ? 0 : 1;
}
return 1;
}
inline std::pair<int,int> create_customer(OdbcPool::Connection* c, const std::string& customer_number,
const nlohmann::json& address, const Defaults& defaults) {
const nlohmann::json a = address.is_null() ? nlohmann::json::object() : address;
@@ -484,14 +505,27 @@ inline int parse_pos_auftrag_id(const std::string& external_id) {
try { return std::stoi(external_id); } catch (...) { return 0; }
}
inline bool external_order_numbers_match(const std::string& mapped, const std::string& incoming) {
if (incoming.empty()) return true;
if (mapped.size() != incoming.size()) return false;
for (size_t i = 0; i < mapped.size(); ++i) {
if (std::tolower(static_cast<unsigned char>(mapped[i]))
!= std::tolower(static_cast<unsigned char>(incoming[i]))) {
return false;
}
}
return true;
}
// PosOrderCreationService.CheckIfOrderExists — already-imported POS order.
inline std::optional<std::pair<int, std::string>> find_existing_pos_order(
OdbcPool::Connection* c, int kPosAuftrag) {
OdbcPool::Connection* c, int kPosAuftrag, const std::string& external_order_number) {
int kShopSubShop = get_active_shop_subshop_id();
if (kPosAuftrag <= 0 || kShopSubShop <= 0) return std::nullopt;
const char* sql =
"SELECT TOP 1 m.kAuftrag, a.cAuftragsNr FROM Pos.tAuftragMapping m "
"SELECT TOP 1 m.kAuftrag, a.cAuftragsNr, ISNULL(a.cExterneAuftragsnummer, '') AS cExterneAuftragsnummer "
"FROM Pos.tAuftragMapping m "
"LEFT JOIN Verkauf.tAuftrag a ON a.kAuftrag = m.kAuftrag "
"WHERE m.kPosAuftrag = ? AND m.kShopSubShop = ? AND m.kAuftrag IS NOT NULL "
"ORDER BY m.kAuftrag DESC";
@@ -509,6 +543,10 @@ inline std::optional<std::pair<int, std::string>> find_existing_pos_order(
int kAuftrag = std::stoi(rs[0][0].str);
if (kAuftrag <= 0) return std::nullopt;
std::string order_number = rs[0].size() > 1 ? rs[0][1].str : "";
std::string mapped_external = rs[0].size() > 2 ? rs[0][2].str : "";
if (!external_order_numbers_match(mapped_external, external_order_number)) {
return std::nullopt;
}
return std::make_pair(kAuftrag, order_number);
}
@@ -516,22 +554,23 @@ inline void upsert_pos_order_mapping(OdbcPool::Connection* c, int kAuftrag, int
int kShopSubShop = get_active_shop_subshop_id();
if (kPosAuftrag <= 0 || kShopSubShop <= 0) return;
const char* sql =
"MERGE INTO Pos.tAuftragMapping WITH (HOLDLOCK) AS Target "
"USING (SELECT ? AS kAuftrag, ? AS kPosAuftrag, ? AS kShopSubShop) AS Source "
"ON Target.kPosAuftrag = Source.kPosAuftrag AND Target.kShopSubShop = Source.kShopSubShop "
"WHEN MATCHED THEN UPDATE SET Target.kAuftrag = Source.kAuftrag "
"WHEN NOT MATCHED BY TARGET THEN "
"INSERT (kAuftrag, kPosAuftrag, kShopSubShop) "
"VALUES (Source.kAuftrag, Source.kPosAuftrag, Source.kShopSubShop)";
ResultSet rs;
std::vector<Param> del_ps = {
{ParamType::Int, "", kPosAuftrag},
{ParamType::Int, "", kShopSubShop},
};
get_pool().execute(c,
"DELETE FROM Pos.tAuftragMapping WHERE kPosAuftrag = ? AND kShopSubShop = ?",
del_ps, rs);
std::vector<Param> ps = {
std::vector<Param> ins_ps = {
{ParamType::Int, "", kAuftrag},
{ParamType::Int, "", kPosAuftrag},
{ParamType::Int, "", kShopSubShop}
{ParamType::Int, "", kShopSubShop},
};
ResultSet rs;
get_pool().execute(c, sql, ps, rs);
get_pool().execute(c,
"INSERT INTO Pos.tAuftragMapping (kAuftrag, kPosAuftrag, kShopSubShop) VALUES (?, ?, ?)",
ins_ps, rs);
}
inline void insert_pos_order_position_mapping(OdbcPool::Connection* c, int kAuftragPosition,
@@ -606,11 +645,13 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
auto* c = guard.get();
int kPosAuftrag = parse_pos_auftrag_id(order.value("externalId", ""));
std::string external_order_number = order.value("externalOrderNumber", "");
if (kPosAuftrag > 0) {
if (auto existing = find_existing_pos_order(c, kPosAuftrag)) {
if (auto existing = find_existing_pos_order(c, kPosAuftrag, external_order_number)) {
return {
{"orderId", std::to_string(existing->first)},
{"orderNumber", existing->second}
{"orderNumber", existing->second},
{"alreadyExists", true}
};
}
}
@@ -645,12 +686,12 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
" (cAuftragsNr, dErstellt, nKomplettAusgeliefert, kBenutzer, kKunde, kBenutzerErstellt, nType, fFaktor,"
" kFirmaHistory, kSprache, cVersandlandWaehrung, fVersandlandWaehrungFaktor, fFinanzierungskosten,"
" cWaehrung, kPlattform, kShop, cKundenNr, cVersandlandISO, kVersandArt, kZahlungsart, kKundengruppe,"
" cExterneAuftragsnummer)"
" cExterneAuftragsnummer, nIstExterneRechnung, cInet, nIstReadOnly, kShopauftrag, nLieferPrioritaet)"
" OUTPUT inserted.kAuftrag INTO @t"
" VALUES (?, ?, 0, ?, ?, ?, 1, 1.0,"
" ?, ?, ?, 1.0, 0.0,"
" ?, ?, ?, ?, ?, ?, ?, ?,"
" ?);"
" ?, ?, ?, ?, 0, 10);"
"SELECT kAuftrag FROM @t";
std::string shipping_iso = order.value("shippingAddress", nlohmann::json::object()).value("countryIso", "DE");
@@ -658,6 +699,8 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
[](unsigned char ch) { return std::toupper(ch); });
std::string currency_iso = order.value("currencyIso", "EUR");
std::string cKundenNr = resolve_auftrag_c_kunden_nr(order);
int nIstExterneRechnung = resolve_n_ist_externe_rechnung(order);
std::vector<Param> ps = {
{ParamType::NVarChar, cAuftragsNr, 0},
{ParamType::NVarChar, order_date_sql(order_date_tm), 0},
@@ -670,12 +713,15 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
{ParamType::NVarChar, currency_iso, 0},
{ParamType::Int, "", defaults.kPlattform},
{ParamType::Int, "", active_shop == 0 ? -1 : active_shop},
{ParamType::NVarChar, order.value("customerNumber", ""), 0},
{ParamType::NVarChar, cKundenNr, 0},
{ParamType::NVarChar, shipping_iso, 0},
{ParamType::Int, "", defaults.kVersandArt},
{ParamType::Int, "", json_int(zahlungsart, "kZahlungsart", 0)},
{ParamType::Int, "", kKundengruppe},
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0}
{ParamType::NVarChar, order.value("externalOrderNumber", ""), 0},
{ParamType::Int, "", nIstExterneRechnung},
{ParamType::NVarChar, "Y", 0},
{ParamType::Int, "", 2},
};
if (active_shop == 0) {
ps[11] = Param::null_int();
@@ -719,7 +765,11 @@ inline nlohmann::json create_order(const nlohmann::json& order) {
if (!get_pool().commit(c)) throw std::runtime_error("failed to commit transaction");
return { {"orderId", std::to_string(kAuftrag)}, {"orderNumber", cAuftragsNr} };
return {
{"orderId", std::to_string(kAuftrag)},
{"orderNumber", cAuftragsNr},
{"alreadyExists", false}
};
} catch (const std::exception&) {
try { get_pool().rollback(c); } catch (...) {}
throw;

View File

@@ -3,6 +3,7 @@
#include "session.hpp"
#include "warehouse.hpp"
#include "reserve.hpp"
#include "stock_shortage.hpp"
#include "commit.hpp"
#include "deliver.hpp"
#include "../db/pool.hpp"
@@ -14,10 +15,13 @@ inline void deliver_order(OdbcPool::Connection* c, int kBenutzer, int kAuftrag,
if (items.empty()) return;
int kWarenLager = resolve_outgoing_warehouse(c);
int kWarenLagerPlatz = resolve_warehouse_place(c, kWarenLager);
int kSessionId = open_session(c, kBenutzer);
try {
reserve_positions(c, kBenutzer, kSessionId, kWarenLager, items);
book_stock_shortfalls_and_rereserve(c, kBenutzer, kSessionId, kWarenLager,
kWarenLagerPlatz, items);
commit_picklists(c, kBenutzer, kSessionId, kAuftrag);
deliver_picklists(c, kBenutzer, kSessionId, kAuftrag, kVersandArt);
} catch (...) {

View File

@@ -50,7 +50,10 @@ inline void reserve_positions(OdbcPool::Connection* c, int kBenutzer, int kSessi
};
ResultSet rs;
if (!get_pool().execute(c, sql, ps, rs)) {
throw std::runtime_error("reserve_positions failed");
const std::string& detail = get_pool().last_error();
throw std::runtime_error(detail.empty()
? "reserve_positions failed"
: "reserve_positions failed: " + detail);
}
}

View File

@@ -0,0 +1,137 @@
#pragma once
#include <cmath>
#include <stdexcept>
#include <string>
#include <vector>
#include "reserve.hpp"
#include "warehouse.hpp"
#include "../db/pool.hpp"
#include "../log.hpp"
namespace delivery {
namespace {
constexpr const char* POS_SHORTAGE_COMMENT = "Korrekturbuchung erstellt durch POS-Abgleich";
constexpr int BUCHUNGSART_WARENEINGANG = 10;
inline double get_reserved_quantity(OdbcPool::Connection* c, int kSessionId, int kAuftragPosition) {
const char* sql =
"SELECT ISNULL(SUM(pp.fAnzahl), 0) FROM dbo.tPicklistePos pp "
"INNER JOIN dbo.tPickliste p ON p.kPickliste = pp.kPickliste "
"WHERE p.kSessionId = ? AND pp.kBestellPos = ?";
std::vector<Param> ps = {
{ParamType::Int, "", kSessionId},
{ParamType::Int, "", kAuftragPosition},
};
ResultSet rs;
if (!get_pool().execute(c, sql, ps, rs) || rs.empty() || rs[0].empty()) {
return 0.0;
}
try {
return std::stod(rs[0][0].str);
} catch (...) {
return 0.0;
}
}
inline int get_position_artikel(OdbcPool::Connection* c, int kAuftragPosition) {
std::vector<Param> ps = {{ParamType::Int, "", kAuftragPosition}};
ResultSet rs;
if (!get_pool().execute(c,
"SELECT kArtikel FROM Verkauf.tAuftragPosition WHERE kAuftragPosition = ?",
ps, rs) || rs.empty() || rs[0].empty() || rs[0][0].type == CellType::Null) {
return 0;
}
try {
return std::stoi(rs[0][0].str);
} catch (...) {
return 0;
}
}
inline void book_wareneingang(OdbcPool::Connection* c, int kBenutzer,
int kWarenLagerPlatz, int kArtikel, double fehlmenge) {
const char* sql =
"DECLARE @kWarenlagerEingang INT;"
"EXEC dbo.spWarenlagerEingangSchreiben"
" @kArtikel = ?,"
" @kWarenLagerPlatz = ?,"
" @kLieferantenBestellungPos = 0,"
" @kBenutzer = ?,"
" @fAnzahl = ?,"
" @fEkEinzel = 0,"
" @cLieferscheinNr = '',"
" @cChargenNr = NULL,"
" @dMHD = NULL,"
" @dGeliefertAm = NULL,"
" @cKommentar = ?,"
" @kGutschriftPos = 0,"
" @kLHM = 0,"
" @kSessionId = 0,"
" @kBuchungsart = ?,"
" @kBestellPosUmlagerung = 0,"
" @kRMRetourePos = 0,"
" @nHistorieNichtSchreiben = 0,"
" @kWarenlagerEingang = @kWarenlagerEingang OUTPUT;"
"SELECT @kWarenlagerEingang AS kWarenlagerEingang;";
std::vector<Param> ps = {
{ParamType::Int, "", kArtikel},
{ParamType::Int, "", kWarenLagerPlatz},
{ParamType::Int, "", kBenutzer},
{ParamType::Double, "", 0, fehlmenge},
{ParamType::NVarChar, POS_SHORTAGE_COMMENT, 0},
{ParamType::Int, "", BUCHUNGSART_WARENEINGANG},
};
ResultSet rs;
if (!get_pool().execute(c, sql, ps, rs)) {
const std::string& detail = get_pool().last_error();
throw std::runtime_error(detail.empty()
? "spWarenlagerEingangSchreiben failed"
: "spWarenlagerEingangSchreiben failed: " + detail);
}
}
} // namespace
// PosStockPositionService.FehlbestandEinbuchen — book missing stock, then re-reserve.
inline void book_stock_shortfalls_and_rereserve(OdbcPool::Connection* c, int kBenutzer,
int kSessionId, int kWarenLager,
int kWarenLagerPlatz,
const std::vector<DeliveredItem>& items) {
std::vector<DeliveredItem> rereserve;
for (const auto& item : items) {
if (item.kAuftragPosition <= 0 || item.quantity <= 0.0) continue;
double reserved = get_reserved_quantity(c, kSessionId, item.kAuftragPosition);
double shortage = item.quantity - reserved;
if (shortage <= 0.0001) continue;
int kArtikel = get_position_artikel(c, item.kAuftragPosition);
if (kArtikel <= 0) continue;
logc::info("POS shortage booking: kBestellPos=%d kArtikel=%d reserved=%.4f need=%.4f book=%.4f",
item.kAuftragPosition, kArtikel, reserved, item.quantity, shortage);
book_wareneingang(c, kBenutzer, kWarenLagerPlatz, kArtikel, shortage);
rereserve.push_back({item.kAuftragPosition, shortage});
}
if (!rereserve.empty()) {
reserve_positions(c, kBenutzer, kSessionId, kWarenLager, rereserve);
}
for (const auto& item : items) {
if (item.kAuftragPosition <= 0 || item.quantity <= 0.0) continue;
double reserved = get_reserved_quantity(c, kSessionId, item.kAuftragPosition);
if (reserved + 0.0001 < item.quantity) {
throw std::runtime_error(
"insufficient stock after POS shortage booking for kBestellPos="
+ std::to_string(item.kAuftragPosition));
}
}
}
} // namespace delivery

View File

@@ -18,4 +18,20 @@ inline int resolve_outgoing_warehouse(OdbcPool::Connection* c) {
return std::stoi(rs[0][0].str);
}
inline int resolve_warehouse_place(OdbcPool::Connection* c, int kWarenLager) {
int configured = config::get_int("JTL_KWARENLAGERPLATZ", 0);
if (configured > 0) return configured;
std::vector<Param> ps = {{ParamType::Int, "", kWarenLager}};
ResultSet rs;
if (!get_pool().execute(c,
"SELECT TOP 1 kWarenLagerPlatz FROM dbo.tWarenLagerPlatz "
"WHERE kWarenLager = ? AND ISNULL(nGesperrt, 0) = 0 "
"ORDER BY nPrio, kWarenLagerPlatz", ps, rs) || rs.empty() || rs[0].empty()) {
throw std::runtime_error("No warehouse place found for kWarenLager="
+ std::to_string(kWarenLager) + "; set JTL_KWARENLAGERPLATZ explicitly.");
}
return std::stoi(rs[0][0].str);
}
} // namespace delivery