diff --git a/crates/api/src/routes/belege_view.rs b/crates/api/src/routes/belege_view.rs index cb8591e..37e801b 100644 --- a/crates/api/src/routes/belege_view.rs +++ b/crates/api/src/routes/belege_view.rs @@ -301,8 +301,13 @@ fn address_lines(a: &Address) -> Vec { if !city.is_empty() { lines.push(city); } - if !a.country.trim().is_empty() { - lines.push(a.country.clone()); + // Land nur außerhalb Deutschlands; der Adresszusatz (Ortsteil, Etage, + // Hinweis) als eigene, gekennzeichnete Zeile. + if let Some(country) = a.foreign_country() { + lines.push(country.to_string()); + } + if let Some(addition) = a.addition.as_deref().filter(|z| !z.trim().is_empty()) { + lines.push(format!("Zusatz: {addition}")); } lines } diff --git a/crates/application/src/dto/delivery_report.rs b/crates/application/src/dto/delivery_report.rs index 7751ba6..fd2e686 100644 --- a/crates/application/src/dto/delivery_report.rs +++ b/crates/application/src/dto/delivery_report.rs @@ -30,6 +30,8 @@ pub struct DeliveryReportData { pub customer_name: String, /// Lieferadresse (Snapshot am Beleg), einzeilig. pub address: String, + /// Adresszusatz der Lieferadresse (Ortsteil, Etage, Hinweis). + pub address_addition: Option, /// Nur gesetzt, wenn Empfänger oder Lieferadresse vom Besteller /// abweichen (ERP-`LieferAdressId`). Dann ist `customer_*` der Besteller /// und `address` die abweichende Lieferadresse. @@ -60,6 +62,7 @@ pub struct ReportRecipient { pub phones: Vec, /// Anschrift des Bestellers (Kundenstamm), einzeilig. pub orderer_address: String, + pub orderer_address_addition: Option, pub orderer_phones: Vec, } diff --git a/crates/domain/src/common.rs b/crates/domain/src/common.rs index a741b18..f3b07c8 100644 --- a/crates/domain/src/common.rs +++ b/crates/domain/src/common.rs @@ -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, +} + +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::>() + .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"); + } } diff --git a/crates/domain/src/lib.rs b/crates/domain/src/lib.rs index 9936659..6135bbb 100644 --- a/crates/domain/src/lib.rs +++ b/crates/domain/src/lib.rs @@ -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::{ diff --git a/crates/infrastructure/src/erp/mssql_delivery_source.rs b/crates/infrastructure/src/erp/mssql_delivery_source.rs index b64f3af..24754f1 100644 --- a/crates/infrastructure/src/erp/mssql_delivery_source.rs +++ b/crates/infrastructure/src/erp/mssql_delivery_source.rs @@ -23,7 +23,7 @@ use holzleitner_application::dto::{ }; use holzleitner_application::error::ApplicationError; use holzleitner_application::ports::ErpDeliverySource; -use holzleitner_domain::{Address, ContactKind, ContactRole}; +use holzleitner_domain::{is_germany, Address, ContactKind, ContactRole}; /// Verbindungsparameter zur ERPframe-MSSQL. #[derive(Debug, Clone)] @@ -141,12 +141,18 @@ SELECT adr.Hausnummer AS custHouseNumber, LTRIM(RTRIM(adr.PLZ)) AS custPostalCode, adr.Ort AS custCity, - adr.Land AS custCountry, - COALESCE(dadr.Strasse, adr.Strasse) AS delivStreet, - COALESCE(dadr.Hausnummer, adr.Hausnummer) AS delivHouseNumber, - LTRIM(RTRIM(COALESCE(dadr.PLZ, adr.PLZ))) AS delivPostalCode, - COALESCE(dadr.Ort, adr.Ort) AS delivCity, - COALESCE(dadr.Land, adr.Land) AS delivCountry, + LTRIM(RTRIM(hland.Land)) AS custCountry, + adr.Land AS custAddition, + -- Lieferadresse als Ganzes: gibt es eine eigene, kommen ALLE Felder aus + -- ihr, sonst alle aus der Belegadresse. Kein Feld-für-Feld-COALESCE — + -- das hat leere Felder der Lieferadresse mit Werten des Bestellers + -- aufgefüllt (z. B. dessen Ortsteil an eine fremde Anschrift gehängt). + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Strasse ELSE adr.Strasse END AS delivStreet, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Hausnummer ELSE adr.Hausnummer END AS delivHouseNumber, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.PLZ ELSE adr.PLZ END)) AS delivPostalCode, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Ort ELSE adr.Ort END AS delivCity, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dland.Land ELSE hland.Land END)) AS delivCountry, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Land ELSE adr.Land END AS delivAddition, LTRIM(RTRIM(CAST(bk._Uhrzeit_Txt AS varchar(200)))) AS desiredTime, LTRIM(RTRIM(CAST(bk.Kopftext AS varchar(MAX)))) AS specialAgreements, CAST(ISNULL(bk._Anz_Bestellung, 0) AS FLOAT) AS prepaidAmount, @@ -174,6 +180,8 @@ SELECT FROM Belegkopf bk JOIN Adressen adr ON bk.AdressId = adr.ROW_ID LEFT JOIN Adressen dadr ON bk.LieferAdressId = dadr.ROW_ID +LEFT JOIN Laender hland ON hland.ROW_ID = adr.LandID +LEFT JOIN Laender dland ON dland.ROW_ID = dadr.LandID LEFT JOIN Adressen radr ON bk.RechnungsAdressId = radr.ROW_ID LEFT JOIN Adressen kadr ON bk.AnsprechpartnerId = kadr.ROW_ID JOIN Belegzeilen bz ON bk.row_id = bz.ParentID @@ -215,12 +223,18 @@ SELECT adr.Hausnummer AS custHouseNumber, LTRIM(RTRIM(adr.PLZ)) AS custPostalCode, adr.Ort AS custCity, - adr.Land AS custCountry, - COALESCE(dadr.Strasse, adr.Strasse) AS delivStreet, - COALESCE(dadr.Hausnummer, adr.Hausnummer) AS delivHouseNumber, - LTRIM(RTRIM(COALESCE(dadr.PLZ, adr.PLZ))) AS delivPostalCode, - COALESCE(dadr.Ort, adr.Ort) AS delivCity, - COALESCE(dadr.Land, adr.Land) AS delivCountry, + LTRIM(RTRIM(hland.Land)) AS custCountry, + adr.Land AS custAddition, + -- Lieferadresse als Ganzes: gibt es eine eigene, kommen ALLE Felder aus + -- ihr, sonst alle aus der Belegadresse. Kein Feld-für-Feld-COALESCE — + -- das hat leere Felder der Lieferadresse mit Werten des Bestellers + -- aufgefüllt (z. B. dessen Ortsteil an eine fremde Anschrift gehängt). + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Strasse ELSE adr.Strasse END AS delivStreet, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Hausnummer ELSE adr.Hausnummer END AS delivHouseNumber, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.PLZ ELSE adr.PLZ END)) AS delivPostalCode, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Ort ELSE adr.Ort END AS delivCity, + LTRIM(RTRIM(CASE WHEN dadr.ROW_ID IS NOT NULL THEN dland.Land ELSE hland.Land END)) AS delivCountry, + CASE WHEN dadr.ROW_ID IS NOT NULL THEN dadr.Land ELSE adr.Land END AS delivAddition, LTRIM(RTRIM(CAST(bk._Uhrzeit_Txt AS varchar(200)))) AS desiredTime, LTRIM(RTRIM(CAST(bk.Kopftext AS varchar(MAX)))) AS specialAgreements, CAST(ISNULL(bk._Anz_Bestellung, 0) AS FLOAT) AS prepaidAmount, @@ -243,6 +257,8 @@ SELECT FROM Belegkopf bk JOIN Adressen adr ON bk.AdressId = adr.ROW_ID LEFT JOIN Adressen dadr ON bk.LieferAdressId = dadr.ROW_ID +LEFT JOIN Laender hland ON hland.ROW_ID = adr.LandID +LEFT JOIN Laender dland ON dland.ROW_ID = dadr.LandID LEFT JOIN Adressen radr ON bk.RechnungsAdressId = radr.ROW_ID LEFT JOIN Adressen kadr ON bk.AnsprechpartnerId = kadr.ROW_ID JOIN Belegzeilen bz ON bk.row_id = bz.ParentID @@ -412,6 +428,14 @@ fn read_all_contact_sources(row: &tiberius::Row) -> Vec { .collect() } +/// Adresszusatz aus dem ERP-Freitextfeld `Adressen.Land`. Das Feld wird +/// praktisch für Ortsteil/Etage/Hinweise genutzt; steht dort nur das Land +/// (Deutschland-Kürzel oder derselbe Name wie im Länderstamm), ist es kein +/// Zusatz. +fn address_addition(free_text: Option, country: &str) -> Option { + free_text.filter(|t| !is_germany(t) && !t.eq_ignore_ascii_case(country.trim())) +} + fn map_row(row: &tiberius::Row) -> ErpRow { ErpRow { driver_personalnummer: i64c(row, "driverPersonalnummer"), @@ -427,6 +451,7 @@ fn map_row(row: &tiberius::Row) -> ErpRow { postal_code: s(row, "custPostalCode"), city: s(row, "custCity"), country: s(row, "custCountry"), + addition: address_addition(opt_s(row, "custAddition"), &s(row, "custCountry")), }, deliv: Address { street: s(row, "delivStreet"), @@ -434,6 +459,7 @@ fn map_row(row: &tiberius::Row) -> ErpRow { postal_code: s(row, "delivPostalCode"), city: s(row, "delivCity"), country: s(row, "delivCountry"), + addition: address_addition(opt_s(row, "delivAddition"), &s(row, "delivCountry")), }, desired_time: opt_s(row, "desiredTime"), special_agreements: opt_s(row, "specialAgreements"), @@ -584,3 +610,23 @@ impl ErpDeliverySource for MssqlErpDeliverySource { Ok(tours) } } + +#[cfg(test)] +mod address_addition_tests { + use super::address_addition; + + #[test] + fn ortsteil_und_hinweise_sind_zusatz() { + assert_eq!(address_addition(Some("Holzheim".into()), "Deutschland").as_deref(), Some("Holzheim")); + assert_eq!(address_addition(Some("3 Etage".into()), "").as_deref(), Some("3 Etage")); + } + + #[test] + fn reines_land_ist_kein_zusatz() { + for t in ["D", "DEU", "Deutschland"] { + assert_eq!(address_addition(Some(t.into()), ""), None, "{t}"); + } + assert_eq!(address_addition(Some("belgien".into()), "Belgien"), None); + assert_eq!(address_addition(None, "Deutschland"), None); + } +} diff --git a/crates/infrastructure/src/persistence/delivery_completion_repository.rs b/crates/infrastructure/src/persistence/delivery_completion_repository.rs index 08c4e6f..24d8e8f 100644 --- a/crates/infrastructure/src/persistence/delivery_completion_repository.rs +++ b/crates/infrastructure/src/persistence/delivery_completion_repository.rs @@ -48,6 +48,7 @@ struct DeliveryRow { snap_postal_code: String, snap_city: String, snap_country: String, + snap_addition: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -68,7 +69,7 @@ async fn lock_delivery( r#" SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, - snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, snap_addition, assigned_car_id, desired_time, special_agreements, state, prepaid_amount, payment_method_id FROM deliveries @@ -109,6 +110,7 @@ fn build_delivery(row: DeliveryRow, state: DeliveryState, state_reason: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -112,7 +113,7 @@ async fn lock_delivery( r#" SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, - snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, snap_addition, assigned_car_id, desired_time, special_agreements, state, prepaid_amount, payment_method_id FROM deliveries @@ -193,6 +194,7 @@ impl DeliveryRepository for PgDeliveryRepository { postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: row.assigned_car_id, contact_person_ids, @@ -250,6 +252,7 @@ impl DeliveryRepository for PgDeliveryRepository { postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: car_id, contact_person_ids, diff --git a/crates/infrastructure/src/persistence/tour_repository.rs b/crates/infrastructure/src/persistence/tour_repository.rs index 4688cd8..2242fbf 100644 --- a/crates/infrastructure/src/persistence/tour_repository.rs +++ b/crates/infrastructure/src/persistence/tour_repository.rs @@ -75,6 +75,7 @@ struct DeliveryRow { snap_postal_code: String, snap_city: String, snap_country: String, + snap_addition: Option, assigned_car_id: Option, desired_time: Option, special_agreements: Option, @@ -113,6 +114,7 @@ struct CustomerRow { postal_code: String, city: String, country: String, + address_addition: Option, } #[derive(sqlx::FromRow)] @@ -251,6 +253,7 @@ fn map_customer(row: CustomerRow) -> Customer { postal_code: row.postal_code, city: row.city, country: row.country, + addition: row.address_addition, }, } } @@ -413,6 +416,7 @@ fn map_delivery( postal_code: row.snap_postal_code, city: row.snap_city, country: row.snap_country, + addition: row.snap_addition, }, assigned_car_id: row.assigned_car_id, contact_person_ids, @@ -490,6 +494,7 @@ impl TourRepository for PgTourRepository { SELECT id, tour_id, erp_belegart_id, erp_belegnummer, customer_id, snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, + snap_addition, assigned_car_id, desired_time, special_agreements, state, state_reason, sort_order, prepaid_amount, payment_method_id @@ -578,7 +583,8 @@ impl TourRepository for PgTourRepository { let customer_ids_vec: Vec = customer_ids.into_iter().collect(); let customers = sqlx::query_as::<_, CustomerRow>( r#" - SELECT id, erp_customer_id, name, street, house_number, postal_code, city, country + SELECT id, erp_customer_id, name, street, house_number, postal_code, city, country, + address_addition FROM customers WHERE id = ANY($1) ORDER BY name @@ -1105,15 +1111,17 @@ async fn upsert_customer( let id: Uuid = sqlx::query_scalar( r#" INSERT INTO customers ( - erp_customer_id, name, street, house_number, postal_code, city, country - ) VALUES ($1, $2, $3, $4, $5, $6, $7) + erp_customer_id, name, street, house_number, postal_code, city, country, + address_addition + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (erp_customer_id) DO UPDATE SET name = EXCLUDED.name, street = EXCLUDED.street, house_number = EXCLUDED.house_number, postal_code = EXCLUDED.postal_code, city = EXCLUDED.city, - country = EXCLUDED.country + country = EXCLUDED.country, + address_addition = EXCLUDED.address_addition RETURNING id "#, ) @@ -1124,6 +1132,7 @@ async fn upsert_customer( .bind(&delivery.customer_address.postal_code) .bind(&delivery.customer_address.city) .bind(&delivery.customer_address.country) + .bind(delivery.customer_address.addition.as_deref()) .fetch_one(&mut **tx) .await .map_err(db)?; @@ -1163,8 +1172,8 @@ async fn upsert_delivery( erp_belegnummer, customer_id, snap_street, snap_house_number, snap_postal_code, snap_city, snap_country, sort_order, desired_time, special_agreements, - prepaid_amount, payment_method_id - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) + prepaid_amount, payment_method_id, snap_addition + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) ON CONFLICT (erp_belegart_id, erp_belegnummer) DO UPDATE SET tour_id = EXCLUDED.tour_id, erp_belegart_code = EXCLUDED.erp_belegart_code, @@ -1175,6 +1184,7 @@ async fn upsert_delivery( snap_postal_code = EXCLUDED.snap_postal_code, snap_city = EXCLUDED.snap_city, snap_country = EXCLUDED.snap_country, + snap_addition = EXCLUDED.snap_addition, sort_order = EXCLUDED.sort_order, desired_time = EXCLUDED.desired_time, special_agreements = EXCLUDED.special_agreements, @@ -1199,6 +1209,7 @@ async fn upsert_delivery( .bind(delivery.special_agreements.as_deref()) .bind(delivery.prepaid_amount) .bind(payment_method_id) + .bind(delivery.delivery_address.addition.as_deref()) .fetch_one(&mut **tx) .await .map_err(db)?; diff --git a/crates/infrastructure/src/report/renderer.rs b/crates/infrastructure/src/report/renderer.rs index 62abe4d..30cee33 100644 --- a/crates/infrastructure/src/report/renderer.rs +++ b/crates/infrastructure/src/report/renderer.rs @@ -482,11 +482,17 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer { ); p.kv("Besteller", &format!("{} (Nr. {})", d.customer_name, d.customer_number)); p.kv("Adresse Besteller", &r.orderer_address); + if let Some(z) = &r.orderer_address_addition { + p.kv("Zusatz Besteller", z); + } if !r.orderer_phones.is_empty() { p.kv("Tel. Besteller", &r.orderer_phones.join(" · ")); } p.kv("Empfänger", &r.name); p.kv("Lieferadresse", &d.address); + if let Some(z) = &d.address_addition { + p.kv("Zusatz", z); + } if !r.phones.is_empty() { p.kv("Tel. Empfänger", &r.phones.join(" · ")); } @@ -494,6 +500,9 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer { None => { p.kv("Kunde", &format!("{} (Nr. {})", d.customer_name, d.customer_number)); p.kv("Adresse", &d.address); + if let Some(z) = &d.address_addition { + p.kv("Zusatz", z); + } } } p.kv("Wunschzeit", opt(&d.desired_time)); diff --git a/crates/infrastructure/src/report/repository.rs b/crates/infrastructure/src/report/repository.rs index 650b8d2..d62b542 100644 --- a/crates/infrastructure/src/report/repository.rs +++ b/crates/infrastructure/src/report/repository.rs @@ -15,6 +15,7 @@ use holzleitner_application::dto::{ }; use holzleitner_application::error::ApplicationError; use holzleitner_application::ports::DeliveryReportRepository; +use holzleitner_domain::Address; pub struct PgDeliveryReportRepository { pool: PgPool, @@ -49,11 +50,13 @@ struct HeadRow { cust_postal_code: String, cust_city: String, cust_country: String, + cust_addition: Option, snap_street: Option, snap_house_number: Option, snap_postal_code: Option, snap_city: Option, snap_country: Option, + snap_addition: Option, desired_time: Option, special_agreements: Option, prepaid_amount: f64, @@ -185,32 +188,6 @@ struct AttachmentRow { uploaded_by: i64, } -fn one_line_address( - street: Option, - house: Option, - plz: Option, - city: Option, - country: Option, -) -> String { - let line1 = [street, house] - .into_iter() - .flatten() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(" "); - let line2 = [plz, city] - .into_iter() - .flatten() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(" "); - [line1, line2, country.unwrap_or_default()] - .into_iter() - .filter(|s| !s.trim().is_empty()) - .collect::>() - .join(", ") -} - #[async_trait] impl DeliveryReportRepository for PgDeliveryReportRepository { async fn load( @@ -229,9 +206,9 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { c.erp_customer_id, c.name AS customer_name, c.street AS cust_street, c.house_number AS cust_house_number, c.postal_code AS cust_postal_code, c.city AS cust_city, - c.country AS cust_country, + c.country AS cust_country, c.address_addition AS cust_addition, d.snap_street, d.snap_house_number, d.snap_postal_code, - d.snap_city, d.snap_country, + d.snap_city, d.snap_country, d.snap_addition, d.desired_time, d.special_agreements, d.prepaid_amount FROM deliveries d JOIN tours t ON t.id = d.tour_id @@ -432,19 +409,21 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { }) .unwrap_or(0); - let snap = AddressParts { + let snap = Address { street: head.snap_street.unwrap_or_default(), house_number: head.snap_house_number.unwrap_or_default(), postal_code: head.snap_postal_code.unwrap_or_default(), city: head.snap_city.unwrap_or_default(), country: head.snap_country.unwrap_or_default(), + addition: head.snap_addition, }; - let orderer = AddressParts { + let orderer = Address { street: head.cust_street, house_number: head.cust_house_number, postal_code: head.cust_postal_code, city: head.cust_city, country: head.cust_country, + addition: head.cust_addition, }; let different_recipient = different_recipient( &head.customer_name, @@ -454,6 +433,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { sources.iter().find(|s| s.role == "delivery"), ); let address = snap.one_line(); + let address_addition = snap.addition.clone(); Ok(Some(DeliveryReportData { generated_at: Utc::now(), @@ -470,6 +450,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { customer_number: head.erp_customer_id, customer_name: head.customer_name, address, + address_addition, different_recipient, desired_time: head.desired_time, special_agreements: head.special_agreements, @@ -597,34 +578,13 @@ impl DeliveryReportRepository for PgDeliveryReportRepository { } } -/// Anschrift in Einzelteilen, wie sie am Kunden bzw. als Beleg-Snapshot liegt. -struct AddressParts { - street: String, - house_number: String, - postal_code: String, - city: String, - country: String, -} - -impl AddressParts { - fn one_line(&self) -> String { - one_line_address( - Some(self.street.clone()), - Some(self.house_number.clone()), - Some(self.postal_code.clone()), - Some(self.city.clone()), - Some(self.country.clone()), - ) - } - - /// Gleiche Anschrift, unabhängig von Groß-/Kleinschreibung und - /// Leerzeichen. Das Land zählt nicht (im ERP oft uneinheitlich gepflegt). - fn same_place(&self, other: &Self) -> bool { - normalized(&self.street) == normalized(&other.street) - && normalized(&self.house_number) == normalized(&other.house_number) - && normalized(&self.postal_code) == normalized(&other.postal_code) - && normalized(&self.city) == normalized(&other.city) - } +/// Gleiche Anschrift, unabhängig von Groß-/Kleinschreibung und Leerzeichen. +/// Land und Zusatz zählen nicht (im ERP uneinheitlich gepflegt). +fn same_place(a: &Address, b: &Address) -> bool { + normalized(&a.street) == normalized(&b.street) + && normalized(&a.house_number) == normalized(&b.house_number) + && normalized(&a.postal_code) == normalized(&b.postal_code) + && normalized(&a.city) == normalized(&b.city) } fn normalized(value: &str) -> String { @@ -637,8 +597,8 @@ fn normalized(value: &str) -> String { /// Kundennamen). fn different_recipient( customer_name: &str, - orderer: &AddressParts, - delivery_address: &AddressParts, + orderer: &Address, + delivery_address: &Address, header: Option<&ContactSourceRow>, delivery: Option<&ContactSourceRow>, ) -> Option { @@ -647,7 +607,7 @@ fn different_recipient( .unwrap_or_else(|| customer_name.to_string()); let recipient_name = delivery.and_then(ContactSourceRow::full_name); - let address_differs = !orderer.same_place(delivery_address); + let address_differs = !same_place(orderer, delivery_address); let name_differs = recipient_name .as_deref() .is_some_and(|n| normalized(n) != normalized(&orderer_name)); @@ -659,6 +619,7 @@ fn different_recipient( name: recipient_name.unwrap_or_else(|| customer_name.to_string()), phones: delivery.map(|s| s.phones.clone()).unwrap_or_default(), orderer_address: orderer.one_line(), + orderer_address_addition: orderer.addition.clone(), orderer_phones: header.map(|s| s.phones.clone()).unwrap_or_default(), }) } @@ -667,13 +628,14 @@ fn different_recipient( mod different_recipient_tests { use super::*; - fn addr(street: &str, city: &str) -> AddressParts { - AddressParts { + fn addr(street: &str, city: &str) -> Address { + Address { street: street.into(), house_number: "1".into(), postal_code: "41460".into(), city: city.into(), - country: "DE".into(), + country: "Deutschland".into(), + addition: None, } } @@ -714,7 +676,7 @@ mod different_recipient_tests { ) .expect("abweichend"); assert_eq!(r.name, "Musterfrau Erika"); - assert_eq!(r.orderer_address, "Am Sandberg 1, 41460 Neuss, DE"); + assert_eq!(r.orderer_address, "Am Sandberg 1, 41460 Neuss"); assert_eq!(r.phones, vec!["0151 1".to_string()]); } diff --git a/migrations/0034_address_addition.sql b/migrations/0034_address_addition.sql new file mode 100644 index 0000000..5d30546 --- /dev/null +++ b/migrations/0034_address_addition.sql @@ -0,0 +1,32 @@ +-- Adresszusatz getrennt vom Land. +-- +-- Das ERP-Textfeld `Adressen.Land` wird von der Sachbearbeitung praktisch +-- nie für das Land genutzt, sondern für Ortsteil, Etage oder Hinweise +-- („Holzheim", „3 Etage", „Parterre"). Bisher landete es in `country` und +-- erschien im Lieferbericht als vermeintliches Land. Ab jetzt: +-- * `*_addition` = dieser freie Zusatz, +-- * `country` = echtes Land aus dem ERP-Länderstamm (`LandID`). +-- +-- Bestandsdaten: der bisherige `country`-Text war immer dieses Freitextfeld +-- → in den Zusatz verschieben. Reine Deutschland-Kürzel („D", „DEU", …) +-- sind kein Zusatz und werden verworfen. Der nächste Sync setzt `country` +-- wieder korrekt aus dem Länderstamm. + +ALTER TABLE customers ADD COLUMN address_addition TEXT; +ALTER TABLE deliveries ADD COLUMN snap_addition TEXT; + +UPDATE customers + SET address_addition = CASE + WHEN lower(btrim(country)) IN ('', 'd', 'de', 'deu', 'deutschland', 'germany', 'brd') + THEN NULL + ELSE btrim(country) + END, + country = ''; + +UPDATE deliveries + SET snap_addition = CASE + WHEN lower(btrim(snap_country)) IN ('', 'd', 'de', 'deu', 'deutschland', 'germany', 'brd') + THEN NULL + ELSE btrim(snap_country) + END, + snap_country = '';