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.
//! 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::<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
.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::<f64, _>("net").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 statkosten = r.get::<f64, _>("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!(
"<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
.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(())
}
}