1use chrono::NaiveDate;
4use serde::{Deserialize, Serialize};
5
6use crate::com::{Amount, InvoicePosition, TimePeriod};
7use crate::enums::{Division, InvoiceStatus, InvoiceType};
8use crate::traits::{Bo4eMeta, Bo4eObject};
9
10#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
30#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "json-schema", schemars(rename = "Rechnung"))]
32#[serde(rename_all = "camelCase")]
33pub struct Invoice {
34 #[serde(flatten)]
36 pub meta: Bo4eMeta,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
40 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsnummer"))]
41 pub invoice_number: Option<String>,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
45 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungstyp"))]
46 pub invoice_type: Option<InvoiceType>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsstatus"))]
51 pub status: Option<InvoiceStatus>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 #[cfg_attr(feature = "json-schema", schemars(rename = "sparte"))]
56 pub division: Option<Division>,
57
58 #[serde(skip_serializing_if = "Option::is_none")]
60 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsdatum"))]
61 pub invoice_date: Option<NaiveDate>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
65 #[cfg_attr(feature = "json-schema", schemars(rename = "faelligkeitsdatum"))]
66 pub due_date: Option<NaiveDate>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 #[cfg_attr(feature = "json-schema", schemars(rename = "abrechnungszeitraum"))]
71 pub billing_period: Option<TimePeriod>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 #[cfg_attr(feature = "json-schema", schemars(rename = "nettobetrag"))]
76 pub net_amount: Option<Amount>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
80 #[cfg_attr(feature = "json-schema", schemars(rename = "steuerbetrag"))]
81 pub tax_amount: Option<Amount>,
82
83 #[serde(skip_serializing_if = "Option::is_none")]
85 #[cfg_attr(feature = "json-schema", schemars(rename = "bruttobetrag"))]
86 pub gross_amount: Option<Amount>,
87
88 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungspositionen"))]
91 pub positions: Vec<InvoicePosition>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
95 #[cfg_attr(feature = "json-schema", schemars(rename = "rechnungsempfaenger"))]
96 pub recipient: Option<Box<super::BusinessPartner>>,
97}
98
99impl Bo4eObject for Invoice {
100 fn type_name_german() -> &'static str {
101 "Rechnung"
102 }
103
104 fn type_name_english() -> &'static str {
105 "Invoice"
106 }
107
108 fn meta(&self) -> &Bo4eMeta {
109 &self.meta
110 }
111
112 fn meta_mut(&mut self) -> &mut Bo4eMeta {
113 &mut self.meta
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_invoice_creation() {
123 let invoice = Invoice {
124 invoice_number: Some("RE-2024-001234".to_string()),
125 invoice_type: Some(InvoiceType::EndCustomerInvoice),
126 status: Some(InvoiceStatus::CheckedOk),
127 net_amount: Some(Amount::eur(1000.00)),
128 tax_amount: Some(Amount::eur(190.00)),
129 gross_amount: Some(Amount::eur(1190.00)),
130 ..Default::default()
131 };
132
133 assert_eq!(invoice.invoice_number, Some("RE-2024-001234".to_string()));
134 assert_eq!(invoice.status, Some(InvoiceStatus::CheckedOk));
135 }
136
137 #[test]
138 fn test_invoice_with_positions() {
139 let invoice = Invoice {
140 invoice_number: Some("RE-001".to_string()),
141 positions: vec![
142 InvoicePosition {
143 position_number: Some(1),
144 position_text: Some("Electricity consumption".to_string()),
145 total_price_value: Some(500.0),
146 ..Default::default()
147 },
148 InvoicePosition {
149 position_number: Some(2),
150 position_text: Some("Network fees".to_string()),
151 total_price_value: Some(100.0),
152 ..Default::default()
153 },
154 ],
155 ..Default::default()
156 };
157
158 assert_eq!(invoice.positions.len(), 2);
159 }
160
161 #[test]
162 fn test_serialize() {
163 let invoice = Invoice {
164 meta: Bo4eMeta::with_type("Rechnung"),
165 invoice_number: Some("RE-001".to_string()),
166 gross_amount: Some(Amount::eur(119.00)),
167 ..Default::default()
168 };
169
170 let json = serde_json::to_string(&invoice).unwrap();
171 assert!(json.contains(r#""invoiceNumber":"RE-001""#));
172 assert!(json.contains(r#""_typ":"Rechnung""#));
173 }
174
175 #[test]
176 fn test_roundtrip() {
177 let invoice = Invoice {
178 meta: Bo4eMeta::with_type("Rechnung"),
179 invoice_number: Some("RE-123".to_string()),
180 invoice_type: Some(InvoiceType::MonthlyInvoice),
181 status: Some(InvoiceStatus::Paid),
182 division: Some(Division::Electricity),
183 net_amount: Some(Amount::eur(100.0)),
184 ..Default::default()
185 };
186
187 let json = serde_json::to_string(&invoice).unwrap();
188 let parsed: Invoice = serde_json::from_str(&json).unwrap();
189 assert_eq!(invoice, parsed);
190 }
191
192 #[test]
193 fn test_bo4e_object_impl() {
194 assert_eq!(Invoice::type_name_german(), "Rechnung");
195 assert_eq!(Invoice::type_name_english(), "Invoice");
196 }
197}