fix(sync): Lieferadresse nicht mehr feldweise mischen, Adresszusatz statt Land

- Sync nimmt die Lieferadresse als Ganzes: mit eigener LieferAdressId
  alle Felder aus ihr, sonst alle aus der Belegadresse. Das bisherige
  COALESCE je Feld hat leere Felder mit Werten des Bestellers gefüllt
  (z. B. dessen Ortsteil an eine fremde Anschrift gehängt).
- ERP-Freitext `Adressen.Land` (praktisch Ortsteil/Etage/Hinweis) wird
  zum Adresszusatz `Address.addition`; `country` kommt jetzt aus dem
  Länderstamm (`LandID` → `Laender.Land`). Deutschland-Kürzel und reine
  Landnamen sind kein Zusatz.
- Migration 0034: `customers.address_addition`, `deliveries.snap_addition`;
  Bestandstext aus `country` wandert in den Zusatz.
- Lieferbericht und Beleg-Ansicht: Land nur außerhalb Deutschlands,
  Zusatz als eigene Zeile „Zusatz: …".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 16:13:13 +02:00
parent af6d522290
commit c9d0ccfdef
11 changed files with 231 additions and 89 deletions

View File

@ -16,5 +16,74 @@ pub struct Address {
pub house_number: String,
pub postal_code: String,
pub city: String,
/// Land laut ERP-Länderstamm (`Adressen.LandID` → `Laender.Land`), z. B.
/// „Deutschland" oder „Belgien". Leer, wenn im ERP kein Land verknüpft ist.
pub country: String,
/// Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das
/// die Sachbearbeitung praktisch für Ortsteil, Etage oder Hinweise nutzt
/// (z. B. „Holzheim", „3. Etage", „Parterre"). `None`, wenn leer.
#[serde(default)]
pub addition: Option<String>,
}
impl Address {
/// Einzeilige Anschrift „Straße Nr, PLZ Ort" — das Land nur, wenn es
/// nicht Deutschland ist. Der Adresszusatz gehört nicht dazu (eigene
/// Zeile in der Anzeige).
pub fn one_line(&self) -> String {
let line1 = join_non_empty(&[&self.street, &self.house_number], " ");
let line2 = join_non_empty(&[&self.postal_code, &self.city], " ");
let country = self.foreign_country().unwrap_or_default();
join_non_empty(&[&line1, &line2, country], ", ")
}
/// Land für die Anzeige: `None` für Deutschland (Heimatmarkt) oder leer.
pub fn foreign_country(&self) -> Option<&str> {
let c = self.country.trim();
(!c.is_empty() && !is_germany(c)).then_some(c)
}
}
/// `true` für die im ERP vorkommenden Schreibweisen von Deutschland.
pub fn is_germany(value: &str) -> bool {
matches!(
value.trim().to_lowercase().as_str(),
"d" | "de" | "deu" | "deutschland" | "germany" | "brd"
)
}
fn join_non_empty(parts: &[&str], sep: &str) -> String {
parts
.iter()
.map(|p| p.trim())
.filter(|p| !p.is_empty())
.collect::<Vec<_>>()
.join(sep)
}
#[cfg(test)]
mod address_tests {
use super::*;
fn address(country: &str) -> Address {
Address {
street: "Am Sandberg".into(),
house_number: "15".into(),
postal_code: "41472".into(),
city: "Neuss".into(),
country: country.into(),
addition: Some("Holzheim".into()),
}
}
#[test]
fn deutschland_und_zusatz_stehen_nicht_in_der_zeile() {
assert_eq!(address("Deutschland").one_line(), "Am Sandberg 15, 41472 Neuss");
assert_eq!(address("").one_line(), "Am Sandberg 15, 41472 Neuss");
}
#[test]
fn ausland_wird_angehaengt() {
assert_eq!(address("Belgien").one_line(), "Am Sandberg 15, 41472 Neuss, Belgien");
}
}

View File

@ -33,7 +33,7 @@ pub use account::Account;
pub use article::Article;
pub use audit::{AuditAction, ScanAuditEntry};
pub use car::Car;
pub use common::Address;
pub use common::{is_germany, Address};
pub use contact::{ContactChannel, ContactKind, ContactRole, ContactSource};
pub use customer::{Customer, CustomerContact};
pub use delivery::{