1use crate::amount::{Amount, InvoiceAmount, UnitPriceAmount};
4use crate::attachment::Attachment;
5use crate::code::Code;
6use crate::date::Date;
7use crate::identifier::{DocumentReference, Identifier};
8use crate::kind::DocumentKind;
9use crate::numeric::{Percentage, Quantity};
10use crate::payment::PaymentMeans;
11use crate::profile::Profile;
12use crate::tax::{TaxCategory, TaxSystem};
13
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct PostalAddress {
17 pub line1: Option<String>,
19 pub line2: Option<String>,
21 pub line3: Option<String>,
23 pub city: Option<String>,
25 pub post_code: Option<String>,
27 pub subdivision: Option<String>,
29 pub country: Option<Code>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
35pub struct Contact {
36 pub point: Option<String>,
38 pub phone: Option<String>,
40 pub email: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PartyTax {
47 pub id: Identifier,
49 pub scheme: String,
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Party {
64 pub name: String,
66 pub trading_name: Option<String>,
68 pub identifiers: Vec<Identifier>,
70 pub legal_registration: Option<Identifier>,
72 pub vat_identifier: Option<Identifier>,
74 pub tax_registration: Option<Identifier>,
76 pub electronic_address: Option<Identifier>,
78 pub party_taxes: Vec<PartyTax>,
80 pub additional_legal: Option<String>,
82 pub address: Option<PostalAddress>,
84 pub contact: Option<Contact>,
86}
87
88impl Party {
89 pub fn new(name: impl Into<String>, country: impl Into<String>) -> Self {
91 let country = country.into();
92 let address = if country.trim().is_empty() {
93 None
94 } else {
95 Some(PostalAddress {
96 country: Some(Code::new(country)),
97 ..PostalAddress::default()
98 })
99 };
100 Self {
101 name: name.into(),
102 trading_name: None,
103 identifiers: Vec::new(),
104 legal_registration: None,
105 vat_identifier: None,
106 tax_registration: None,
107 electronic_address: None,
108 party_taxes: Vec::new(),
109 additional_legal: None,
110 address,
111 contact: None,
112 }
113 }
114
115 pub fn country(&self) -> &str {
117 self.address
118 .as_ref()
119 .and_then(|a| a.country.as_ref())
120 .map(Code::as_str)
121 .unwrap_or("")
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Price {
128 pub net: UnitPriceAmount,
130 pub discount: Option<UnitPriceAmount>,
132 pub gross: Option<UnitPriceAmount>,
134 pub base_qty: Option<Quantity>,
136 pub base_unit: Option<Code>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct LineAllowanceCharge {
144 pub amount: InvoiceAmount,
146 pub base: Option<InvoiceAmount>,
148 pub percent: Option<Percentage>,
150 pub reason: Option<String>,
152 pub reason_code: Option<Code>,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Line {
159 pub id: String,
161 pub name: String,
163 pub net: Amount,
165 pub tax: TaxCategory,
167 pub quantity: Option<Quantity>,
169 pub unit: Option<Code>,
171 pub price: Option<Price>,
173 pub note: Option<String>,
175 pub description: Option<String>,
177 pub period: Option<Period>,
179 pub allowances: Vec<LineAllowanceCharge>,
181 pub charges: Vec<LineAllowanceCharge>,
183 pub standard_id: Option<Identifier>,
185 pub item_id: Option<Identifier>,
187 pub buyer_id: Option<Identifier>,
189 pub order_line: Option<String>,
191 pub accounting_reference: Option<String>,
193 pub attributes: Vec<ItemAttribute>,
195 pub origin_country: Option<Code>,
197 pub classifications: Vec<Identifier>,
199 pub invoiced_object: Option<Identifier>,
201 pub invoiced_object_code: Option<Code>,
203 pub extra_tax: Vec<TaxCategory>,
205 pub tax_total: Option<InvoiceAmount>,
207}
208
209impl Line {
210 pub fn new(
212 id: impl Into<String>,
213 name: impl Into<String>,
214 net: Amount,
215 tax: TaxCategory,
216 ) -> Self {
217 Self {
218 id: id.into(),
219 name: name.into(),
220 net,
221 tax,
222 quantity: None,
223 unit: None,
224 price: None,
225 note: None,
226 description: None,
227 period: None,
228 allowances: vec![],
229 charges: vec![],
230 standard_id: None,
231 item_id: None,
232 buyer_id: None,
233 order_line: None,
234 accounting_reference: None,
235 attributes: vec![],
236 origin_country: None,
237 classifications: vec![],
238 invoiced_object: None,
239 invoiced_object_code: None,
240 extra_tax: vec![],
241 tax_total: None,
242 }
243 }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct ItemAttribute {
249 pub name: String,
251 pub value: String,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct InvoiceNote {
258 pub subject: Option<Code>,
260 pub text: String,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct PrecedingInvoice {
267 pub reference: DocumentReference,
269 pub issue_date: Option<Date>,
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct Period {
276 pub start: Option<Date>,
278 pub end: Option<Date>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct Payee {
285 pub name: String,
287 pub identifier: Option<Identifier>,
289 pub legal_registration: Option<Identifier>,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct TaxRepresentative {
296 pub name: String,
298 pub vat_identifier: Option<Identifier>,
300 pub address: Option<PostalAddress>,
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct Delivery {
307 pub name: Option<String>,
309 pub location_id: Option<Identifier>,
311 pub date: Option<Date>,
313 pub address: Option<PostalAddress>,
315}
316
317#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct PaymentInstructions {
321 pub means_code: Option<Code>,
323 pub means_text: Option<String>,
325 pub remittance: Option<String>,
327 pub means: Option<PaymentMeans>,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct AllowanceCharge {
334 pub amount: InvoiceAmount,
336 pub base: Option<InvoiceAmount>,
338 pub percent: Option<Percentage>,
340 pub reason: Option<String>,
342 pub reason_code: Option<Code>,
344 pub tax: Option<TaxCategory>,
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct TaxBreakdown {
351 pub system: TaxSystem,
353 pub scheme: String,
355 pub category: Code,
357 pub rate: Option<Percentage>,
359 pub taxable: InvoiceAmount,
361 pub tax: InvoiceAmount,
363 pub exemption_reason: Option<String>,
365 pub exemption_code: Option<Code>,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq, Default)]
371pub struct DocumentTotals {
372 pub line_net: Option<InvoiceAmount>,
374 pub allowance_total: Option<InvoiceAmount>,
376 pub charge_total: Option<InvoiceAmount>,
378 pub without_tax: Option<InvoiceAmount>,
380 pub tax_total: Option<InvoiceAmount>,
382 pub tax_total_accounting: Option<InvoiceAmount>,
384 pub with_tax: Option<InvoiceAmount>,
386 pub paid: Option<InvoiceAmount>,
388 pub rounding: Option<InvoiceAmount>,
390 pub payable: Option<InvoiceAmount>,
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct SupportingDocument {
397 pub id: DocumentReference,
399 pub description: Option<String>,
401 pub uri: Option<String>,
403 pub attachment: Option<Attachment>,
405}
406
407#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct Invoice {
412 pub profile: Profile,
414 pub specification_id: Option<String>,
416 pub kind: DocumentKind,
418 pub number: String,
420 pub currency: String,
422 pub issue_date: Option<Date>,
424 pub type_code: Option<Code>,
426 pub tax_currency: Option<Code>,
428 pub due_date: Option<Date>,
430 pub tax_point_date: Option<Date>,
432 pub tax_point_code: Option<Code>,
434 pub business_process: Option<String>,
436 pub buyer_reference: Option<DocumentReference>,
438 pub project: Option<DocumentReference>,
440 pub contract: Option<DocumentReference>,
442 pub purchase_order: Option<DocumentReference>,
444 pub sales_order: Option<DocumentReference>,
446 pub receiving_advice: Option<DocumentReference>,
448 pub despatch: Option<DocumentReference>,
450 pub tender: Option<DocumentReference>,
452 pub invoiced_object: Option<Identifier>,
454 pub buyer_accounting: Option<String>,
456 pub payment_terms: Option<String>,
458 pub notes: Vec<InvoiceNote>,
460 pub preceding: Vec<PrecedingInvoice>,
462 pub seller: Party,
464 pub buyer: Party,
466 pub payee: Option<Payee>,
468 pub tax_representative: Option<TaxRepresentative>,
470 pub delivery: Option<Delivery>,
472 pub period: Option<Period>,
474 pub payment: Option<PaymentInstructions>,
476 pub document_allowances: Vec<AllowanceCharge>,
478 pub document_charges: Vec<AllowanceCharge>,
480 pub tax_breakdown: Vec<TaxBreakdown>,
482 pub totals: Option<DocumentTotals>,
484 pub supporting_documents: Vec<SupportingDocument>,
486 pub lines: Vec<Line>,
488}
489
490impl Invoice {
491 pub fn blank(
493 profile: Profile,
494 number: impl Into<String>,
495 currency: impl Into<String>,
496 seller: Party,
497 buyer: Party,
498 ) -> Self {
499 Self {
500 profile,
501 specification_id: Some(profile.specification_id().into()),
502 kind: DocumentKind::Invoice,
503 number: number.into(),
504 currency: currency.into(),
505 issue_date: None,
506 type_code: None,
507 tax_currency: None,
508 due_date: None,
509 tax_point_date: None,
510 tax_point_code: None,
511 business_process: None,
512 buyer_reference: None,
513 project: None,
514 contract: None,
515 purchase_order: None,
516 sales_order: None,
517 receiving_advice: None,
518 despatch: None,
519 tender: None,
520 invoiced_object: None,
521 buyer_accounting: None,
522 payment_terms: None,
523 notes: vec![],
524 preceding: vec![],
525 seller,
526 buyer,
527 payee: None,
528 tax_representative: None,
529 delivery: None,
530 period: None,
531 payment: None,
532 document_allowances: vec![],
533 document_charges: vec![],
534 tax_breakdown: vec![],
535 totals: None,
536 supporting_documents: vec![],
537 lines: vec![],
538 }
539 }
540
541 pub fn payable(&self) -> Option<Amount> {
543 self.totals.as_ref().and_then(|t| t.payable)
544 }
545
546 pub fn tax_total(&self) -> Option<Amount> {
548 self.totals.as_ref().and_then(|t| t.tax_total)
549 }
550
551 pub fn stamp_profile(&mut self, profile: Profile) {
555 self.profile = profile;
556 if profile == Profile::Unknown {
557 return;
558 }
559 self.specification_id = Some(profile.specification_id().into());
560 if let Some(bt23) = profile.process_id() {
561 self.business_process = Some(bt23.into());
562 }
563 }
564
565 pub fn line_net_sum(&self) -> Option<Amount> {
567 self.lines
568 .iter()
569 .try_fold(Amount::ZERO, |acc, line| acc.checked_add(line.net))
570 }
571
572 pub fn to_credit_note(&self, new_number: impl Into<String>, new_issue_date: Date) -> Self {
574 let mut next = self.clone();
575 next.kind = DocumentKind::CreditNote;
576 next.type_code = Some(Code::new("381"));
577 next.preceding = vec![PrecedingInvoice {
578 reference: DocumentReference::new(self.number.clone()),
579 issue_date: self.issue_date,
580 }];
581 next.number = new_number.into();
582 next.issue_date = Some(new_issue_date);
583 next
584 }
585}
586
587#[cfg(test)]
588mod tests {
589 use super::*;
590 use crate::identifier::Identifier;
591 use crate::tax::TaxCategory;
592 use rust_decimal::Decimal;
593
594 #[test]
595 fn credit_note_does_not_negate() {
596 let mut inv = Invoice::blank(
597 Profile::En16931,
598 "INV-1",
599 "EUR",
600 Party::new("S", "DE"),
601 Party::new("B", "FR"),
602 );
603 inv.issue_date = Date::parse("2026-01-15").ok();
604 inv.type_code = Some(Code::new("380"));
605 inv.lines = vec![Line::new(
606 "1",
607 "A",
608 Amount::parse("100.00").unwrap(),
609 TaxCategory::vat("S", Decimal::from(19)),
610 )];
611 let _ = crate::reconcile::reconcile(&mut inv);
612 let cn = inv.to_credit_note("CN-1", Date::parse("2026-01-16").unwrap());
613 assert_eq!(cn.kind, DocumentKind::CreditNote);
614 assert_eq!(cn.payable(), inv.payable());
615 assert_eq!(cn.preceding[0].reference.as_str(), "INV-1");
616 assert_eq!(cn.type_code.as_ref().map(Code::as_str), Some("381"));
617 }
618
619 #[test]
620 fn table2_refs_and_line_groups_exist() {
621 let mut inv = Invoice::blank(
622 Profile::En16931,
623 "INV-1",
624 "EUR",
625 Party::new("S", "DE"),
626 Party::new("B", "FR"),
627 );
628 inv.tax_point_date = Date::parse("2026-01-10").ok();
629 inv.tax_point_code = Some(Code::new("3"));
630 inv.purchase_order = Some(DocumentReference::new("PO-9"));
631 inv.invoiced_object = Some(Identifier::new("OBJ-1"));
632 inv.lines.push({
633 let mut line = Line::new(
634 "1",
635 "A",
636 Amount::parse("90.00").unwrap(),
637 TaxCategory::vat("S", Decimal::from(19)),
638 );
639 line.period = Some(Period {
640 start: Date::parse("2026-01-01").ok(),
641 end: Date::parse("2026-01-31").ok(),
642 });
643 line.allowances.push(LineAllowanceCharge {
644 amount: Amount::parse("10.00").unwrap(),
645 base: None,
646 percent: None,
647 reason: Some("discount".into()),
648 reason_code: None,
649 });
650 line.standard_id = Some(Identifier::schemed("01234567890128", "0160"));
651 line.origin_country = Some(Code::new("DE"));
652 line
653 });
654 assert_eq!(inv.purchase_order.as_ref().unwrap().as_str(), "PO-9");
655 assert_eq!(inv.lines[0].allowances.len(), 1);
656 assert_eq!(inv.lines[0].origin_country.as_ref().unwrap().as_str(), "DE");
657 assert!(inv.invoiced_object.is_some());
658 }
659}