use super::{order, prohibitions};
use crate::xml::{Xml, base64};
use en16931::invoice::{
Code, DocumentAllowanceCharge, Invoice, InvoiceLine, Item, LineAllowanceCharge, LineVat, Party,
PaymentInstructions, PaymentMeans, Period, PostalAddress, PriceDetails, SupportingDocument,
};
use en16931::{DocumentKind, DocumentReference, Identifier, InvoiceAmount};
const NS_INVOICE: &str = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
const NS_CREDIT_NOTE: &str = "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2";
const NS_CAC: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
const NS_CBC: &str = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
static RULES: crate::xml::Rules = crate::xml::Rules {
order: order::children_of,
forbidden_path: prohibitions::forbidden_path,
forbidden_attribute: prohibitions::forbidden_attribute,
};
fn amt(a: InvoiceAmount) -> String {
a.to_string()
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Written {
pub xml: String,
pub dropped: Vec<String>,
}
#[must_use]
pub fn write(inv: &Invoice) -> Written {
let credit = matches!(inv.kind, DocumentKind::CreditNote);
let (root, ns) = if credit {
("CreditNote", NS_CREDIT_NOTE)
} else {
("Invoice", NS_INVOICE)
};
let ccy = inv.currency.as_ref().map_or("", Code::as_str);
let mut x = Xml::new(
root,
vec![
("xmlns".to_owned(), ns.to_owned()),
("xmlns:cac".to_owned(), NS_CAC.to_owned()),
("xmlns:cbc".to_owned(), NS_CBC.to_owned()),
],
&RULES,
);
opt(
&mut x,
"cbc:CustomizationID",
inv.specification_id.as_deref(),
);
opt(&mut x, "cbc:ProfileID", inv.business_process.as_deref());
opt(&mut x, "cbc:ID", inv.number.as_deref());
if let Some(d) = inv.issue_date {
x.leaf("cbc:IssueDate", &[], &d.to_string());
}
if let Some(d) = inv.due_date {
x.leaf("cbc:DueDate", &[], &d.to_string());
}
if let Some(c) = &inv.type_code {
let name = if credit {
"cbc:CreditNoteTypeCode"
} else {
"cbc:InvoiceTypeCode"
};
x.leaf(name, &[], c.as_str());
}
for n in &inv.notes {
let body = n.note.as_deref().unwrap_or_default();
match &n.subject_code {
Some(c) => x.leaf("cbc:Note", &[], &format!("#{}#{}", c.as_str(), body)),
None => x.leaf("cbc:Note", &[], body),
}
}
if let Some(d) = inv.vat_point_date {
x.leaf("cbc:TaxPointDate", &[], &d.to_string());
}
if !ccy.is_empty() {
x.leaf("cbc:DocumentCurrencyCode", &[], ccy);
}
if let Some(c) = &inv.vat_accounting_currency {
x.leaf("cbc:TaxCurrencyCode", &[], c.as_str());
}
opt(
&mut x,
"cbc:AccountingCost",
inv.accounting_reference.as_deref(),
);
opt(&mut x, "cbc:BuyerReference", inv.buyer_reference.as_deref());
if let Some(p) = &inv.invoicing_period {
period(
&mut x,
"cac:InvoicePeriod",
p,
inv.vat_point_date_code.as_ref(),
);
}
if let Some(r) = &inv.purchase_order_reference {
x.group("cac:OrderReference", |x| {
x.leaf("cbc:ID", &[], r.as_str());
if let Some(s) = &inv.sales_order_reference {
x.leaf("cbc:SalesOrderID", &[], s.as_str());
}
});
} else if let Some(s) = &inv.sales_order_reference {
x.group("cac:OrderReference", |x| {
x.leaf("cbc:ID", &[], "");
x.leaf("cbc:SalesOrderID", &[], s.as_str());
});
}
for p in &inv.preceding_invoices {
x.group("cac:BillingReference", |x| {
x.group("cac:InvoiceDocumentReference", |x| {
x.leaf("cbc:ID", &[], p.reference.as_str());
if let Some(d) = p.issue_date {
x.leaf("cbc:IssueDate", &[], &d.to_string());
}
});
});
}
doc_ref(
&mut x,
"cac:DespatchDocumentReference",
inv.despatch_advice_reference.as_ref(),
);
doc_ref(
&mut x,
"cac:ReceiptDocumentReference",
inv.receiving_advice_reference.as_ref(),
);
doc_ref(
&mut x,
"cac:OriginatorDocumentReference",
inv.tender_reference.as_ref(),
);
doc_ref(
&mut x,
"cac:ContractDocumentReference",
inv.contract_reference.as_ref(),
);
for a in &inv.attachments {
attachment(&mut x, a);
}
if let Some(o) = &inv.object_identifier {
x.group("cac:AdditionalDocumentReference", |x| {
ident(x, "cbc:ID", o);
x.leaf("cbc:DocumentTypeCode", &[], "130");
});
}
doc_ref(
&mut x,
"cac:ProjectReference",
inv.project_reference.as_ref(),
);
party(&mut x, "cac:AccountingSupplierParty", &inv.seller);
party(&mut x, "cac:AccountingCustomerParty", &inv.buyer);
if let Some(p) = &inv.payee {
x.group("cac:PayeeParty", |x| {
if let Some(i) = &p.identifier {
x.group("cac:PartyIdentification", |x| ident(x, "cbc:ID", i));
}
if let Some(n) = &p.name {
x.group("cac:PartyName", |x| x.leaf("cbc:Name", &[], n));
}
if let Some(l) = &p.legal_registration {
x.group("cac:PartyLegalEntity", |x| ident(x, "cbc:CompanyID", l));
}
});
}
if let Some(t) = &inv.tax_representative {
x.group("cac:TaxRepresentativeParty", |x| {
if let Some(n) = &t.name {
x.group("cac:PartyName", |x| x.leaf("cbc:Name", &[], n));
}
address(x, &t.address);
if let Some(v) = &t.vat_identifier {
x.group("cac:PartyTaxScheme", |x| {
x.leaf("cbc:CompanyID", &[], v);
x.group("cac:TaxScheme", |x| x.leaf("cbc:ID", &[], "VAT"));
});
}
});
}
if let Some(d) = &inv.delivery {
x.group("cac:Delivery", |x| {
if let Some(date) = d.date {
x.leaf("cbc:ActualDeliveryDate", &[], &date.to_string());
}
if d.location.is_some() || d.address.is_some() {
x.group("cac:DeliveryLocation", |x| {
if let Some(l) = &d.location {
ident(x, "cbc:ID", l);
}
if let Some(a) = &d.address {
x.group("cac:Address", |x| address_body(x, a));
}
});
}
if let Some(n) = &d.party_name {
x.group("cac:DeliveryParty", |x| {
x.group("cac:PartyName", |x| x.leaf("cbc:Name", &[], n));
});
}
});
}
if let Some(p) = &inv.payment {
payment_means(&mut x, p);
if let Some(t) = &inv.payment_terms {
x.group("cac:PaymentTerms", |x| x.leaf("cbc:Note", &[], t));
}
} else if let Some(t) = &inv.payment_terms {
x.group("cac:PaymentTerms", |x| x.leaf("cbc:Note", &[], t));
}
for (a, is_charge) in inv
.allowances
.iter()
.map(|a| (a, false))
.chain(inv.charges.iter().map(|c| (c, true)))
{
doc_allowance(&mut x, a, is_charge, ccy);
}
if !inv.vat_breakdown.is_empty() || inv.totals.vat_total.is_some() {
x.group("cac:TaxTotal", |x| {
if let Some(t) = inv.totals.vat_total {
x.leaf("cbc:TaxAmount", &[("currencyID", ccy)], &amt(t));
}
for b in &inv.vat_breakdown {
x.group("cac:TaxSubtotal", |x| {
x.leaf(
"cbc:TaxableAmount",
&[("currencyID", ccy)],
&amt(b.taxable_amount),
);
x.leaf("cbc:TaxAmount", &[("currencyID", ccy)], &amt(b.tax_amount));
x.group("cac:TaxCategory", |x| {
x.leaf("cbc:ID", &[], b.category.as_str());
if let Some(r) = b.rate {
x.leaf("cbc:Percent", &[], &r.to_string());
}
if let Some(c) = &b.exemption_reason_code {
x.leaf("cbc:TaxExemptionReasonCode", &[], c.as_str());
}
if let Some(r) = &b.exemption_reason {
x.leaf("cbc:TaxExemptionReason", &[], r);
}
x.group("cac:TaxScheme", |x| x.leaf("cbc:ID", &[], "VAT"));
});
});
}
});
}
if let (Some(t), Some(c)) = (
inv.totals.vat_total_accounting,
inv.vat_accounting_currency.as_ref(),
) {
x.group("cac:TaxTotal", |x| {
x.leaf("cbc:TaxAmount", &[("currencyID", c.as_str())], &amt(t));
});
}
let t = &inv.totals;
x.group("cac:LegalMonetaryTotal", |x| {
x.leaf(
"cbc:LineExtensionAmount",
&[("currencyID", ccy)],
&amt(t.line_total),
);
x.leaf(
"cbc:TaxExclusiveAmount",
&[("currencyID", ccy)],
&amt(t.taxable_total),
);
x.leaf(
"cbc:TaxInclusiveAmount",
&[("currencyID", ccy)],
&amt(t.gross_total),
);
if let Some(a) = t.allowance_total {
x.leaf("cbc:AllowanceTotalAmount", &[("currencyID", ccy)], &amt(a));
}
if let Some(c) = t.charge_total {
x.leaf("cbc:ChargeTotalAmount", &[("currencyID", ccy)], &amt(c));
}
if let Some(p) = t.paid {
x.leaf("cbc:PrepaidAmount", &[("currencyID", ccy)], &amt(p));
}
if let Some(r) = t.rounding {
x.leaf("cbc:PayableRoundingAmount", &[("currencyID", ccy)], &amt(r));
}
x.leaf("cbc:PayableAmount", &[("currencyID", ccy)], &amt(t.due));
});
for l in &inv.lines {
line(&mut x, l, credit, ccy);
}
let (xml, dropped) = x.finish();
Written { xml, dropped }
}
fn opt(x: &mut Xml, name: &str, v: Option<&str>) {
if let Some(v) = v {
x.leaf(name, &[], v);
}
}
fn ident(x: &mut Xml, name: &str, i: &Identifier) {
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(s) = i.scheme() {
attrs.push(("schemeID", s));
}
if let Some(v) = i.scheme_version() {
attrs.push(("schemeVersionID", v));
}
x.leaf(name, &attrs, i.content());
}
fn doc_ref(x: &mut Xml, name: &str, r: Option<&DocumentReference>) {
if let Some(r) = r {
x.group(name, |x| x.leaf("cbc:ID", &[], r.as_str()));
}
}
fn period(x: &mut Xml, name: &str, p: &Period, code: Option<&Code>) {
x.group(name, |x| {
if let Some(s) = p.start {
x.leaf("cbc:StartDate", &[], &s.to_string());
}
if let Some(e) = p.end {
x.leaf("cbc:EndDate", &[], &e.to_string());
}
if let Some(c) = code {
x.leaf("cbc:DescriptionCode", &[], c.as_str());
}
});
}
fn attachment(x: &mut Xml, a: &SupportingDocument) {
x.group("cac:AdditionalDocumentReference", |x| {
x.leaf("cbc:ID", &[], a.reference.as_str());
if let Some(d) = &a.description {
x.leaf("cbc:DocumentDescription", &[], d);
}
if a.uri.is_some() || a.attachment.is_some() {
x.group("cac:Attachment", |x| {
if let Some(f) = &a.attachment {
x.leaf(
"cbc:EmbeddedDocumentBinaryObject",
&[("mimeCode", f.mime_code()), ("filename", f.filename())],
&base64(f.content()),
);
}
if let Some(u) = &a.uri {
x.group("cac:ExternalReference", |x| x.leaf("cbc:URI", &[], u));
}
});
}
});
}
fn address(x: &mut Xml, a: &PostalAddress) {
x.group("cac:PostalAddress", |x| address_body(x, a));
}
fn address_body(x: &mut Xml, a: &PostalAddress) {
if let Some(l) = &a.line1 {
x.leaf("cbc:StreetName", &[], l);
}
if let Some(l) = &a.line2 {
x.leaf("cbc:AdditionalStreetName", &[], l);
}
if let Some(c) = &a.city {
x.leaf("cbc:CityName", &[], c);
}
if let Some(p) = &a.post_code {
x.leaf("cbc:PostalZone", &[], p);
}
if let Some(s) = &a.subdivision {
x.leaf("cbc:CountrySubentity", &[], s);
}
if let Some(l) = &a.line3 {
x.group("cac:AddressLine", |x| x.leaf("cbc:Line", &[], l));
}
if let Some(c) = &a.country {
x.group("cac:Country", |x| {
x.leaf("cbc:IdentificationCode", &[], c.as_str());
});
}
}
fn party(x: &mut Xml, wrapper: &str, p: &Party) {
x.group(wrapper, |x| {
x.group("cac:Party", |x| {
if let Some(e) = &p.electronic_address {
ident(x, "cbc:EndpointID", e);
}
for i in &p.identifiers {
x.group("cac:PartyIdentification", |x| ident(x, "cbc:ID", i));
}
if let Some(n) = &p.trading_name {
x.group("cac:PartyName", |x| x.leaf("cbc:Name", &[], n));
}
address(x, &p.address);
if let Some(v) = &p.vat_identifier {
x.group("cac:PartyTaxScheme", |x| {
x.leaf("cbc:CompanyID", &[], v);
x.group("cac:TaxScheme", |x| x.leaf("cbc:ID", &[], "VAT"));
});
}
if let Some(t) = &p.tax_registration {
x.group("cac:PartyTaxScheme", |x| {
x.leaf("cbc:CompanyID", &[], t);
x.group("cac:TaxScheme", |x| x.leaf("cbc:ID", &[], "FC"));
});
}
if p.name.is_some()
|| p.legal_registration.is_some()
|| p.additional_legal_information.is_some()
{
x.group("cac:PartyLegalEntity", |x| {
if let Some(n) = &p.name {
x.leaf("cbc:RegistrationName", &[], n);
}
if let Some(l) = &p.legal_registration {
ident(x, "cbc:CompanyID", l);
}
if let Some(a) = &p.additional_legal_information {
x.leaf("cbc:CompanyLegalForm", &[], a);
}
});
}
let c = &p.contact;
if c.name.is_some() || c.phone.is_some() || c.email.is_some() {
x.group("cac:Contact", |x| {
if let Some(n) = &c.name {
x.leaf("cbc:Name", &[], n);
}
if let Some(t) = &c.phone {
x.leaf("cbc:Telephone", &[], t);
}
if let Some(e) = &c.email {
x.leaf("cbc:ElectronicMail", &[], e);
}
});
}
});
});
}
fn payment_means(x: &mut Xml, p: &PaymentInstructions) {
x.group("cac:PaymentMeans", |x| {
if let Some(c) = &p.means_code {
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(t) = &p.means_text {
attrs.push(("name", t));
}
x.leaf("cbc:PaymentMeansCode", &attrs, c.as_str());
}
if let Some(r) = &p.remittance_information {
x.leaf("cbc:PaymentID", &[], r);
}
match &p.means {
Some(PaymentMeans::Card(c)) => {
x.group("cac:CardAccount", |x| {
if let Some(n) = &c.primary_account_number {
x.leaf("cbc:PrimaryAccountNumberID", &[], n);
}
x.leaf("cbc:NetworkID", &[], "NA");
if let Some(h) = &c.holder_name {
x.leaf("cbc:HolderName", &[], h);
}
});
}
Some(PaymentMeans::CreditTransfer(ts)) => {
for t in ts {
x.group("cac:PayeeFinancialAccount", |x| {
if let Some(a) = &t.account_identifier {
x.leaf("cbc:ID", &[], a);
}
if let Some(n) = &t.account_name {
x.leaf("cbc:Name", &[], n);
}
if let Some(p) = &t.provider_identifier {
x.group("cac:FinancialInstitutionBranch", |x| {
x.leaf("cbc:ID", &[], p);
});
}
});
}
}
Some(PaymentMeans::DirectDebit(d)) => {
x.group("cac:PaymentMandate", |x| {
if let Some(m) = &d.mandate_reference {
x.leaf("cbc:ID", &[], m);
}
if let Some(a) = &d.debited_account {
x.group("cac:PayerFinancialAccount", |x| x.leaf("cbc:ID", &[], a));
}
});
}
None => {}
}
});
}
fn tax_category(x: &mut Xml, v: &LineVat, wrapper: &str) {
x.group(wrapper, |x| {
x.leaf("cbc:ID", &[], v.category.as_str());
if let Some(r) = v.rate {
x.leaf("cbc:Percent", &[], &r.to_string());
}
x.group("cac:TaxScheme", |x| x.leaf("cbc:ID", &[], "VAT"));
});
}
fn doc_allowance(x: &mut Xml, a: &DocumentAllowanceCharge, is_charge: bool, ccy: &str) {
x.group("cac:AllowanceCharge", |x| {
x.leaf(
"cbc:ChargeIndicator",
&[],
if is_charge { "true" } else { "false" },
);
if let Some(c) = &a.reason_code {
x.leaf("cbc:AllowanceChargeReasonCode", &[], c.as_str());
}
if let Some(r) = &a.reason {
x.leaf("cbc:AllowanceChargeReason", &[], r);
}
if let Some(p) = a.percentage {
x.leaf("cbc:MultiplierFactorNumeric", &[], &p.to_string());
}
x.leaf("cbc:Amount", &[("currencyID", ccy)], &amt(a.amount));
if let Some(b) = a.base_amount {
x.leaf("cbc:BaseAmount", &[("currencyID", ccy)], &amt(b));
}
tax_category(x, &a.vat, "cac:TaxCategory");
});
}
fn line_allowance(x: &mut Xml, a: &LineAllowanceCharge, is_charge: bool, ccy: &str) {
x.group("cac:AllowanceCharge", |x| {
x.leaf(
"cbc:ChargeIndicator",
&[],
if is_charge { "true" } else { "false" },
);
if let Some(c) = &a.reason_code {
x.leaf("cbc:AllowanceChargeReasonCode", &[], c.as_str());
}
if let Some(r) = &a.reason {
x.leaf("cbc:AllowanceChargeReason", &[], r);
}
if let Some(p) = a.percentage {
x.leaf("cbc:MultiplierFactorNumeric", &[], &p.to_string());
}
x.leaf("cbc:Amount", &[("currencyID", ccy)], &amt(a.amount));
if let Some(b) = a.base_amount {
x.leaf("cbc:BaseAmount", &[("currencyID", ccy)], &amt(b));
}
});
}
fn line(x: &mut Xml, l: &InvoiceLine, credit: bool, ccy: &str) {
let root = if credit {
"cac:CreditNoteLine"
} else {
"cac:InvoiceLine"
};
let qty_name = if credit {
"cbc:CreditedQuantity"
} else {
"cbc:InvoicedQuantity"
};
x.group(root, |x| {
x.leaf("cbc:ID", &[], &l.id);
if let Some(n) = &l.note {
x.leaf("cbc:Note", &[], n);
}
x.leaf(
qty_name,
&[("unitCode", l.unit_code.as_str())],
&l.quantity.to_string(),
);
x.leaf(
"cbc:LineExtensionAmount",
&[("currencyID", ccy)],
&amt(l.net_amount),
);
if let Some(a) = &l.accounting_reference {
x.leaf("cbc:AccountingCost", &[], a);
}
if let Some(p) = &l.period {
period(x, "cac:InvoicePeriod", p, None);
}
if let Some(o) = &l.order_line_reference {
x.group("cac:OrderLineReference", |x| {
x.leaf("cbc:LineID", &[], o.as_str());
});
}
if let Some(o) = &l.object_identifier {
x.group("cac:DocumentReference", |x| {
ident(x, "cbc:ID", o);
x.leaf("cbc:DocumentTypeCode", &[], "130");
});
}
for (a, c) in l
.allowances
.iter()
.map(|a| (a, false))
.chain(l.charges.iter().map(|c| (c, true)))
{
line_allowance(x, a, c, ccy);
}
item(x, &l.item, &l.vat);
price(x, &l.price, ccy);
});
}
fn item(x: &mut Xml, i: &Item, vat: &LineVat) {
x.group("cac:Item", |x| {
if let Some(d) = &i.description {
x.leaf("cbc:Description", &[], d);
}
if let Some(n) = &i.name {
x.leaf("cbc:Name", &[], n);
}
if let Some(b) = &i.buyer_identifier {
x.group("cac:BuyersItemIdentification", |x| x.leaf("cbc:ID", &[], b));
}
if let Some(s) = &i.seller_identifier {
x.group("cac:SellersItemIdentification", |x| {
x.leaf("cbc:ID", &[], s);
});
}
if let Some(s) = &i.standard_identifier {
x.group("cac:StandardItemIdentification", |x| ident(x, "cbc:ID", s));
}
if let Some(c) = &i.origin_country {
x.group("cac:OriginCountry", |x| {
x.leaf("cbc:IdentificationCode", &[], c.as_str());
});
}
for c in &i.classification_identifiers {
x.group("cac:CommodityClassification", |x| {
let mut attrs: Vec<(&str, &str)> = Vec::new();
if let Some(l) = c.scheme() {
attrs.push(("listID", l));
}
if let Some(v) = c.scheme_version() {
attrs.push(("listVersionID", v));
}
x.leaf("cbc:ItemClassificationCode", &attrs, c.content());
});
}
tax_category(x, vat, "cac:ClassifiedTaxCategory");
for a in &i.attributes {
x.group("cac:AdditionalItemProperty", |x| {
x.leaf("cbc:Name", &[], a.name.as_deref().unwrap_or_default());
x.leaf("cbc:Value", &[], a.value.as_deref().unwrap_or_default());
});
}
});
}
fn price(x: &mut Xml, p: &PriceDetails, ccy: &str) {
x.group("cac:Price", |x| {
x.leaf(
"cbc:PriceAmount",
&[("currencyID", ccy)],
&p.net_price.to_string(),
);
if let Some(q) = p.base_quantity {
let unit = p.base_quantity_code.as_ref().map_or("", Code::as_str);
x.leaf("cbc:BaseQuantity", &[("unitCode", unit)], &q.to_string());
}
if let (Some(d), Some(g)) = (p.price_discount, p.gross_price) {
x.group("cac:AllowanceCharge", |x| {
x.leaf("cbc:ChargeIndicator", &[], "false");
x.leaf("cbc:Amount", &[("currencyID", ccy)], &d.to_string());
x.leaf("cbc:BaseAmount", &[("currencyID", ccy)], &g.to_string());
});
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn children_are_sorted_into_schema_order() {
let mut x = Xml::new("Invoice", vec![], &RULES);
x.leaf("cbc:ID", &[], "1");
x.leaf("cbc:CustomizationID", &[], "urn:x");
let (xml, _) = x.finish();
let cust = xml.find("CustomizationID").expect("present");
let id = xml.find("<cbc:ID>").expect("present");
assert!(cust < id, "CustomizationID must precede ID\n{xml}");
}
#[test]
fn sorting_is_stable_for_repeats() {
let mut x = Xml::new("Invoice", vec![], &RULES);
x.leaf("cbc:Note", &[], "first");
x.leaf("cbc:Note", &[], "second");
let (xml, _) = x.finish();
assert!(
xml.find("first").unwrap() < xml.find("second").unwrap(),
"{xml}"
);
}
#[test]
fn an_unplaceable_element_is_reported() {
let mut x = Xml::new("Invoice", vec![], &RULES);
x.leaf("cbc:NotAUblElement", &[], "x");
let (xml, dropped) = x.finish();
assert!(!xml.contains("NotAUblElement"), "{xml}");
assert_eq!(dropped, ["Invoice/cbc:NotAUblElement"]);
}
#[test]
fn a_credit_note_uses_the_other_root() {
let mut inv = Invoice::default();
inv.kind = DocumentKind::CreditNote;
let out = write(&inv);
assert!(out.xml.contains("<CreditNote "), "{}", out.xml);
assert!(out.xml.contains("CreditNote-2"));
assert!(!out.xml.contains("cbc:InvoiceTypeCode"));
}
}