feat(report): Zahlungsabwicklung im Lieferbericht dokumentieren
- Abschnitt "Zahlung": Zahlungsart mit Ergebnis (erhalten / bestaetigt), Zahlbetrag, Zeitpunkt, erfassender Fahrer und Fahrzeug der beim Abschluss gueltigen Zahlung (vor dem Abschluss: juengster Eintrag). Abschluesse ohne Zahlungs-Step behalten die bisherige Zeile "Betrag erhalten" - Neuer Abschnitt "Protokoll - Zahlungsabwicklung" mit allen Eintraegen inkl. Korrekturen; Status "beim Abschluss" / "aktuell" / "ersetzt" - Report-Daten: ReportPayment + ReportCompletion.payment_id Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@ -7,6 +7,7 @@
|
||||
//! damit der Renderer rein (ohne IO) bleibt.
|
||||
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DeliveryReportData {
|
||||
@ -39,6 +40,8 @@ pub struct DeliveryReportData {
|
||||
pub completion: Option<ReportCompletion>,
|
||||
pub scan_audit: Vec<ReportScanAudit>,
|
||||
pub credit_audit: Vec<ReportCreditAudit>,
|
||||
/// Protokoll der Zahlungsabwicklung, chronologisch (inkl. Korrekturen).
|
||||
pub payments: Vec<ReportPayment>,
|
||||
pub attachments: Vec<ReportAttachment>,
|
||||
// Bild-Bytes (vom Use Case aus dem lokalen Speicher nachgeladen):
|
||||
pub customer_signature_png: Option<Vec<u8>>,
|
||||
@ -105,6 +108,23 @@ pub struct ReportCompletion {
|
||||
pub payment_collected: bool,
|
||||
/// Snapshot des kassierten offenen Betrags in Cent (None = kein Inkasso).
|
||||
pub collected_amount_cents: Option<i64>,
|
||||
/// Die beim Abschluss gültige protokollierte Zahlung (None = ohne
|
||||
/// Zahlungs-Step abgeschlossen, z. B. nichts offen oder alte App).
|
||||
pub payment_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
/// Ein Eintrag im Zahlungsprotokoll („Abkassieren").
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReportPayment {
|
||||
pub id: Uuid,
|
||||
pub recorded_at: DateTime<Utc>,
|
||||
/// Anzeigename der Methode aus den Stammdaten (None = nicht mehr vorhanden).
|
||||
pub method_name: Option<String>,
|
||||
/// Code-Snapshot zum Zeitpunkt der Abwicklung (cash/ec_card/invoice).
|
||||
pub method_code: String,
|
||||
pub amount_cents: i64,
|
||||
pub recorded_by_personalnummer: i64,
|
||||
pub car_plate: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@ -32,7 +32,7 @@ pub use car::{
|
||||
pub use complete::CompleteDeliveryAcknowledgements;
|
||||
pub use credit::{CreditAction, DeliveryCreditEventRequest, DeliveryCreditResponse};
|
||||
pub use delivery_report::{
|
||||
DeliveryReportData, ReportAttachment, ReportCompletion, ReportContact, ReportCreditAudit,
|
||||
DeliveryReportData, ReportAttachment, ReportCompletion, ReportContact, ReportCreditAudit, ReportPayment,
|
||||
ReportItem, ReportNote, ReportScanAudit, ReportService,
|
||||
};
|
||||
pub use delivery_action::{CancelDeliveryRequest, DeliveryResponse, HoldDeliveryRequest};
|
||||
|
||||
@ -10,7 +10,7 @@ use printpdf::{
|
||||
IndirectFontRef, Line, Mm, PdfDocument, PdfDocumentReference, PdfLayerReference, Point, Px, Rgb,
|
||||
};
|
||||
|
||||
use holzleitner_application::dto::DeliveryReportData;
|
||||
use holzleitner_application::dto::{DeliveryReportData, ReportPayment};
|
||||
use holzleitner_application::error::ApplicationError;
|
||||
use holzleitner_application::ports::DeliveryReportRenderer;
|
||||
|
||||
@ -399,6 +399,32 @@ fn scan_action_de(s: &str) -> &str {
|
||||
}
|
||||
|
||||
/// Deutsche Beschriftung der Gutschriftaktion.
|
||||
/// Bar/EC = vor Ort kassiert; alles andere (Rechnung) bleibt offen.
|
||||
fn is_collected_on_site(code: &str) -> bool {
|
||||
code == "cash" || code == "ec_card"
|
||||
}
|
||||
|
||||
/// Anzeigename der Zahlungsart: Stammdaten-Name, sonst aus dem Code-Snapshot.
|
||||
fn payment_method_label(pay: &ReportPayment) -> String {
|
||||
if let Some(name) = pay.method_name.as_deref().filter(|n| !n.trim().is_empty()) {
|
||||
return name.to_string();
|
||||
}
|
||||
match pay.method_code.as_str() {
|
||||
"cash" => "Bar".into(),
|
||||
"ec_card" => "EC-Karte".into(),
|
||||
"invoice" => "Rechnung".into(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// „Fahrer 423, Fahrzeug AB-CD 12" (Fahrzeug nur, wenn bekannt).
|
||||
fn payment_actor(pay: &ReportPayment) -> String {
|
||||
match &pay.car_plate {
|
||||
Some(plate) => format!("Fahrer {}, Fahrzeug {}", pay.recorded_by_personalnummer, plate),
|
||||
None => format!("Fahrer {}", pay.recorded_by_personalnummer),
|
||||
}
|
||||
}
|
||||
|
||||
fn credit_action_de(s: &str) -> &str {
|
||||
match s {
|
||||
"set" => "Gesetzt",
|
||||
@ -513,9 +539,32 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer {
|
||||
p.kv("Gutschrift", ¢s(d.current_credit_cents));
|
||||
p.kv("Offener Betrag", &money(open));
|
||||
p.kv("Zahlungsmethode", opt(&d.payment_method));
|
||||
// Inkasso-Bestätigung: nur wenn beim Abschluss tatsächlich kassiert
|
||||
// wurde (Bar/EC bei offenem Betrag). „Auf Rechnung"/voll bezahlt → keine.
|
||||
if let Some(c) = &d.completion {
|
||||
// Zahlungsabwicklung aus dem Step „Zahlung": nach dem Abschluss die
|
||||
// dabei gültige Zahlung, vorher der jüngste Eintrag.
|
||||
let settled = match &d.completion {
|
||||
Some(c) => c
|
||||
.payment_id
|
||||
.and_then(|id| d.payments.iter().find(|pay| pay.id == id)),
|
||||
None => d.payments.last(),
|
||||
};
|
||||
if let Some(pay) = settled {
|
||||
let verb = if is_collected_on_site(&pay.method_code) {
|
||||
"erhalten"
|
||||
} else {
|
||||
// Name der Methode sagt bereits „Rechnung" — nicht doppeln.
|
||||
"bestätigt"
|
||||
};
|
||||
// Eigene Zeilen statt einer Satzzeile: Der Betrag steht so immer
|
||||
// am Zeilenende (die eingebaute Helvetica setzt nach „€" zu eng).
|
||||
p.kv(
|
||||
"Zahlungsabwicklung",
|
||||
&format!("{}, {}", payment_method_label(pay), verb),
|
||||
);
|
||||
p.kv("Zahlbetrag", ¢s(pay.amount_cents));
|
||||
p.kv("Abgewickelt am", &dt(self.tz, &pay.recorded_at));
|
||||
p.kv("Erfasst von", &payment_actor(pay));
|
||||
} else if let Some(c) = &d.completion {
|
||||
// Abschluss ohne Zahlungs-Step (ältere App): nur das Inkasso-Flag.
|
||||
if let Some(collected) = c.collected_amount_cents {
|
||||
p.kv(
|
||||
"Betrag erhalten",
|
||||
@ -676,6 +725,43 @@ impl DeliveryReportRenderer for PdfDeliveryReportRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
// 9b. Protokoll: Zahlungsabwicklung (alle Einträge inkl. Korrekturen)
|
||||
p.heading("Protokoll — Zahlungsabwicklung");
|
||||
if d.payments.is_empty() {
|
||||
p.text("— keine Einträge —", 9.0, false, 0.0);
|
||||
} else {
|
||||
p.row(
|
||||
&[
|
||||
("Zeit".to_string(), 30.0, true),
|
||||
("Zahlungsart".to_string(), 48.0, true),
|
||||
("Betrag".to_string(), 22.0, true),
|
||||
("Fahrer".to_string(), 16.0, true),
|
||||
("Fahrzeug".to_string(), 28.0, true),
|
||||
("Status".to_string(), 28.0, true),
|
||||
],
|
||||
7.5,
|
||||
);
|
||||
let current = settled.map(|pay| pay.id);
|
||||
for pay in &d.payments {
|
||||
let status = match (current == Some(pay.id), d.completion.is_some()) {
|
||||
(true, true) => "beim Abschluss",
|
||||
(true, false) => "aktuell",
|
||||
(false, _) => "ersetzt",
|
||||
};
|
||||
p.row(
|
||||
&[
|
||||
(dt(self.tz, &pay.recorded_at), 30.0, false),
|
||||
(payment_method_label(pay), 48.0, false),
|
||||
(cents(pay.amount_cents), 22.0, false),
|
||||
(pay.recorded_by_personalnummer.to_string(), 16.0, false),
|
||||
(pay.car_plate.clone().unwrap_or_else(|| "—".into()), 28.0, false),
|
||||
(status.to_string(), 28.0, false),
|
||||
],
|
||||
7.5,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Anhänge
|
||||
p.heading("Anhänge");
|
||||
if d.attachments.is_empty() {
|
||||
|
||||
@ -10,7 +10,7 @@ use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use holzleitner_application::dto::{
|
||||
DeliveryReportData, ReportAttachment, ReportCompletion, ReportContact, ReportCreditAudit,
|
||||
DeliveryReportData, ReportAttachment, ReportCompletion, ReportContact, ReportCreditAudit, ReportPayment,
|
||||
ReportItem, ReportNote, ReportScanAudit, ReportService,
|
||||
};
|
||||
use holzleitner_application::error::ApplicationError;
|
||||
@ -103,6 +103,18 @@ struct CompletionRow {
|
||||
driver_signature_path: String,
|
||||
payment_collected: bool,
|
||||
collected_amount_cents: Option<i64>,
|
||||
payment_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct PaymentRow {
|
||||
id: Uuid,
|
||||
recorded_at: DateTime<Utc>,
|
||||
method_name: Option<String>,
|
||||
method_code: String,
|
||||
amount_cents: i64,
|
||||
recorded_by_personalnummer: i64,
|
||||
car_plate: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
@ -277,7 +289,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository {
|
||||
r#"
|
||||
SELECT completed_at, completed_by_personalnummer, receipt_confirmed,
|
||||
notes_acknowledged, customer_signature_path, driver_signature_path,
|
||||
payment_collected, collected_amount_cents
|
||||
payment_collected, collected_amount_cents, payment_id
|
||||
FROM delivery_completions
|
||||
WHERE delivery_id = $1
|
||||
"#,
|
||||
@ -322,6 +334,24 @@ impl DeliveryReportRepository for PgDeliveryReportRepository {
|
||||
.await
|
||||
.map_err(db)?;
|
||||
|
||||
// --- Audit: Zahlungsabwicklung ---
|
||||
let payments: Vec<PaymentRow> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT p.id, p.recorded_at, pm.name AS method_name,
|
||||
p.payment_method_code AS method_code, p.amount_cents,
|
||||
p.recorded_by_personalnummer, c.plate AS car_plate
|
||||
FROM delivery_payments p
|
||||
LEFT JOIN payment_methods pm ON pm.id = p.payment_method_id
|
||||
LEFT JOIN cars c ON c.id = p.recorded_by_car_id
|
||||
WHERE p.delivery_id = $1
|
||||
ORDER BY p.recorded_at, p.id
|
||||
"#,
|
||||
)
|
||||
.bind(delivery_id)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(db)?;
|
||||
|
||||
// --- Anhänge ---
|
||||
let attachments: Vec<AttachmentRow> = sqlx::query_as(
|
||||
r#"
|
||||
@ -435,6 +465,7 @@ impl DeliveryReportRepository for PgDeliveryReportRepository {
|
||||
driver_signature_path: c.driver_signature_path,
|
||||
payment_collected: c.payment_collected,
|
||||
collected_amount_cents: c.collected_amount_cents,
|
||||
payment_id: c.payment_id,
|
||||
}),
|
||||
scan_audit: scan_audit
|
||||
.into_iter()
|
||||
@ -464,6 +495,18 @@ impl DeliveryReportRepository for PgDeliveryReportRepository {
|
||||
author_personalnummer: c.author_personalnummer,
|
||||
})
|
||||
.collect(),
|
||||
payments: payments
|
||||
.into_iter()
|
||||
.map(|p| ReportPayment {
|
||||
id: p.id,
|
||||
recorded_at: p.recorded_at,
|
||||
method_name: p.method_name,
|
||||
method_code: p.method_code,
|
||||
amount_cents: p.amount_cents,
|
||||
recorded_by_personalnummer: p.recorded_by_personalnummer,
|
||||
car_plate: p.car_plate,
|
||||
})
|
||||
.collect(),
|
||||
attachments: attachments
|
||||
.into_iter()
|
||||
.map(|a| ReportAttachment {
|
||||
|
||||
Reference in New Issue
Block a user