1use crate::amount::{Amount, InvoiceAmount, UnitPriceAmount};
2use crate::attachment::Attachment;
3use crate::code::Code;
4use crate::date::Date;
5use crate::identifier::{DocumentReference, Identifier};
6use crate::kind::DocumentKind;
7use crate::numeric::{Percentage, Quantity};
8use crate::payment::PaymentMeans;
9use crate::profile::Profile;
10use crate::tax::{TaxCategory, TaxSystem};
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
14pub struct PostalAddress {
15 pub line1: Option<String>,
17 pub line2: Option<String>,
19 pub line3: Option<String>,
21 pub city: Option<String>,
23 pub post_code: Option<String>,
25 pub subdivision: Option<String>,
27 pub country: Option<Code>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
33pub struct Contact {
34 pub point: Option<String>,
36 pub phone: Option<String>,
38 pub email: Option<String>,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct PartyTax {
45 pub id: Identifier,
46 pub scheme: String,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct Party {
60 pub name: String,
61 pub trading_name: Option<String>,
62 pub identifiers: Vec<Identifier>,
63 pub legal_registration: Option<Identifier>,
64 pub vat_identifier: Option<Identifier>,
65 pub tax_registration: Option<Identifier>,
66 pub electronic_address: Option<Identifier>,
67 pub party_taxes: Vec<PartyTax>,
68 pub additional_legal: Option<String>,
69 pub address: Option<PostalAddress>,
70 pub contact: Option<Contact>,
71}
72
73impl Party {
74 pub fn new(name: impl Into<String>, country: impl Into<String>) -> Self {
75 let country = country.into();
76 let address = if country.trim().is_empty() {
77 None
78 } else {
79 Some(PostalAddress {
80 country: Some(Code::new(country)),
81 ..PostalAddress::default()
82 })
83 };
84 Self {
85 name: name.into(),
86 trading_name: None,
87 identifiers: Vec::new(),
88 legal_registration: None,
89 vat_identifier: None,
90 tax_registration: None,
91 electronic_address: None,
92 party_taxes: Vec::new(),
93 additional_legal: None,
94 address,
95 contact: None,
96 }
97 }
98
99 pub fn country(&self) -> &str {
101 self.address
102 .as_ref()
103 .and_then(|a| a.country.as_ref())
104 .map(Code::as_str)
105 .unwrap_or("")
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct Price {
112 pub net: UnitPriceAmount,
114 pub discount: Option<UnitPriceAmount>,
116 pub gross: Option<UnitPriceAmount>,
118 pub base_qty: Option<Quantity>,
120 pub base_unit: Option<Code>,
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct LineAllowanceCharge {
128 pub amount: InvoiceAmount,
130 pub base: Option<InvoiceAmount>,
132 pub percent: Option<Percentage>,
134 pub reason: Option<String>,
136 pub reason_code: Option<Code>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct Line {
142 pub id: String,
144 pub name: String,
146 pub net: Amount,
148 pub tax: TaxCategory,
150 pub quantity: Option<Quantity>,
152 pub unit: Option<Code>,
154 pub price: Option<Price>,
156 pub note: Option<String>,
158 pub description: Option<String>,
160 pub period: Option<Period>,
162 pub allowances: Vec<LineAllowanceCharge>,
163 pub charges: Vec<LineAllowanceCharge>,
164 pub standard_id: Option<Identifier>,
166 pub item_id: Option<Identifier>,
168 pub buyer_id: Option<Identifier>,
170 pub order_line: Option<String>,
172 pub accounting_reference: Option<String>,
174 pub attributes: Vec<ItemAttribute>,
176 pub origin_country: Option<Code>,
178 pub classifications: Vec<Identifier>,
180 pub invoiced_object: Option<Identifier>,
182 pub invoiced_object_code: Option<Code>,
184 pub extra_tax: Vec<TaxCategory>,
186 pub tax_total: Option<InvoiceAmount>,
188}
189
190impl Line {
191 pub fn new(
192 id: impl Into<String>,
193 name: impl Into<String>,
194 net: Amount,
195 tax: TaxCategory,
196 ) -> Self {
197 Self {
198 id: id.into(),
199 name: name.into(),
200 net,
201 tax,
202 quantity: None,
203 unit: None,
204 price: None,
205 note: None,
206 description: None,
207 period: None,
208 allowances: vec![],
209 charges: vec![],
210 standard_id: None,
211 item_id: None,
212 buyer_id: None,
213 order_line: None,
214 accounting_reference: None,
215 attributes: vec![],
216 origin_country: None,
217 classifications: vec![],
218 invoiced_object: None,
219 invoiced_object_code: None,
220 extra_tax: vec![],
221 tax_total: None,
222 }
223 }
224}
225
226#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct ItemAttribute {
229 pub name: String,
230 pub value: String,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub struct InvoiceNote {
236 pub subject: Option<Code>,
238 pub text: String,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq)]
244pub struct PrecedingInvoice {
245 pub reference: DocumentReference,
247 pub issue_date: Option<Date>,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct Period {
254 pub start: Option<Date>,
256 pub end: Option<Date>,
258}
259
260#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct Payee {
263 pub name: String,
265 pub identifier: Option<Identifier>,
267 pub legal_registration: Option<Identifier>,
269}
270
271#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct TaxRepresentative {
274 pub name: String,
276 pub vat_identifier: Option<Identifier>,
278 pub address: Option<PostalAddress>,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct Delivery {
285 pub name: Option<String>,
287 pub location_id: Option<Identifier>,
289 pub date: Option<Date>,
291 pub address: Option<PostalAddress>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct PaymentInstructions {
299 pub means_code: Option<Code>,
301 pub means_text: Option<String>,
303 pub remittance: Option<String>,
305 pub means: Option<PaymentMeans>,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct AllowanceCharge {
311 pub amount: InvoiceAmount,
313 pub base: Option<InvoiceAmount>,
315 pub percent: Option<Percentage>,
317 pub reason: Option<String>,
319 pub reason_code: Option<Code>,
321 pub tax: Option<TaxCategory>,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
327pub struct TaxBreakdown {
328 pub system: TaxSystem,
329 pub scheme: String,
330 pub category: Code,
332 pub rate: Option<Percentage>,
334 pub taxable: InvoiceAmount,
336 pub tax: InvoiceAmount,
338 pub exemption_reason: Option<String>,
340 pub exemption_code: Option<Code>,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq, Default)]
346pub struct DocumentTotals {
347 pub line_net: Option<InvoiceAmount>,
349 pub allowance_total: Option<InvoiceAmount>,
351 pub charge_total: Option<InvoiceAmount>,
353 pub without_tax: Option<InvoiceAmount>,
355 pub tax_total: Option<InvoiceAmount>,
357 pub tax_total_accounting: Option<InvoiceAmount>,
359 pub with_tax: Option<InvoiceAmount>,
361 pub paid: Option<InvoiceAmount>,
363 pub rounding: Option<InvoiceAmount>,
365 pub payable: Option<InvoiceAmount>,
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct SupportingDocument {
372 pub id: DocumentReference,
374 pub description: Option<String>,
376 pub uri: Option<String>,
378 pub attachment: Option<Attachment>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
386pub struct Invoice {
387 pub profile: Profile,
388 pub specification_id: Option<String>,
390 pub kind: DocumentKind,
392 pub number: String,
394 pub currency: String,
396 pub issue_date: Option<Date>,
398 pub type_code: Option<Code>,
400 pub tax_currency: Option<Code>,
402 pub due_date: Option<Date>,
404 pub tax_point_date: Option<Date>,
406 pub tax_point_code: Option<Code>,
408 pub business_process: Option<String>,
410 pub buyer_reference: Option<DocumentReference>,
412 pub project: Option<DocumentReference>,
414 pub contract: Option<DocumentReference>,
416 pub purchase_order: Option<DocumentReference>,
418 pub sales_order: Option<DocumentReference>,
420 pub receiving_advice: Option<DocumentReference>,
422 pub despatch: Option<DocumentReference>,
424 pub tender: Option<DocumentReference>,
426 pub invoiced_object: Option<Identifier>,
428 pub buyer_accounting: Option<String>,
430 pub payment_terms: Option<String>,
432 pub notes: Vec<InvoiceNote>,
434 pub preceding: Vec<PrecedingInvoice>,
436 pub seller: Party,
438 pub buyer: Party,
440 pub payee: Option<Payee>,
442 pub tax_representative: Option<TaxRepresentative>,
444 pub delivery: Option<Delivery>,
446 pub period: Option<Period>,
448 pub payment: Option<PaymentInstructions>,
450 pub document_allowances: Vec<AllowanceCharge>,
452 pub document_charges: Vec<AllowanceCharge>,
454 pub tax_breakdown: Vec<TaxBreakdown>,
456 pub totals: Option<DocumentTotals>,
458 pub supporting_documents: Vec<SupportingDocument>,
460 pub lines: Vec<Line>,
462}
463
464impl Invoice {
465 pub fn blank(
466 profile: Profile,
467 number: impl Into<String>,
468 currency: impl Into<String>,
469 seller: Party,
470 buyer: Party,
471 ) -> Self {
472 Self {
473 profile,
474 specification_id: Some(profile.specification_id().into()),
475 kind: DocumentKind::Invoice,
476 number: number.into(),
477 currency: currency.into(),
478 issue_date: None,
479 type_code: None,
480 tax_currency: None,
481 due_date: None,
482 tax_point_date: None,
483 tax_point_code: None,
484 business_process: None,
485 buyer_reference: None,
486 project: None,
487 contract: None,
488 purchase_order: None,
489 sales_order: None,
490 receiving_advice: None,
491 despatch: None,
492 tender: None,
493 invoiced_object: None,
494 buyer_accounting: None,
495 payment_terms: None,
496 notes: vec![],
497 preceding: vec![],
498 seller,
499 buyer,
500 payee: None,
501 tax_representative: None,
502 delivery: None,
503 period: None,
504 payment: None,
505 document_allowances: vec![],
506 document_charges: vec![],
507 tax_breakdown: vec![],
508 totals: None,
509 supporting_documents: vec![],
510 lines: vec![],
511 }
512 }
513
514 pub fn payable(&self) -> Option<Amount> {
516 self.totals.as_ref().and_then(|t| t.payable)
517 }
518
519 pub fn tax_total(&self) -> Option<Amount> {
521 self.totals.as_ref().and_then(|t| t.tax_total)
522 }
523
524 pub fn stamp_profile(&mut self, profile: Profile) {
528 self.profile = profile;
529 if profile == Profile::Unknown {
530 return;
531 }
532 self.specification_id = Some(profile.specification_id().into());
533 if let Some(bt23) = profile.process_id() {
534 self.business_process = Some(bt23.into());
535 }
536 }
537
538 pub fn line_net_sum(&self) -> Option<Amount> {
539 self.lines
540 .iter()
541 .try_fold(Amount::ZERO, |acc, line| acc.checked_add(line.net))
542 }
543
544 pub fn to_credit_note(&self, new_number: impl Into<String>, new_issue_date: Date) -> Self {
546 let mut next = self.clone();
547 next.kind = DocumentKind::CreditNote;
548 next.type_code = Some(Code::new("381"));
549 next.preceding = vec![PrecedingInvoice {
550 reference: DocumentReference::new(self.number.clone()),
551 issue_date: self.issue_date,
552 }];
553 next.number = new_number.into();
554 next.issue_date = Some(new_issue_date);
555 next
556 }
557}
558
559#[cfg(test)]
560mod tests {
561 use super::*;
562 use crate::identifier::Identifier;
563 use crate::tax::TaxCategory;
564 use rust_decimal::Decimal;
565
566 #[test]
567 fn credit_note_does_not_negate() {
568 let mut inv = Invoice::blank(
569 Profile::En16931,
570 "INV-1",
571 "EUR",
572 Party::new("S", "DE"),
573 Party::new("B", "FR"),
574 );
575 inv.issue_date = Date::parse("2026-01-15").ok();
576 inv.type_code = Some(Code::new("380"));
577 inv.lines = vec![Line::new(
578 "1",
579 "A",
580 Amount::parse("100.00").unwrap(),
581 TaxCategory::vat("S", Decimal::from(19)),
582 )];
583 let _ = crate::reconcile::reconcile(&mut inv);
584 let cn = inv.to_credit_note("CN-1", Date::parse("2026-01-16").unwrap());
585 assert_eq!(cn.kind, DocumentKind::CreditNote);
586 assert_eq!(cn.payable(), inv.payable());
587 assert_eq!(cn.preceding[0].reference.as_str(), "INV-1");
588 assert_eq!(cn.type_code.as_ref().map(Code::as_str), Some("381"));
589 }
590
591 #[test]
592 fn table2_refs_and_line_groups_exist() {
593 let mut inv = Invoice::blank(
594 Profile::En16931,
595 "INV-1",
596 "EUR",
597 Party::new("S", "DE"),
598 Party::new("B", "FR"),
599 );
600 inv.tax_point_date = Date::parse("2026-01-10").ok();
601 inv.tax_point_code = Some(Code::new("3"));
602 inv.purchase_order = Some(DocumentReference::new("PO-9"));
603 inv.invoiced_object = Some(Identifier::new("OBJ-1"));
604 inv.lines.push({
605 let mut line = Line::new(
606 "1",
607 "A",
608 Amount::parse("90.00").unwrap(),
609 TaxCategory::vat("S", Decimal::from(19)),
610 );
611 line.period = Some(Period {
612 start: Date::parse("2026-01-01").ok(),
613 end: Date::parse("2026-01-31").ok(),
614 });
615 line.allowances.push(LineAllowanceCharge {
616 amount: Amount::parse("10.00").unwrap(),
617 base: None,
618 percent: None,
619 reason: Some("discount".into()),
620 reason_code: None,
621 });
622 line.standard_id = Some(Identifier::schemed("01234567890128", "0160"));
623 line.origin_country = Some(Code::new("DE"));
624 line
625 });
626 assert_eq!(inv.purchase_order.as_ref().unwrap().as_str(), "PO-9");
627 assert_eq!(inv.lines[0].allowances.len(), 1);
628 assert_eq!(inv.lines[0].origin_country.as_ref().unwrap().as_str(), "DE");
629 assert!(inv.invoiced_object.is_some());
630 }
631}