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#[serde(rename_all = "camelCase")]
31pub struct Invoice {
32 #[serde(flatten)]
34 pub meta: Bo4eMeta,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub invoice_number: Option<String>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub invoice_type: Option<InvoiceType>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub status: Option<InvoiceStatus>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub division: Option<Division>,
51
52 #[serde(skip_serializing_if = "Option::is_none")]
54 pub invoice_date: Option<NaiveDate>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub due_date: Option<NaiveDate>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
62 pub billing_period: Option<TimePeriod>,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub net_amount: Option<Amount>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub tax_amount: Option<Amount>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub gross_amount: Option<Amount>,
75
76 #[serde(default, skip_serializing_if = "Vec::is_empty")]
78 pub positions: Vec<InvoicePosition>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub recipient: Option<Box<super::BusinessPartner>>,
83}
84
85impl Bo4eObject for Invoice {
86 fn type_name_german() -> &'static str {
87 "Rechnung"
88 }
89
90 fn type_name_english() -> &'static str {
91 "Invoice"
92 }
93
94 fn meta(&self) -> &Bo4eMeta {
95 &self.meta
96 }
97
98 fn meta_mut(&mut self) -> &mut Bo4eMeta {
99 &mut self.meta
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn test_invoice_creation() {
109 let invoice = Invoice {
110 invoice_number: Some("RE-2024-001234".to_string()),
111 invoice_type: Some(InvoiceType::EndCustomerInvoice),
112 status: Some(InvoiceStatus::CheckedOk),
113 net_amount: Some(Amount::eur(1000.00)),
114 tax_amount: Some(Amount::eur(190.00)),
115 gross_amount: Some(Amount::eur(1190.00)),
116 ..Default::default()
117 };
118
119 assert_eq!(invoice.invoice_number, Some("RE-2024-001234".to_string()));
120 assert_eq!(invoice.status, Some(InvoiceStatus::CheckedOk));
121 }
122
123 #[test]
124 fn test_invoice_with_positions() {
125 let invoice = Invoice {
126 invoice_number: Some("RE-001".to_string()),
127 positions: vec![
128 InvoicePosition {
129 position_number: Some(1),
130 position_text: Some("Electricity consumption".to_string()),
131 total_price_value: Some(500.0),
132 ..Default::default()
133 },
134 InvoicePosition {
135 position_number: Some(2),
136 position_text: Some("Network fees".to_string()),
137 total_price_value: Some(100.0),
138 ..Default::default()
139 },
140 ],
141 ..Default::default()
142 };
143
144 assert_eq!(invoice.positions.len(), 2);
145 }
146
147 #[test]
148 fn test_serialize() {
149 let invoice = Invoice {
150 meta: Bo4eMeta::with_type("Rechnung"),
151 invoice_number: Some("RE-001".to_string()),
152 gross_amount: Some(Amount::eur(119.00)),
153 ..Default::default()
154 };
155
156 let json = serde_json::to_string(&invoice).unwrap();
157 assert!(json.contains(r#""invoiceNumber":"RE-001""#));
158 assert!(json.contains(r#""_typ":"Rechnung""#));
159 }
160
161 #[test]
162 fn test_roundtrip() {
163 let invoice = Invoice {
164 meta: Bo4eMeta::with_type("Rechnung"),
165 invoice_number: Some("RE-123".to_string()),
166 invoice_type: Some(InvoiceType::MonthlyInvoice),
167 status: Some(InvoiceStatus::Paid),
168 division: Some(Division::Electricity),
169 net_amount: Some(Amount::eur(100.0)),
170 ..Default::default()
171 };
172
173 let json = serde_json::to_string(&invoice).unwrap();
174 let parsed: Invoice = serde_json::from_str(&json).unwrap();
175 assert_eq!(invoice, parsed);
176 }
177
178 #[test]
179 fn test_bo4e_object_impl() {
180 assert_eq!(Invoice::type_name_german(), "Rechnung");
181 assert_eq!(Invoice::type_name_english(), "Invoice");
182 }
183}