use std::collections::BTreeSet;
use en16931::invoice::{
Code, Contact, CreditTransfer, Delivery, DirectDebit, DocumentAllowanceCharge, Invoice,
InvoiceLine, InvoiceNote, Item, ItemAttribute, LineAllowanceCharge, LineVat, Party, Payee,
PaymentCard, PaymentInstructions, PaymentMeans, Period, PostalAddress, PrecedingInvoice,
PriceDetails, SupportingDocument, TaxRepresentative, VatBreakdown,
};
use en16931::{
Date, DocumentKind, DocumentReference, Identifier, InvoiceAmount, Percentage, Quantity,
};
#[derive(Default)]
pub struct Reader {
pub unmapped: BTreeSet<String>,
tax_amounts: Vec<(Option<String>, Option<InvoiceAmount>)>,
pub malformed: Vec<String>,
}
fn name<'i>(n: roxmltree::Node<'_, 'i>) -> &'i str {
n.tag_name().name()
}
fn kids<'a, 'i>(n: roxmltree::Node<'a, 'i>) -> impl Iterator<Item = roxmltree::Node<'a, 'i>> {
n.children().filter(roxmltree::Node::is_element)
}
fn kid<'a, 'i>(n: roxmltree::Node<'a, 'i>, want: &str) -> Option<roxmltree::Node<'a, 'i>> {
kids(n).find(|c| name(*c) == want)
}
fn text(n: roxmltree::Node<'_, '_>, want: &str) -> Option<String> {
kid(n, want)
.and_then(|c| c.text())
.map(|t| t.trim().to_owned())
}
fn own_text(n: roxmltree::Node<'_, '_>) -> String {
n.text().unwrap_or_default().trim().to_owned()
}
fn amount(n: roxmltree::Node<'_, '_>, want: &str) -> Option<InvoiceAmount> {
text(n, want).and_then(|t| InvoiceAmount::parse(&t).ok())
}
fn amount_here(n: roxmltree::Node<'_, '_>) -> Option<InvoiceAmount> {
InvoiceAmount::parse(&own_text(n)).ok()
}
fn decimal(n: roxmltree::Node<'_, '_>, want: &str) -> Option<rust_decimal::Decimal> {
text(n, want).and_then(|t| t.parse().ok())
}
fn date(n: roxmltree::Node<'_, '_>, want: &str) -> Option<Date> {
text(n, want).and_then(|t| Date::parse(&t).ok())
}
fn is_malformed<T>(
n: roxmltree::Node<'_, '_>,
want: &str,
parse: impl Fn(&str) -> Option<T>,
) -> bool {
text(n, want).is_some_and(|t| !t.is_empty() && parse(&t).is_none())
}
fn code(n: roxmltree::Node<'_, '_>, want: &str) -> Option<Code> {
text(n, want).map(Code::new)
}
fn identifier(n: roxmltree::Node<'_, '_>) -> Identifier {
match n.attribute("schemeID") {
Some(s) => Identifier::schemed(own_text(n), s),
None => Identifier::new(own_text(n)),
}
}
impl Reader {
fn skip(&mut self, parent: &str, n: roxmltree::Node<'_, '_>) {
self.unmapped.insert(format!("{parent}/{}", name(n)));
}
pub fn read(&mut self, root: roxmltree::Node<'_, '_>) -> Invoice {
let mut inv = Invoice::default();
inv.kind = if name(root) == "CreditNote" {
DocumentKind::CreditNote
} else {
DocumentKind::Invoice
};
for c in kids(root) {
match name(c) {
"CustomizationID" => inv.specification_id = Some(own_text(c)),
"ProfileID" => inv.business_process = Some(own_text(c)),
"ID" => inv.number = Some(own_text(c)),
"IssueDate" => {
if is_malformed(root, "IssueDate", |t| Date::parse(t).ok()) {
self.malformed.push("IssueDate".to_owned());
}
inv.issue_date = date(root, "IssueDate");
}
"DueDate" => inv.due_date = date(root, "DueDate"),
"InvoiceTypeCode" | "CreditNoteTypeCode" => {
inv.type_code = Some(Code::new(own_text(c)));
}
"Note" => inv.notes.push(read_note(&own_text(c))),
"TaxPointDate" => inv.vat_point_date = date(root, "TaxPointDate"),
"DocumentCurrencyCode" => inv.currency = Some(Code::new(own_text(c))),
"TaxCurrencyCode" => inv.vat_accounting_currency = Some(Code::new(own_text(c))),
"AccountingCost" => inv.accounting_reference = Some(own_text(c)),
"BuyerReference" => inv.buyer_reference = Some(own_text(c)),
"InvoicePeriod" => {
if let Some(dc) = code(c, "DescriptionCode") {
inv.vat_point_date_code = Some(dc);
}
let p = self.period(c);
if p.start.is_some() || p.end.is_some() {
inv.invoicing_period = Some(p);
}
}
"OrderReference" => {
inv.purchase_order_reference = text(c, "ID").map(DocumentReference::new);
inv.sales_order_reference = text(c, "SalesOrderID").map(DocumentReference::new);
}
"BillingReference" => {
if let Some(r) = kid(c, "InvoiceDocumentReference") {
inv.preceding_invoices.push(PrecedingInvoice {
reference: text(r, "ID")
.map_or_else(|| DocumentReference::new(""), DocumentReference::new),
issue_date: date(r, "IssueDate"),
});
}
}
"DespatchDocumentReference" => {
inv.despatch_advice_reference = text(c, "ID").map(DocumentReference::new);
}
"ReceiptDocumentReference" => {
inv.receiving_advice_reference = text(c, "ID").map(DocumentReference::new);
}
"OriginatorDocumentReference" => {
inv.tender_reference = text(c, "ID").map(DocumentReference::new);
}
"ContractDocumentReference" => {
inv.contract_reference = text(c, "ID").map(DocumentReference::new);
}
"ProjectReference" => {
inv.project_reference = text(c, "ID").map(DocumentReference::new);
}
"AdditionalDocumentReference" => self.additional_document(c, &mut inv),
"AccountingSupplierParty" => {
if let Some(p) = kid(c, "Party") {
inv.seller = self.party(p);
}
}
"AccountingCustomerParty" => {
if let Some(p) = kid(c, "Party") {
inv.buyer = self.party(p);
}
}
"PayeeParty" => inv.payee = Some(self.payee(c)),
"TaxRepresentativeParty" => inv.tax_representative = Some(self.tax_rep(c)),
"Delivery" => inv.delivery = Some(self.delivery(c)),
"PaymentMeans" => inv.payment = Some(self.payment(c)),
"PaymentTerms" => {
inv.payment_terms = kid(c, "Note").and_then(|n| n.text()).map(str::to_owned);
}
"AllowanceCharge" => {
let is_charge = text(c, "ChargeIndicator").as_deref() == Some("true");
let ac = self.allowance(c);
if is_charge {
inv.charges.push(ac);
} else {
inv.allowances.push(ac);
}
}
"TaxTotal" => self.tax_total(c, &mut inv),
"PrepaidPayment" => {
inv.extensions
.third_party_payments
.push(en16931::ThirdPartyPayment {
payment_type: text(c, "ID"),
amount: amount(c, "PaidAmount"),
description: text(c, "InstructionID"),
});
}
"LegalMonetaryTotal" => self.totals(c, &mut inv),
"InvoiceLine" | "CreditNoteLine" => {
let l = self.line(c);
let subs = self.sub_lines(c);
if !subs.is_empty() {
inv.extensions
.sub_invoice_lines
.push((inv.lines.len(), subs));
}
inv.lines.push(l);
}
"UBLVersionID"
| "CopyIndicator"
| "UUID"
| "IssueTime"
| "LineCountNumeric"
| "Signature"
| "TaxExchangeRate"
| "InvoiceDocumentReference" => {}
_ => self.skip("Invoice", c),
}
}
let sepa = inv
.seller
.identifiers
.iter()
.find(|id| id.scheme() == Some("SEPA"))
.map(|id| id.content().to_owned());
if let Some(PaymentMeans::DirectDebit(dd)) =
inv.payment.as_mut().and_then(|p| p.means.as_mut())
{
dd.creditor_identifier = sepa;
}
let doc_ccy = inv.currency.as_ref().map(|c| c.as_str().to_owned());
let tax_ccy = inv
.vat_accounting_currency
.as_ref()
.map(|c| c.as_str().to_owned());
for (ccy, amount) in std::mem::take(&mut self.tax_amounts) {
let is_doc = ccy.is_none() || ccy == doc_ccy;
let is_tax = tax_ccy.is_some() && ccy == tax_ccy;
if is_doc && inv.totals.vat_total.is_none() {
inv.totals.vat_total = amount;
}
if is_tax && inv.totals.vat_total_accounting.is_none() {
inv.totals.vat_total_accounting = amount;
}
}
inv
}
fn period(&mut self, n: roxmltree::Node<'_, '_>) -> Period {
for c in kids(n) {
if !matches!(name(c), "StartDate" | "EndDate" | "DescriptionCode") {
self.skip("InvoicePeriod", c);
}
}
Period {
start: date(n, "StartDate"),
end: date(n, "EndDate"),
}
}
fn address(&mut self, n: roxmltree::Node<'_, '_>) -> PostalAddress {
let mut a = PostalAddress::default();
for c in kids(n) {
match name(c) {
"StreetName" => a.line1 = Some(own_text(c)),
"AdditionalStreetName" => a.line2 = Some(own_text(c)),
"AddressLine" => a.line3 = text(c, "Line"),
"CityName" => a.city = Some(own_text(c)),
"PostalZone" => a.post_code = Some(own_text(c)),
"CountrySubentity" => a.subdivision = Some(own_text(c)),
"Country" => a.country = code(c, "IdentificationCode"),
_ => self.skip("PostalAddress", c),
}
}
a
}
fn party(&mut self, n: roxmltree::Node<'_, '_>) -> Party {
let mut p = Party::default();
for c in kids(n) {
match name(c) {
"EndpointID" => p.electronic_address = Some(identifier(c)),
"PartyIdentification" => {
if let Some(id) = kid(c, "ID") {
p.identifiers.push(identifier(id));
}
}
"PartyName" => p.trading_name = text(c, "Name"),
"PostalAddress" => p.address = self.address(c),
"PartyTaxScheme" => {
let scheme = kid(c, "TaxScheme").and_then(|s| text(s, "ID"));
let id = text(c, "CompanyID");
if scheme.as_deref() == Some("VAT") {
p.vat_identifier = id;
} else {
p.tax_registration = id;
}
}
"PartyLegalEntity" => {
if let Some(rn) = text(c, "RegistrationName") {
p.name = Some(rn);
}
if let Some(cid) = kid(c, "CompanyID") {
p.legal_registration = Some(identifier(cid));
}
if let Some(extra) = text(c, "CompanyLegalForm") {
p.additional_legal_information = Some(extra);
}
}
"Contact" => {
p.contact = Contact {
name: text(c, "Name"),
phone: text(c, "Telephone"),
email: text(c, "ElectronicMail"),
};
}
_ => self.skip("Party", c),
}
}
p
}
fn payee(&mut self, n: roxmltree::Node<'_, '_>) -> Payee {
let p = self.party(kid(n, "Party").unwrap_or(n));
Payee {
name: p.name.or(p.trading_name),
identifier: p.identifiers.into_iter().next(),
legal_registration: p.legal_registration,
}
}
fn tax_rep(&mut self, n: roxmltree::Node<'_, '_>) -> TaxRepresentative {
let p = self.party(n);
TaxRepresentative {
name: p.name.or(p.trading_name),
vat_identifier: p.vat_identifier,
address: p.address,
}
}
fn delivery(&mut self, n: roxmltree::Node<'_, '_>) -> Delivery {
let mut d = Delivery::default();
for c in kids(n) {
match name(c) {
"ActualDeliveryDate" => d.date = date(n, "ActualDeliveryDate"),
"DeliveryLocation" => {
if let Some(id) = kid(c, "ID") {
d.location = Some(identifier(id));
}
if let Some(a) = kid(c, "Address") {
d.address = Some(self.address(a));
}
}
"DeliveryParty" => d.party_name = kid(c, "PartyName").and_then(|p| text(p, "Name")),
_ => self.skip("Delivery", c),
}
}
d
}
fn payment(&mut self, n: roxmltree::Node<'_, '_>) -> PaymentInstructions {
let mut p = PaymentInstructions {
means_code: code(n, "PaymentMeansCode"),
means_text: kid(n, "PaymentMeansCode")
.and_then(|c| c.attribute("name").map(str::to_owned)),
remittance_information: text(n, "PaymentID"),
means: None,
};
for c in kids(n) {
match name(c) {
"PayeeFinancialAccount" => {
p.means = Some(PaymentMeans::CreditTransfer(vec![CreditTransfer {
account_identifier: text(c, "ID"),
account_name: text(c, "Name"),
provider_identifier: kid(c, "FinancialInstitutionBranch")
.and_then(|b| text(b, "ID")),
}]));
}
"CardAccount" => {
p.means = Some(PaymentMeans::Card(PaymentCard {
primary_account_number: text(c, "PrimaryAccountNumberID"),
holder_name: text(c, "HolderName"),
}));
}
"PaymentMandate" => {
p.means = Some(PaymentMeans::DirectDebit(DirectDebit {
mandate_reference: text(c, "ID"),
creditor_identifier: None,
debited_account: kid(c, "PayerFinancialAccount")
.and_then(|a| text(a, "ID")),
}));
}
"PaymentMeansCode" | "PaymentID" | "PaymentDueDate" | "InstructionNote" => {}
_ => self.skip("PaymentMeans", c),
}
}
p
}
fn tax_category(
&mut self,
n: roxmltree::Node<'_, '_>,
) -> (Code, Option<Percentage>, Option<String>, Option<Code>) {
let mut reason = None;
let mut reason_code = None;
for c in kids(n) {
match name(c) {
"ID" | "Percent" | "TaxScheme" => {}
"TaxExemptionReason" => reason = Some(own_text(c)),
"TaxExemptionReasonCode" => reason_code = Some(Code::new(own_text(c))),
_ => self.skip("TaxCategory", c),
}
}
(
code(n, "ID").unwrap_or_default(),
decimal(n, "Percent").map(Percentage::new),
reason,
reason_code,
)
}
fn allowance(&mut self, n: roxmltree::Node<'_, '_>) -> DocumentAllowanceCharge {
let mut vat = LineVat::default();
for c in kids(n) {
match name(c) {
"TaxCategory" => {
let (cat, pct, _, _) = self.tax_category(c);
vat = LineVat {
category: cat,
rate: pct,
};
}
"ChargeIndicator"
| "Amount"
| "BaseAmount"
| "MultiplierFactorNumeric"
| "AllowanceChargeReason"
| "AllowanceChargeReasonCode"
| "TaxScheme" => {}
_ => self.skip("AllowanceCharge", c),
}
}
DocumentAllowanceCharge {
amount: amount(n, "Amount").unwrap_or_default(),
base_amount: amount(n, "BaseAmount"),
percentage: decimal(n, "MultiplierFactorNumeric").map(Percentage::new),
vat,
reason: text(n, "AllowanceChargeReason"),
reason_code: code(n, "AllowanceChargeReasonCode"),
}
}
fn tax_total(&mut self, n: roxmltree::Node<'_, '_>, inv: &mut Invoice) {
if let Some(ta) = kid(n, "TaxAmount") {
self.tax_amounts.push((
ta.attribute("currencyID").map(str::to_owned),
amount_here(ta),
));
}
let subtotals: Vec<_> = kids(n).filter(|c| name(*c) == "TaxSubtotal").collect();
if subtotals.is_empty() {
return;
}
for st in subtotals {
let mut cat = Code::default();
let mut rate = None;
let mut reason = None;
let mut reason_code = None;
for c in kids(st) {
match name(c) {
"TaxCategory" => {
let (a, b, r, rc) = self.tax_category(c);
cat = a;
rate = b;
reason = r;
reason_code = rc;
}
"TaxableAmount" | "TaxAmount" => {}
_ => self.skip("TaxSubtotal", c),
}
}
inv.vat_breakdown.push(VatBreakdown {
taxable_amount: amount(st, "TaxableAmount").unwrap_or_default(),
tax_amount: amount(st, "TaxAmount").unwrap_or_default(),
category: cat,
rate,
exemption_reason: reason,
exemption_reason_code: reason_code,
});
}
}
fn opt_amt(&mut self, n: roxmltree::Node<'_, '_>, what: &str) -> Option<InvoiceAmount> {
let raw = own_text(n);
let parsed = InvoiceAmount::parse(&raw).ok();
if parsed.is_none() && !raw.is_empty() {
self.malformed.push(format!("{what}={raw}"));
}
parsed
}
fn amt(&mut self, n: roxmltree::Node<'_, '_>, what: &str) -> InvoiceAmount {
self.opt_amt(n, what).unwrap_or_default()
}
fn totals(&mut self, n: roxmltree::Node<'_, '_>, inv: &mut Invoice) {
let t = &mut inv.totals;
for c in kids(n) {
match name(c) {
"LineExtensionAmount" => t.line_total = self.amt(c, "LineExtensionAmount"),
"TaxExclusiveAmount" => t.taxable_total = self.amt(c, "TaxExclusiveAmount"),
"TaxInclusiveAmount" => t.gross_total = self.amt(c, "TaxInclusiveAmount"),
"AllowanceTotalAmount" => {
t.allowance_total = self.opt_amt(c, "AllowanceTotalAmount");
}
"ChargeTotalAmount" => t.charge_total = self.opt_amt(c, "ChargeTotalAmount"),
"PrepaidAmount" => t.paid = self.opt_amt(c, "PrepaidAmount"),
"PayableRoundingAmount" => t.rounding = self.opt_amt(c, "PayableRoundingAmount"),
"PayableAmount" => t.due = self.amt(c, "PayableAmount"),
_ => self.skip("LegalMonetaryTotal", c),
}
}
}
fn line(&mut self, n: roxmltree::Node<'_, '_>) -> InvoiceLine {
let mut l = InvoiceLine {
id: text(n, "ID").unwrap_or_default(),
note: text(n, "Note"),
order_line_reference: None,
accounting_reference: text(n, "AccountingCost"),
object_identifier: None,
quantity: Quantity::new(
text(n, "InvoicedQuantity")
.or_else(|| text(n, "CreditedQuantity"))
.and_then(|q| q.parse().ok())
.unwrap_or_default(),
),
unit_code: kid(n, "InvoicedQuantity")
.or_else(|| kid(n, "CreditedQuantity"))
.and_then(|q| q.attribute("unitCode"))
.map(Code::new)
.unwrap_or_default(),
net_amount: amount(n, "LineExtensionAmount").unwrap_or_default(),
period: None,
allowances: vec![],
charges: vec![],
price: PriceDetails::default(),
vat: LineVat::default(),
item: Item::default(),
};
for c in kids(n) {
match name(c) {
"InvoicePeriod" => l.period = Some(self.period(c)),
"OrderLineReference" => {
l.order_line_reference = text(c, "LineID").map(DocumentReference::new);
}
"DocumentReference" => {
if text(c, "DocumentTypeCode").as_deref() == Some("130")
&& let Some(id) = kid(c, "ID")
{
l.object_identifier = Some(identifier(id));
}
}
"AllowanceCharge" => {
let is_charge = text(c, "ChargeIndicator").as_deref() == Some("true");
let a = LineAllowanceCharge {
amount: amount(c, "Amount").unwrap_or_default(),
base_amount: amount(c, "BaseAmount"),
percentage: decimal(c, "MultiplierFactorNumeric").map(Percentage::new),
reason: text(c, "AllowanceChargeReason"),
reason_code: code(c, "AllowanceChargeReasonCode"),
};
if is_charge {
l.charges.push(a);
} else {
l.allowances.push(a);
}
}
"Price" => l.price = self.price(c),
"Item" => {
let (item, vat) = self.item(c);
l.item = item;
l.vat = vat;
}
"ID"
| "Note"
| "InvoicedQuantity"
| "CreditedQuantity"
| "LineExtensionAmount"
| "AccountingCost"
| "TaxTotal"
| "SubInvoiceLine" => {}
_ => self.skip("InvoiceLine", c),
}
}
l
}
fn sub_lines(&mut self, n: roxmltree::Node<'_, '_>) -> Vec<en16931::SubInvoiceLine> {
kids(n)
.filter(|c| name(*c) == "SubInvoiceLine")
.map(|c| {
let line = self.line(c);
let categories = kid(c, "Item").map_or(0, |i| {
kids(i)
.filter(|x| name(*x) == "ClassifiedTaxCategory")
.count()
});
en16931::SubInvoiceLine {
vat: (categories == 1).then(|| line.vat.clone()),
line,
children: self.sub_lines(c),
}
})
.collect()
}
fn price(&mut self, n: roxmltree::Node<'_, '_>) -> PriceDetails {
let mut p = PriceDetails {
net_price: text(n, "PriceAmount")
.and_then(|t| t.parse().ok())
.map(en16931::UnitPriceAmount::new)
.unwrap_or_default(),
price_discount: None,
gross_price: None,
base_quantity: decimal(n, "BaseQuantity").map(Quantity::new),
base_quantity_code: kid(n, "BaseQuantity")
.and_then(|q| q.attribute("unitCode"))
.map(Code::new),
};
for c in kids(n) {
match name(c) {
"AllowanceCharge" => {
p.price_discount = text(c, "Amount")
.and_then(|t| t.parse().ok())
.map(en16931::UnitPriceAmount::new);
p.gross_price = text(c, "BaseAmount")
.and_then(|t| t.parse().ok())
.map(en16931::UnitPriceAmount::new);
}
"PriceAmount" | "BaseQuantity" => {}
_ => self.skip("Price", c),
}
}
p
}
fn item(&mut self, n: roxmltree::Node<'_, '_>) -> (Item, LineVat) {
let mut item = Item {
name: text(n, "Name"),
description: text(n, "Description"),
seller_identifier: kid(n, "SellersItemIdentification").and_then(|i| text(i, "ID")),
buyer_identifier: kid(n, "BuyersItemIdentification").and_then(|i| text(i, "ID")),
standard_identifier: kid(n, "StandardItemIdentification")
.and_then(|i| kid(i, "ID"))
.map(identifier),
classification_identifiers: vec![],
origin_country: kid(n, "OriginCountry").and_then(|c| code(c, "IdentificationCode")),
attributes: vec![],
};
let mut vat = LineVat::default();
for c in kids(n) {
match name(c) {
"ClassifiedTaxCategory" => {
let (cat, pct, _, _) = self.tax_category(c);
vat = LineVat {
category: cat,
rate: pct,
};
}
"CommodityClassification" => {
if let Some(icc) = kid(c, "ItemClassificationCode") {
let id = match icc.attribute("listID") {
Some(s) => Identifier::schemed(own_text(icc), s),
None => Identifier::new(own_text(icc)),
};
item.classification_identifiers.push(id);
}
}
"AdditionalItemProperty" => item.attributes.push(ItemAttribute {
name: text(c, "Name"),
value: text(c, "Value"),
}),
"Name"
| "Description"
| "SellersItemIdentification"
| "BuyersItemIdentification"
| "StandardItemIdentification"
| "OriginCountry" => {}
_ => self.skip("Item", c),
}
}
(item, vat)
}
fn additional_document(&mut self, n: roxmltree::Node<'_, '_>, inv: &mut Invoice) {
if text(n, "DocumentTypeCode").as_deref() == Some("130") {
if let Some(id) = kid(n, "ID") {
inv.object_identifier = Some(identifier(id));
}
return;
}
let attachment = kid(n, "Attachment")
.and_then(|a| kid(a, "EmbeddedDocumentBinaryObject"))
.and_then(|b| {
match en16931::Attachment::new(
crate::xml::decode_base64(&own_text(b)),
b.attribute("mimeCode").unwrap_or_default(),
b.attribute("filename").unwrap_or_default(),
) {
Ok(a) => Some(a),
Err(e) => {
self.malformed
.push(format!("EmbeddedDocumentBinaryObject: {e}"));
None
}
}
});
let uri = kid(n, "Attachment")
.and_then(|a| kid(a, "ExternalReference"))
.and_then(|e| text(e, "URI"));
inv.attachments.push(SupportingDocument {
reference: text(n, "ID")
.map_or_else(|| DocumentReference::new(""), DocumentReference::new),
description: text(n, "DocumentDescription"),
uri,
attachment,
});
}
}
fn read_note(raw: &str) -> InvoiceNote {
if let Some(rest) = raw.strip_prefix('#')
&& let Some((subject, body)) = rest.split_once('#')
&& subject.len() == 3
{
return InvoiceNote {
subject_code: Some(Code::new(subject)),
note: Some(body.to_owned()),
};
}
InvoiceNote::new(raw)
}