feat(erp): Writeback schreibt abgeleitete Beleg-Felder mit, _SV_DELIVERY raus

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) <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-07-10 23:25:09 +02:00
parent 09cd45d67c
commit d7a63b5df3

View File

@ -11,7 +11,6 @@
//! 2. Je Belegzeile die `Menge` auf die ausgelieferte Menge setzen. //! 2. Je Belegzeile die `Menge` auf die ausgelieferte Menge setzen.
//! 3. Gutschrift-Zeile (`GUTSCHRIFT10`) anlegen/aktualisieren. //! 3. Gutschrift-Zeile (`GUTSCHRIFT10`) anlegen/aktualisieren.
//! 4. Belegsummen neu berechnen (`Σ Einzelpreis × Menge`, `Σ Brutto × Menge`). //! 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, //! Hinweis: Das Setzen der Menge per Raw-`UPDATE` umgeht die ERPframe-Engine,
//! erzeugt also KEINE Lagerbuchung. Das ist gewollt — das Lager verbucht den //! 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 /// TEST: Setzt die Menge einer Belegzeile auf die ausgelieferte Menge UND
/// tatsächlich ausgelieferte Menge. /// 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( async fn set_line_menge(
client: &mut TiberiusClient, client: &mut TiberiusClient,
bk_row_id: i64, bk_row_id: i64,
belegzeilen_nr: i32, belegzeilen_nr: i32,
menge: i32, menge: i32,
lager: &str,
) -> Result<(), ApplicationError> { ) -> 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::<f64, _>("m").unwrap_or(0.0);
let su = row.get::<f64, _>("su").unwrap_or(0.0);
let sk = row.get::<f64, _>("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!(
"<Root>\r\n <Row>\r\n <Lager>{}</Lager>\r\n <Menge>{}</Menge>\r\n </Row>\r\n</Root>",
lager.trim(),
menge
);
// VPEMenge NICHT setzen (berechnete Spalte → Fehler 271).
client client
.execute( .execute(
r#"UPDATE Belegzeilen SET Menge = @P1 r#"UPDATE Belegzeilen
WHERE ParentID = @P2 AND BelegzeilenNr = @P3"#, SET Menge = @P1, StatUmsatz = @P2,
&[&menge, &bk_row_id, &belegzeilen_nr], 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 .await
.map_err(repo)?; .map_err(repo)?;
@ -250,7 +296,11 @@ impl MssqlErpDeliveryWriteback {
Ok(()) 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( async fn recalc_head(
client: &mut TiberiusClient, client: &mut TiberiusClient,
bk_row_id: i64, bk_row_id: i64,
@ -259,7 +309,8 @@ impl MssqlErpDeliveryWriteback {
.query( .query(
r#"SELECT CAST(ISNULL(Einzelpreis,0) AS FLOAT) AS net, r#"SELECT CAST(ISNULL(Einzelpreis,0) AS FLOAT) AS net,
CAST(ISNULL(EinzelPreisBrutto,0) AS FLOAT) AS gross, 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"#, FROM Belegzeilen WHERE ParentID = @P1"#,
&[&bk_row_id], &[&bk_row_id],
) )
@ -271,44 +322,49 @@ impl MssqlErpDeliveryWriteback {
let mut net_sum = 0.0_f64; let mut net_sum = 0.0_f64;
let mut gross_sum = 0.0_f64; let mut gross_sum = 0.0_f64;
let mut statkosten_sum = 0.0_f64;
for r in &rows { for r in &rows {
let net = r.get::<f64, _>("net").unwrap_or(0.0); let net = r.get::<f64, _>("net").unwrap_or(0.0);
let gross = r.get::<f64, _>("gross").unwrap_or(0.0); let gross = r.get::<f64, _>("gross").unwrap_or(0.0);
let menge = r.get::<f64, _>("menge").unwrap_or(0.0); let menge = r.get::<f64, _>("menge").unwrap_or(0.0);
let statkosten = r.get::<f64, _>("statkosten").unwrap_or(0.0);
net_sum += net * menge; net_sum += net * menge;
gross_sum += gross * menge; gross_sum += gross * menge;
statkosten_sum += statkosten;
} }
// Kalkulations-XML (Material = Σ StatKosten, SUMMEVK = Netto-Warenwert).
let kalk = format!(
"<Root>\r\n <Row>\r\n <Material>{sk}</Material>\r\n \
<SUMMEVK>{vk}</SUMMEVK>\r\n <Material_Soll>{sk}</Material_Soll>\r\n \
</Row>\r\n</Root>",
sk = statkosten_sum,
vk = net_sum
);
let druckkennzeichen: &str = "1";
client client
.execute( .execute(
r#"UPDATE Belegkopf r#"UPDATE Belegkopf
SET WarenwertNetto = @P1, WarenwertBrutto = @P2, PosSummeNetto = @P3 SET WarenwertNetto = @P1, WarenwertBrutto = @P2, PosSummeNetto = @P3,
WHERE row_id = @P4"#, StatKosten = @P4, kalk = @P5, Druckkennzeichen = @P6,
&[&net_sum, &gross_sum, &net_sum, &bk_row_id], row_update_time = GETDATE()
WHERE row_id = @P7"#,
&[
&net_sum,
&gross_sum,
&net_sum,
&statkosten_sum,
&kalk,
&druckkennzeichen,
&bk_row_id,
],
) )
.await .await
.map_err(repo)?; .map_err(repo)?;
Ok(()) 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- /// Setzt `Belegkopf.ZahlungsbedingungId` anhand der gewählten Zahlungs-
/// methode (Code → ERP-Zahlungsbedingung → deren `row_id`). Idempotent /// methode (Code → ERP-Zahlungsbedingung → deren `row_id`). Idempotent
@ -366,12 +422,29 @@ impl MssqlErpDeliveryWriteback {
) -> Result<(), ApplicationError> { ) -> Result<(), ApplicationError> {
let bk = Self::resolve_belegkopf(client, cmd.belegart_id, &cmd.belegnummer).await?; let bk = Self::resolve_belegkopf(client, cmd.belegart_id, &cmd.belegnummer).await?;
// Je Belegzeile die Menge auf die ausgelieferte Menge setzen. Umgeht // Default-Lager des Belegs (für die Lagerverteilung der Zeilen).
// die ERPframe-Engine (keine Lagerbuchung) — das Lager verbucht den let lager_rows = client
// Bestand manuell nach Rückgabe des Geräts. Geänderte Lieferungen sind .query(
// zusätzlich per Vier-Augen-Prüfung markiert. "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 { 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). // Geld-Gutschrift (sofern per Config aktiviert).
@ -386,9 +459,6 @@ impl MssqlErpDeliveryWriteback {
Self::set_payment_condition(client, bk, code).await?; 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(()) Ok(())
} }
} }