From d7a63b5df3edc3921bddd4d21d3acbf001cb6534 Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Fri, 10 Jul 2026 23:25:09 +0200 Subject: [PATCH] feat(erp): Writeback schreibt abgeleitete Beleg-Felder mit, _SV_DELIVERY raus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Beim Mengen-Rückschreiben werden jetzt auch die von der ERPframe-Engine mitgeführten abgeleiteten Felder gesetzt, damit der Beleg konsistent bleibt (Lagerbestand bucht das Lager weiterhin manuell — keine Lagerbewegung): - Belegzeile: StatUmsatz, StatKosten (linear aus Alt-Stand skaliert), Lagerverteilung (XML neu), row_update_time. VPEMenge NICHT (berechnete Spalte → Fehler 271). - Belegkopf: zusätzlich StatKosten, kalk (Kalkulations-XML), Druckkennzeichen, row_update_time. - Entfernt: mark_delivered (_SV_DELIVERY_DELIVERED_AT/_SV_DELIVERY_STATE) — wird nicht mehr benötigt. Alle geschriebenen Spalten gegen sys.computed_columns verifiziert. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/erp/mssql_delivery_writeback.rs | 144 +++++++++++++----- 1 file changed, 107 insertions(+), 37 deletions(-) diff --git a/crates/infrastructure/src/erp/mssql_delivery_writeback.rs b/crates/infrastructure/src/erp/mssql_delivery_writeback.rs index d326584..fd9b363 100644 --- a/crates/infrastructure/src/erp/mssql_delivery_writeback.rs +++ b/crates/infrastructure/src/erp/mssql_delivery_writeback.rs @@ -11,7 +11,6 @@ //! 2. Je Belegzeile die `Menge` auf die ausgelieferte Menge setzen. //! 3. Gutschrift-Zeile (`GUTSCHRIFT10`) anlegen/aktualisieren. //! 4. Belegsummen neu berechnen (`Σ Einzelpreis × Menge`, `Σ Brutto × Menge`). -//! 5. `_SV_DELIVERY_DELIVERED_AT` + `_SV_DELIVERY_STATE='geliefert'`. //! //! Hinweis: Das Setzen der Menge per Raw-`UPDATE` umgeht die ERPframe-Engine, //! erzeugt also KEINE Lagerbuchung. Das ist gewollt — das Lager verbucht den @@ -125,19 +124,66 @@ impl MssqlErpDeliveryWriteback { }) } - /// Setzt die Menge einer Belegzeile (absolut, idempotent) auf die - /// tatsächlich ausgelieferte Menge. + /// TEST: Setzt die Menge einer Belegzeile auf die ausgelieferte Menge UND + /// die von der ERPframe-Engine mitgeführten abgeleiteten Felder + /// (`StatUmsatz`, `StatKosten`, `Lagerverteilung`, `row_update_time`). + /// Stat-Werte werden **linear** aus dem Alt-Stand skaliert (Faktor = + /// neue/alte Menge). `VPEMenge` wird NICHT gesetzt — es ist eine + /// **berechnete** Spalte und aktualisiert sich automatisch aus `Menge`. + /// KEINE Lagerbuchung — das Lager verbucht den Bestand manuell. async fn set_line_menge( client: &mut TiberiusClient, bk_row_id: i64, belegzeilen_nr: i32, menge: i32, + lager: &str, ) -> Result<(), ApplicationError> { + // Alt-Werte lesen (für die lineare Skalierung). + let rows = client + .query( + r#"SELECT CAST(ISNULL(Menge,0) AS FLOAT) AS m, + CAST(ISNULL(StatUmsatz,0) AS FLOAT) AS su, + CAST(ISNULL(StatKosten,0) AS FLOAT) AS sk + FROM Belegzeilen WHERE ParentID = @P1 AND BelegzeilenNr = @P2"#, + &[&bk_row_id, &belegzeilen_nr], + ) + .await + .map_err(repo)? + .into_first_result() + .await + .map_err(repo)?; + let Some(row) = rows.first() else { + return Ok(()); // Zeile nicht gefunden → nichts zu tun + }; + let old_m = row.get::("m").unwrap_or(0.0); + let su = row.get::("su").unwrap_or(0.0); + let sk = row.get::("sk").unwrap_or(0.0); + + let factor = if old_m != 0.0 { menge as f64 / old_m } else { 0.0 }; + let new_su = su * factor; + let new_sk = sk * factor; + let lagerverteilung = format!( + "\r\n \r\n {}\r\n {}\r\n \r\n", + lager.trim(), + menge + ); + + // VPEMenge NICHT setzen (berechnete Spalte → Fehler 271). client .execute( - r#"UPDATE Belegzeilen SET Menge = @P1 - WHERE ParentID = @P2 AND BelegzeilenNr = @P3"#, - &[&menge, &bk_row_id, &belegzeilen_nr], + r#"UPDATE Belegzeilen + SET Menge = @P1, StatUmsatz = @P2, + StatKosten = @P3, Lagerverteilung = @P4, + row_update_time = GETDATE() + WHERE ParentID = @P5 AND BelegzeilenNr = @P6"#, + &[ + &menge, + &new_su, + &new_sk, + &lagerverteilung, + &bk_row_id, + &belegzeilen_nr, + ], ) .await .map_err(repo)?; @@ -250,7 +296,11 @@ impl MssqlErpDeliveryWriteback { Ok(()) } - /// Summiert alle Belegzeilen neu und schreibt die Kopf-Summen. + /// Summiert alle Belegzeilen neu und schreibt die Kopf-Summen. TEST: + /// zusätzlich `StatKosten`, `kalk` (Kalkulations-XML), `Druckkennzeichen` + /// und `row_update_time` — analog zur ERPframe-Engine. `kalk` nutzt die + /// (bereits skalierten) Zeilen-Werte; wegen Rundung nicht auf die letzte + /// Nachkommastelle deckungsgleich mit der Engine. async fn recalc_head( client: &mut TiberiusClient, bk_row_id: i64, @@ -259,7 +309,8 @@ impl MssqlErpDeliveryWriteback { .query( r#"SELECT CAST(ISNULL(Einzelpreis,0) AS FLOAT) AS net, CAST(ISNULL(EinzelPreisBrutto,0) AS FLOAT) AS gross, - CAST(ISNULL(Menge,0) AS FLOAT) AS menge + CAST(ISNULL(Menge,0) AS FLOAT) AS menge, + CAST(ISNULL(StatKosten,0) AS FLOAT) AS statkosten FROM Belegzeilen WHERE ParentID = @P1"#, &[&bk_row_id], ) @@ -271,44 +322,49 @@ impl MssqlErpDeliveryWriteback { let mut net_sum = 0.0_f64; let mut gross_sum = 0.0_f64; + let mut statkosten_sum = 0.0_f64; for r in &rows { let net = r.get::("net").unwrap_or(0.0); let gross = r.get::("gross").unwrap_or(0.0); let menge = r.get::("menge").unwrap_or(0.0); + let statkosten = r.get::("statkosten").unwrap_or(0.0); net_sum += net * menge; gross_sum += gross * menge; + statkosten_sum += statkosten; } + // Kalkulations-XML (Material = Σ StatKosten, SUMMEVK = Netto-Warenwert). + let kalk = format!( + "\r\n \r\n {sk}\r\n \ + {vk}\r\n {sk}\r\n \ + \r\n", + sk = statkosten_sum, + vk = net_sum + ); + let druckkennzeichen: &str = "1"; + client .execute( r#"UPDATE Belegkopf - SET WarenwertNetto = @P1, WarenwertBrutto = @P2, PosSummeNetto = @P3 - WHERE row_id = @P4"#, - &[&net_sum, &gross_sum, &net_sum, &bk_row_id], + SET WarenwertNetto = @P1, WarenwertBrutto = @P2, PosSummeNetto = @P3, + StatKosten = @P4, kalk = @P5, Druckkennzeichen = @P6, + row_update_time = GETDATE() + WHERE row_id = @P7"#, + &[ + &net_sum, + &gross_sum, + &net_sum, + &statkosten_sum, + &kalk, + &druckkennzeichen, + &bk_row_id, + ], ) .await .map_err(repo)?; Ok(()) } - /// Setzt Liefer-Zeitpunkt + Status. ISO-8601 mit `T` als String - /// (impliziter SQL-Cast), exakt wie das Alt-Makro. - async fn mark_delivered( - client: &mut TiberiusClient, - bk_row_id: i64, - delivered_at_iso: &str, - ) -> Result<(), ApplicationError> { - client - .execute( - r#"UPDATE Belegkopf - SET _SV_DELIVERY_DELIVERED_AT = @P1, _SV_DELIVERY_STATE = 'geliefert' - WHERE row_id = @P2"#, - &[&delivered_at_iso, &bk_row_id], - ) - .await - .map_err(repo)?; - Ok(()) - } /// Setzt `Belegkopf.ZahlungsbedingungId` anhand der gewählten Zahlungs- /// methode (Code → ERP-Zahlungsbedingung → deren `row_id`). Idempotent @@ -366,12 +422,29 @@ impl MssqlErpDeliveryWriteback { ) -> Result<(), ApplicationError> { let bk = Self::resolve_belegkopf(client, cmd.belegart_id, &cmd.belegnummer).await?; - // Je Belegzeile die Menge auf die ausgelieferte Menge setzen. Umgeht - // die ERPframe-Engine (keine Lagerbuchung) — das Lager verbucht den - // Bestand manuell nach Rückgabe des Geräts. Geänderte Lieferungen sind - // zusätzlich per Vier-Augen-Prüfung markiert. + // Default-Lager des Belegs (für die Lagerverteilung der Zeilen). + let lager_rows = client + .query( + "SELECT LTRIM(RTRIM(Lager)) AS lager FROM Belegkopf WHERE row_id = @P1", + &[&bk], + ) + .await + .map_err(repo)? + .into_first_result() + .await + .map_err(repo)?; + let lager = lager_rows + .first() + .and_then(|r| r.get::<&str, _>("lager")) + .unwrap_or("") + .to_string(); + + // Je Belegzeile die Menge (+ abgeleitete Felder) auf die ausgelieferte + // Menge setzen. Umgeht die ERPframe-Engine (keine Lagerbuchung) — das + // Lager verbucht den Bestand manuell nach Rückgabe des Geräts. for line in &cmd.lines { - Self::set_line_menge(client, bk, line.belegzeilen_nr, line.delivered_quantity).await?; + Self::set_line_menge(client, bk, line.belegzeilen_nr, line.delivered_quantity, &lager) + .await?; } // Geld-Gutschrift (sofern per Config aktiviert). @@ -386,9 +459,6 @@ impl MssqlErpDeliveryWriteback { Self::set_payment_condition(client, bk, code).await?; } - let iso = cmd.delivered_at.format("%Y-%m-%dT%H:%M:%S").to_string(); - Self::mark_delivered(client, bk, &iso).await?; - Ok(()) } }