Skip to main content

core_invoice/
invoice.rs

1//! Semantic invoice: parties, lines, totals, and Table 2 groups.
2
3use 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/// Postal address (BG-5/8/12/15). Country is BT-40 / BT-55 / BT-69 / BT-80.
15#[derive(Debug, Clone, PartialEq, Eq, Default)]
16pub struct PostalAddress {
17    /// BT-35 / BT-50 / BT-75 street.
18    pub line1: Option<String>,
19    /// BT-36 / BT-51 / BT-76 additional street.
20    pub line2: Option<String>,
21    /// BT-162 / BT-163 / BT-164 additional address line.
22    pub line3: Option<String>,
23    /// BT-37 / BT-52 / BT-77 city.
24    pub city: Option<String>,
25    /// BT-38 / BT-53 / BT-78 post code.
26    pub post_code: Option<String>,
27    /// BT-39 / BT-54 / BT-79 subdivision.
28    pub subdivision: Option<String>,
29    /// BT-40 / BT-55 / BT-69 / BT-80 country code.
30    pub country: Option<Code>,
31}
32
33/// BG-6 / BG-9 contact.
34#[derive(Debug, Clone, PartialEq, Eq, Default)]
35pub struct Contact {
36    /// BT-41 / BT-56 contact point.
37    pub point: Option<String>,
38    /// BT-42 / BT-57 telephone.
39    pub phone: Option<String>,
40    /// BT-43 / BT-58 email.
41    pub email: Option<String>,
42}
43
44/// Extra PartyTaxScheme row (PINT-MY SST + TIN + TTx).
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct PartyTax {
47    /// Tax identifier on this extra PartyTaxScheme row.
48    pub id: Identifier,
49    /// TaxScheme ID (`VAT`, `GST`, `AAL`). Never `SST`.
50    pub scheme: String,
51}
52
53/// Party identifiers are four slots, not a leftover `tax_id`:
54///
55/// - `legal_registration` — BT-30 / BT-47 (PINT-MY BRN, unschemed)
56/// - `vat_identifier` — BT-31 / BT-48
57/// - `tax_registration` — BT-32 (PINT-MY TIN, scheme `GST`)
58/// - `electronic_address` — BT-34 / BT-49 (PINT-MY endpoint scheme `0230`)
59///
60/// Country (BT-40 / BT-55) lives on [`PostalAddress::country`]. [`Party::country`]
61/// reads that field.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Party {
64    /// BT-27 / BT-44 name.
65    pub name: String,
66    /// BT-28 / BT-45 trading name.
67    pub trading_name: Option<String>,
68    /// BT-29 / BT-46 party identifiers.
69    pub identifiers: Vec<Identifier>,
70    /// BT-30 / BT-47 legal registration (PINT-MY BRN, unschemed).
71    pub legal_registration: Option<Identifier>,
72    /// BT-31 / BT-48 VAT identifier.
73    pub vat_identifier: Option<Identifier>,
74    /// BT-32 seller tax registration (PINT-MY TIN, scheme `GST`).
75    pub tax_registration: Option<Identifier>,
76    /// BT-34 / BT-49 electronic address (PINT-MY endpoint scheme `0230`).
77    pub electronic_address: Option<Identifier>,
78    /// Extra PartyTaxScheme rows (PINT-MY SST + TIN + TTX).
79    pub party_taxes: Vec<PartyTax>,
80    /// BT-33 additional legal information.
81    pub additional_legal: Option<String>,
82    /// BG-5 / BG-8 postal address. Country is BT-40 / BT-55.
83    pub address: Option<PostalAddress>,
84    /// BG-6 / BG-9 contact.
85    pub contact: Option<Contact>,
86}
87
88impl Party {
89    /// Name plus country on [`PostalAddress::country`]. Empty country leaves `address` absent.
90    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    /// BT-40 / BT-55 from [`PostalAddress::country`]. Empty when address or code is absent.
116    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/// BG-29 item price. BT-146 net; BT-147 discount; BT-148 gross (unit price, not Amount.Type).
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Price {
128    /// BT-146 item net price.
129    pub net: UnitPriceAmount,
130    /// BT-147 item price discount (`Price/AllowanceCharge/Amount`).
131    pub discount: Option<UnitPriceAmount>,
132    /// BT-148 item gross price (`Price/AllowanceCharge/BaseAmount`).
133    pub gross: Option<UnitPriceAmount>,
134    /// BT-149 item price base quantity.
135    pub base_qty: Option<Quantity>,
136    /// BT-150 item price base quantity unit.
137    pub base_unit: Option<Code>,
138}
139
140/// Line allowance/charge (BG-27/28). No tax child: Peppol/PINT inherit the line category.
141/// Line A/C already sits in BT-131. Do not add them again in taxable_for.
142#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct LineAllowanceCharge {
144    /// BT-136 / BT-141 amount.
145    pub amount: InvoiceAmount,
146    /// BT-137 / BT-142 base amount.
147    pub base: Option<InvoiceAmount>,
148    /// BT-138 / BT-143 percentage.
149    pub percent: Option<Percentage>,
150    /// BT-139 / BT-144 reason.
151    pub reason: Option<String>,
152    /// BT-140 / BT-145 reason code.
153    pub reason_code: Option<Code>,
154}
155
156/// BG-25 invoice line.
157#[derive(Debug, Clone, PartialEq, Eq)]
158pub struct Line {
159    /// BT-126 line identifier, not a GTIN (BT-157).
160    pub id: String,
161    /// BT-153 item name.
162    pub name: String,
163    /// BT-131 line net amount.
164    pub net: Amount,
165    /// BT-151 / BT-152 classified tax category.
166    pub tax: TaxCategory,
167    /// BT-129 invoiced quantity.
168    pub quantity: Option<Quantity>,
169    /// BT-130 unit of measure.
170    pub unit: Option<Code>,
171    /// BG-29 item price.
172    pub price: Option<Price>,
173    /// BT-127 line note.
174    pub note: Option<String>,
175    /// BT-154 item description.
176    pub description: Option<String>,
177    /// BG-26 invoicing period (BT-134/BT-135). Not [`Invoice::period`] (BG-14).
178    pub period: Option<Period>,
179    /// BG-27 line allowances.
180    pub allowances: Vec<LineAllowanceCharge>,
181    /// BG-28 line charges.
182    pub charges: Vec<LineAllowanceCharge>,
183    /// BT-157 Item standard identifier (often GTIN). UBL `StandardItemIdentification`; scheme ICD (BR-64 / BR-CL-21). Not BT-155.
184    pub standard_id: Option<Identifier>,
185    /// BT-155 Seller's item identifier. UBL `SellersItemIdentification`. Not BT-156, not BT-157.
186    pub item_id: Option<Identifier>,
187    /// BT-156 Buyer's item identifier. UBL `BuyersItemIdentification`. Not BT-155, not BT-157.
188    pub buyer_id: Option<Identifier>,
189    /// BT-132 referenced purchase order line (`OrderLineReference/LineID`). Not BT-13, not BT-126.
190    pub order_line: Option<String>,
191    /// BT-133 invoice line buyer accounting reference (`cbc:AccountingCost`). Not header BT-19.
192    pub accounting_reference: Option<String>,
193    /// BG-32 item attributes (BT-160 name, BT-161 value). Not BT-158 classifications.
194    pub attributes: Vec<ItemAttribute>,
195    /// BT-159 item origin country (BR-CL-15), not BT-80.
196    pub origin_country: Option<Code>,
197    /// BG-32 classification identifiers (PINT-MY CLASS is listID `CG`, not LHDN).
198    pub classifications: Vec<Identifier>,
199    /// Line invoiced object (BT-128). Peppol R101: DocumentTypeCode 130 only.
200    pub invoiced_object: Option<Identifier>,
201    /// UBL DocumentTypeCode on the line DocumentReference. Absent means 130.
202    pub invoiced_object_code: Option<Code>,
203    /// Extra ClassifiedTaxCategory after BT-151 (PINT-MY TTX beside HVG/SA). Not BT-151.
204    pub extra_tax: Vec<TaxCategory>,
205    /// Line `TaxTotal/cbc:TaxAmount`. ALIGNED-IBRP-TTX-09-MY sums this on TTX lines. Not BT-117.
206    pub tax_total: Option<InvoiceAmount>,
207}
208
209impl Line {
210    /// Line id, name, net (BT-131), and tax category. Does not invent quantity or price.
211    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/// BG-32 item attribute: BT-160 name + BT-161 value (`AdditionalItemProperty`).
247#[derive(Debug, Clone, PartialEq, Eq)]
248pub struct ItemAttribute {
249    /// BT-160 item attribute name.
250    pub name: String,
251    /// BT-161 item attribute value.
252    pub value: String,
253}
254
255/// BT-21 / BT-22 invoice note.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct InvoiceNote {
258    /// BT-21 note subject code (UNCL 4451).
259    pub subject: Option<Code>,
260    /// BT-22 note text.
261    pub text: String,
262}
263
264/// BG-3 preceding invoice reference.
265#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct PrecedingInvoice {
267    /// BT-25 preceding invoice reference.
268    pub reference: DocumentReference,
269    /// BT-26 preceding invoice issue date.
270    pub issue_date: Option<Date>,
271}
272
273/// Header invoicing period BG-14 (BT-73/74) or line period BG-26 (BT-134/135).
274#[derive(Debug, Clone, PartialEq, Eq)]
275pub struct Period {
276    /// BT-73 / BT-134 start.
277    pub start: Option<Date>,
278    /// BT-74 / BT-135 end.
279    pub end: Option<Date>,
280}
281
282/// BG-10 payee (if different from seller).
283#[derive(Debug, Clone, PartialEq, Eq)]
284pub struct Payee {
285    /// BT-59 payee name.
286    pub name: String,
287    /// BT-60 payee identifier.
288    pub identifier: Option<Identifier>,
289    /// BT-61 payee legal registration identifier.
290    pub legal_registration: Option<Identifier>,
291}
292
293/// BG-11 seller tax representative.
294#[derive(Debug, Clone, PartialEq, Eq)]
295pub struct TaxRepresentative {
296    /// BT-62 name.
297    pub name: String,
298    /// BT-63 VAT identifier.
299    pub vat_identifier: Option<Identifier>,
300    /// BG-12 postal address (BT-69 country).
301    pub address: Option<PostalAddress>,
302}
303
304/// BG-13 delivery information.
305#[derive(Debug, Clone, PartialEq, Eq)]
306pub struct Delivery {
307    /// BT-70 deliver-to party name.
308    pub name: Option<String>,
309    /// BT-71 deliver-to location identifier.
310    pub location_id: Option<Identifier>,
311    /// BT-72 actual delivery date.
312    pub date: Option<Date>,
313    /// BG-15 deliver-to address.
314    pub address: Option<PostalAddress>,
315}
316
317/// Payment instructions. BT-81 is `means_code`. Account/IBAN/BIC, card PAN, and
318/// mandate live only on [`PaymentMeans`] (exclusive BG-17/18/19).
319#[derive(Debug, Clone, PartialEq, Eq)]
320pub struct PaymentInstructions {
321    /// BT-81 Payment means type code.
322    pub means_code: Option<Code>,
323    /// BT-82 Payment means text (`@name` on UBL PaymentMeansCode, not InstructionNote).
324    pub means_text: Option<String>,
325    /// BT-83 Remittance information (UBL PaymentID).
326    pub remittance: Option<String>,
327    /// Exclusive BG-17 / BG-18 / BG-19. Several IBANs are several credit-transfer accounts.
328    pub means: Option<PaymentMeans>,
329}
330
331/// BG-20 / BG-21 document level allowance or charge.
332#[derive(Debug, Clone, PartialEq, Eq)]
333pub struct AllowanceCharge {
334    /// BT-92 / BT-99 amount.
335    pub amount: InvoiceAmount,
336    /// BT-93 / BT-100 base amount.
337    pub base: Option<InvoiceAmount>,
338    /// BT-94 / BT-101 percentage.
339    pub percent: Option<Percentage>,
340    /// BT-97 / BT-104 reason.
341    pub reason: Option<String>,
342    /// BT-98 / BT-105 reason code.
343    pub reason_code: Option<Code>,
344    /// BT-95/96 or BT-102/103 tax category and rate.
345    pub tax: Option<TaxCategory>,
346}
347
348/// BG-23 VAT/GST/SST breakdown row.
349#[derive(Debug, Clone, PartialEq, Eq)]
350pub struct TaxBreakdown {
351    /// In-memory tax system. SST is never TaxScheme `SST` on the wire.
352    pub system: TaxSystem,
353    /// TaxScheme/cbc:ID (`VAT`, `GST`, `AAL`). Never `SST`.
354    pub scheme: String,
355    /// BT-118 category code.
356    pub category: Code,
357    /// BT-119 rate. None for EN `O` and PINT-MY TTX.
358    pub rate: Option<Percentage>,
359    /// BT-116 taxable amount.
360    pub taxable: InvoiceAmount,
361    /// BT-117 tax amount.
362    pub tax: InvoiceAmount,
363    /// BT-120 exemption reason.
364    pub exemption_reason: Option<String>,
365    /// BT-121 exemption reason code.
366    pub exemption_code: Option<Code>,
367}
368
369/// BG-22 document totals. Absent optional amounts are `None`, not 0.00.
370#[derive(Debug, Clone, PartialEq, Eq, Default)]
371pub struct DocumentTotals {
372    /// BT-106 sum of invoice line net amounts.
373    pub line_net: Option<InvoiceAmount>,
374    /// BT-107 sum of allowances on document level.
375    pub allowance_total: Option<InvoiceAmount>,
376    /// BT-108 sum of charges on document level.
377    pub charge_total: Option<InvoiceAmount>,
378    /// BT-109 invoice total amount without VAT.
379    pub without_tax: Option<InvoiceAmount>,
380    /// BT-110 invoice total VAT amount.
381    pub tax_total: Option<InvoiceAmount>,
382    /// BT-111 invoice total VAT amount in accounting currency.
383    pub tax_total_accounting: Option<InvoiceAmount>,
384    /// BT-112 invoice total amount with VAT.
385    pub with_tax: Option<InvoiceAmount>,
386    /// BT-113 paid amount.
387    pub paid: Option<InvoiceAmount>,
388    /// BT-114 rounding amount.
389    pub rounding: Option<InvoiceAmount>,
390    /// BT-115 amount due for payment. Missing PayableAmount is `None`, not 0 (BR-15).
391    pub payable: Option<InvoiceAmount>,
392}
393
394/// BG-24 additional supporting document.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub struct SupportingDocument {
397    /// BT-122 supporting document reference.
398    pub id: DocumentReference,
399    /// BT-123 supporting document description.
400    pub description: Option<String>,
401    /// BT-124 external document location.
402    pub uri: Option<String>,
403    /// BT-125 attached document.
404    pub attachment: Option<Attachment>,
405}
406
407/// Semantic invoice. Fields are `pub` on 2.x so embedders can [`Invoice::blank`]
408/// then set terms. Do not match on the struct layout — `#[non_exhaustive]` would
409/// be a 3.0 break. A proved document is [`crate::Validated`].
410#[derive(Debug, Clone, PartialEq, Eq)]
411pub struct Invoice {
412    /// In-memory rule set used by [`crate::validate()`]. Not BT-24; BT-24 is [`Self::specification_id`].
413    pub profile: Profile,
414    /// BT-24 specification identifier.
415    pub specification_id: Option<String>,
416    /// Syntax root analogue. Not derived from BT-3.
417    pub kind: DocumentKind,
418    /// BT-1 invoice number.
419    pub number: String,
420    /// BT-5 invoice currency.
421    pub currency: String,
422    /// BT-2 issue date.
423    pub issue_date: Option<Date>,
424    /// BT-3 invoice type code.
425    pub type_code: Option<Code>,
426    /// BT-6 VAT accounting currency.
427    pub tax_currency: Option<Code>,
428    /// BT-9 payment due date.
429    pub due_date: Option<Date>,
430    /// BT-7 / BT-8. BR-CO-03 when both the date and the code rules apply.
431    pub tax_point_date: Option<Date>,
432    /// BT-8 VAT point date code.
433    pub tax_point_code: Option<Code>,
434    /// BT-23 business process type.
435    pub business_process: Option<String>,
436    /// BT-10 buyer reference. Do not overload with BT-13.
437    pub buyer_reference: Option<DocumentReference>,
438    /// BT-11 project reference.
439    pub project: Option<DocumentReference>,
440    /// BT-12 contract reference.
441    pub contract: Option<DocumentReference>,
442    /// BT-13 purchase order reference (Peppol R003 with BT-10).
443    pub purchase_order: Option<DocumentReference>,
444    /// BT-14 sales order reference.
445    pub sales_order: Option<DocumentReference>,
446    /// BT-15 receiving advice reference.
447    pub receiving_advice: Option<DocumentReference>,
448    /// BT-16 despatch advice reference.
449    pub despatch: Option<DocumentReference>,
450    /// BT-17 tender or lot reference.
451    pub tender: Option<DocumentReference>,
452    /// BT-18 invoiced object identifier (not a BG-24 supporting document).
453    pub invoiced_object: Option<Identifier>,
454    /// BT-19 buyer accounting reference.
455    pub buyer_accounting: Option<String>,
456    /// BT-20 payment terms.
457    pub payment_terms: Option<String>,
458    /// BG-1 notes (BT-21/22).
459    pub notes: Vec<InvoiceNote>,
460    /// BG-3 preceding invoice reference.
461    pub preceding: Vec<PrecedingInvoice>,
462    /// BG-4 seller.
463    pub seller: Party,
464    /// BG-7 buyer.
465    pub buyer: Party,
466    /// BG-10 payee.
467    pub payee: Option<Payee>,
468    /// BG-11 seller tax representative.
469    pub tax_representative: Option<TaxRepresentative>,
470    /// BG-13 delivery.
471    pub delivery: Option<Delivery>,
472    /// BG-14 invoicing period.
473    pub period: Option<Period>,
474    /// BG-16 payment instructions.
475    pub payment: Option<PaymentInstructions>,
476    /// BG-20 document level allowances.
477    pub document_allowances: Vec<AllowanceCharge>,
478    /// BG-21 document level charges.
479    pub document_charges: Vec<AllowanceCharge>,
480    /// BG-23 VAT/GST/SST breakdown.
481    pub tax_breakdown: Vec<TaxBreakdown>,
482    /// BG-22 document totals.
483    pub totals: Option<DocumentTotals>,
484    /// BG-24 additional supporting documents.
485    pub supporting_documents: Vec<SupportingDocument>,
486    /// BG-25 invoice lines.
487    pub lines: Vec<Line>,
488}
489
490impl Invoice {
491    /// 2.x constructor. Stamps BT-24 from [`Profile::specification_id`]. Then set `pub` fields.
492    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    /// BT-115 from [`DocumentTotals`]. Absent BG-22 or absent PayableAmount is not 0.00 (BR-15).
542    pub fn payable(&self) -> Option<Amount> {
543        self.totals.as_ref().and_then(|t| t.payable)
544    }
545
546    /// BT-110 from [`DocumentTotals`]. Absent totals is not 0.00.
547    pub fn tax_total(&self) -> Option<Amount> {
548        self.totals.as_ref().and_then(|t| t.tax_total)
549    }
550
551    /// BT-24 and BT-23 come from the proved profile, not leftover fields on Invoice.
552    ///
553    /// EN 16931 has no required ProfileID: BT-23 is left as-is (the UBL writer omits it).
554    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    /// Sum of line BT-131. Overflow is `None`. Not BT-106 unless totals exist.
566    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    /// Credit note: new number/date, BG-3 to the original, amounts **not** negated.
573    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}