pint-ae-toolkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,35 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ .pytest_cache/
7
+ .mypy_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+
12
+ # Data — NEVER commit client data
13
+ data/client/
14
+ *.duckdb
15
+ *.parquet
16
+
17
+ # Fabric / Azure
18
+ fabric/.local/
19
+ *.publishsettings
20
+ .env
21
+ .env.*
22
+
23
+ # OS / IDE
24
+ .DS_Store
25
+ .vscode/settings.json
26
+ .idea/
27
+
28
+ # Secrets
29
+ *.pem
30
+ *.key
31
+ secrets/
32
+ docs/sources/**/*.pdf
33
+ docs/sources/**/*.html
34
+
35
+ packages/core/dist/
@@ -0,0 +1,19 @@
1
+ Metadata-Version: 2.5
2
+ Name: pint-ae-toolkit
3
+ Version: 0.1.0
4
+ Summary: PINT AE toolkit - parsing, validation, and diagnostics for UAE e-invoicing
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: lxml>=5.0
7
+ Requires-Dist: pydantic>=2.5
8
+ Requires-Dist: rich>=13.0
9
+ Requires-Dist: typer>=0.12
10
+ Provides-Extra: dev
11
+ Requires-Dist: ipykernel>=6.20; extra == 'dev'
12
+ Requires-Dist: jupyterlab>=4.0; extra == 'dev'
13
+ Requires-Dist: lxml-stubs; extra == 'dev'
14
+ Requires-Dist: mypy; extra == 'dev'
15
+ Requires-Dist: nbformat>=5.7; extra == 'dev'
16
+ Requires-Dist: pre-commit; extra == 'dev'
17
+ Requires-Dist: pytest-cov; extra == 'dev'
18
+ Requires-Dist: pytest>=8; extra == 'dev'
19
+ Requires-Dist: ruff; extra == 'dev'
@@ -0,0 +1,3 @@
1
+ # Core package
2
+
3
+ Parser and types for PINT AE.
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "pint-ae-toolkit"
3
+ version = "0.1.0"
4
+ description = "PINT AE toolkit - parsing, validation, and diagnostics for UAE e-invoicing"
5
+ requires-python = ">=3.11"
6
+ dependencies = ["lxml>=5.0", "pydantic>=2.5", "typer>=0.12", "rich>=13.0"]
7
+
8
+ [project.optional-dependencies]
9
+ dev = ["pytest>=8", "pytest-cov", "ruff", "mypy", "pre-commit", "lxml-stubs", "ipykernel>=6.20", "nbformat>=5.7", "jupyterlab>=4.0"]
10
+
11
+ [project.scripts]
12
+ pint-ae = "pint_ae.cli:app"
13
+
14
+ [build-system]
15
+ requires = ["hatchling"]
16
+ build-backend = "hatchling.build"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ packages = ["src/pint_ae"]
20
+
21
+ [tool.ruff]
22
+ line-length = 100
23
+ target-version = "py311"
24
+
25
+ [tool.ruff.lint]
26
+ select = ["E", "F", "I", "N", "W", "UP", "B"]
27
+
28
+ [tool.ruff.lint.per-file-ignores]
29
+ "src/pint_ae/cli.py" = ["B008"]
30
+
31
+ [tool.mypy]
32
+ strict = true
33
+ python_version = "3.11"
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+ addopts = "--cov=pint_ae --cov-report=term-missing --cov-fail-under=0"
@@ -0,0 +1,74 @@
1
+ """Command-line interface for the pint-ae toolkit."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+ from rich.console import Console
7
+
8
+ from pint_ae.parser import parse
9
+ from pint_ae.rules import validate
10
+
11
+ app = typer.Typer(
12
+ name="pint-ae",
13
+ help="PINT AE toolkit - validate UAE e-invoices against Peppol rules.",
14
+ no_args_is_help=True,
15
+ )
16
+ console = Console()
17
+
18
+
19
+ @app.callback()
20
+ def main() -> None:
21
+ """PINT AE toolkit - validate UAE e-invoices against Peppol rules."""
22
+ pass
23
+
24
+
25
+ @app.command()
26
+ def diagnose(
27
+ files: list[Path] = typer.Argument(..., help="Invoice XML files to validate"),
28
+ verbose: bool = typer.Option(
29
+ False, "--verbose", "-v", help="Show clean invoices too, not just violations"
30
+ ),
31
+ ) -> None:
32
+ """Validate PINT AE invoices against business rules and print a report."""
33
+ parse_errors = 0
34
+ invoices_with_violations = 0
35
+ invoices_clean = 0
36
+
37
+ for file in files:
38
+ try:
39
+ inv = parse(file.read_bytes())
40
+ except Exception as exc:
41
+ parse_errors += 1
42
+ console.print(f"[red]x[/red] {file.name} -- parse error: {exc}")
43
+ continue
44
+
45
+ violations = validate(inv)
46
+ if not violations:
47
+ invoices_clean += 1
48
+ if verbose:
49
+ console.print(f"[green]OK[/green] {file.name} ({inv.invoice_id}) -- clean")
50
+ continue
51
+
52
+ invoices_with_violations += 1
53
+ console.print(
54
+ f"[red]x[/red] {file.name} ({inv.invoice_id}) -- {len(violations)} violation(s)"
55
+ )
56
+ for v in violations:
57
+ console.print(f" [{v.severity}] {v.rule_id}: {v.message}")
58
+ console.print(f" Citation: {v.citation}")
59
+
60
+ total = parse_errors + invoices_with_violations + invoices_clean
61
+ console.print(
62
+ f"\nSummary: {invoices_clean}/{total} clean, "
63
+ f"{invoices_with_violations} with violations, {parse_errors} parse error(s)"
64
+ )
65
+
66
+ if parse_errors:
67
+ raise typer.Exit(code=2)
68
+ if invoices_with_violations:
69
+ raise typer.Exit(code=1)
70
+ raise typer.Exit(code=0)
71
+
72
+
73
+ if __name__ == "__main__":
74
+ app()
@@ -0,0 +1,127 @@
1
+ """Parser for PINT AE invoices (UBL 2.1 XML).
2
+
3
+ Exposes: parse(xml: str | bytes) -> PintAeInvoice
4
+ """
5
+
6
+ from datetime import date
7
+ from decimal import Decimal
8
+
9
+ from lxml import etree
10
+
11
+ from pint_ae.types import LineItem, Party, PintAeInvoice, TaxCategory
12
+
13
+ NSMAP = {
14
+ "inv": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
15
+ "cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
16
+ "cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
17
+ }
18
+
19
+
20
+ def _text(el: etree._Element, xpath: str) -> str:
21
+ found = el.find(xpath, namespaces=NSMAP)
22
+ if found is None or found.text is None:
23
+ raise ValueError(f"Missing required element: {xpath}")
24
+ return str(found.text).strip()
25
+
26
+
27
+ def _opt_text(el: etree._Element, xpath: str) -> str | None:
28
+ found = el.find(xpath, namespaces=NSMAP)
29
+ if found is None or found.text is None:
30
+ return None
31
+ return str(found.text).strip()
32
+
33
+
34
+ def _decimal(el: etree._Element, xpath: str) -> Decimal:
35
+ return Decimal(_text(el, xpath))
36
+
37
+
38
+ def _parse_party(el: etree._Element, base: str) -> Party:
39
+ name = _text(el, f"{base}/cac:Party/cac:PartyName/cbc:Name")
40
+ trn = _text(el, f"{base}/cac:Party/cac:PartyTaxScheme/cbc:CompanyID")
41
+ id_el = el.find(f"{base}/cac:Party/cac:PartyIdentification/cbc:ID", namespaces=NSMAP)
42
+ peppol_id: str | None = None
43
+ if id_el is not None and id_el.text is not None:
44
+ scheme = id_el.get("schemeID")
45
+ peppol_id = f"{scheme}:{id_el.text.strip()}" if scheme else id_el.text.strip()
46
+ country = _opt_text(
47
+ el, f"{base}/cac:Party/cac:PostalAddress/cac:Country/cbc:IdentificationCode"
48
+ )
49
+ return Party(name=name, trn=trn, peppol_id=peppol_id, country_code=country)
50
+
51
+
52
+ def _parse_tax_category(cat_el: etree._Element) -> TaxCategory:
53
+ code = cat_el.findtext("cbc:ID", namespaces=NSMAP)
54
+ pct = cat_el.findtext("cbc:Percent", namespaces=NSMAP)
55
+ if code is None or pct is None:
56
+ raise ValueError("TaxCategory missing cbc:ID or cbc:Percent")
57
+ return TaxCategory(code=code.strip(), percent=Decimal(pct.strip()))
58
+
59
+
60
+ def _parse_line(line_el: etree._Element) -> LineItem:
61
+ line_id = _text(line_el, "cbc:ID")
62
+ desc = _text(line_el, "cac:Item/cbc:Name")
63
+ qty_el = line_el.find("cbc:InvoicedQuantity", namespaces=NSMAP)
64
+ if qty_el is None or qty_el.text is None:
65
+ raise ValueError("InvoiceLine missing cbc:InvoicedQuantity")
66
+ qty = Decimal(qty_el.text.strip())
67
+ unit = qty_el.get("unitCode", "EA")
68
+ price = _decimal(line_el, "cac:Price/cbc:PriceAmount")
69
+ total = _decimal(line_el, "cbc:LineExtensionAmount")
70
+ cat_el = line_el.find("cac:Item/cac:ClassifiedTaxCategory", namespaces=NSMAP)
71
+ if cat_el is None:
72
+ raise ValueError("InvoiceLine missing cac:ClassifiedTaxCategory")
73
+ return LineItem(
74
+ line_id=line_id,
75
+ description=desc,
76
+ quantity=qty,
77
+ unit_code=unit,
78
+ unit_price=price,
79
+ line_total=total,
80
+ tax_category=_parse_tax_category(cat_el),
81
+ )
82
+
83
+
84
+ def parse(xml: str | bytes) -> PintAeInvoice:
85
+ """Parse a PINT AE UBL 2.1 XML invoice into a PintAeInvoice."""
86
+ if isinstance(xml, str):
87
+ xml = xml.encode("utf-8")
88
+ root = etree.fromstring(xml)
89
+
90
+ supplier = _parse_party(root, "cac:AccountingSupplierParty")
91
+ customer = _parse_party(root, "cac:AccountingCustomerParty")
92
+
93
+ tax_cats: list[TaxCategory] = []
94
+ for sub in root.findall("cac:TaxTotal/cac:TaxSubtotal", namespaces=NSMAP):
95
+ cat_el = sub.find("cac:TaxCategory", namespaces=NSMAP)
96
+ if cat_el is None:
97
+ raise ValueError("TaxSubtotal missing cac:TaxCategory")
98
+ base = _parse_tax_category(cat_el)
99
+ taxable = sub.findtext("cbc:TaxableAmount", namespaces=NSMAP)
100
+ tax_amt = sub.findtext("cbc:TaxAmount", namespaces=NSMAP)
101
+ tax_cats.append(
102
+ TaxCategory(
103
+ code=base.code,
104
+ percent=base.percent,
105
+ taxable_amount=Decimal(taxable.strip()) if taxable else None,
106
+ tax_amount=Decimal(tax_amt.strip()) if tax_amt else None,
107
+ )
108
+ )
109
+
110
+ line_items = [_parse_line(line) for line in root.findall("cac:InvoiceLine", namespaces=NSMAP)]
111
+
112
+ return PintAeInvoice(
113
+ customization_id=_text(root, "cbc:CustomizationID"),
114
+ profile_id=_text(root, "cbc:ProfileID"),
115
+ invoice_id=_text(root, "cbc:ID"),
116
+ issue_date=date.fromisoformat(_text(root, "cbc:IssueDate")),
117
+ invoice_type_code=_text(root, "cbc:InvoiceTypeCode"),
118
+ document_currency_code=_text(root, "cbc:DocumentCurrencyCode"),
119
+ supplier=supplier,
120
+ customer=customer,
121
+ tax_categories=tax_cats,
122
+ line_items=line_items,
123
+ line_extension_amount=_decimal(root, "cac:LegalMonetaryTotal/cbc:LineExtensionAmount"),
124
+ tax_exclusive_amount=_decimal(root, "cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount"),
125
+ tax_inclusive_amount=_decimal(root, "cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount"),
126
+ payable_amount=_decimal(root, "cac:LegalMonetaryTotal/cbc:PayableAmount"),
127
+ )
@@ -0,0 +1,142 @@
1
+ """PINT AE business rules validator.
2
+
3
+ Each rule is a pure function: PintAeInvoice -> list[RuleViolation].
4
+ Rules are registered in _RULES; validate() runs all of them.
5
+
6
+ Every rule cites a BR-* number from Peppol BIS Billing 3.0. PINT AE
7
+ inherits these rules unchanged unless noted in the PINT AE BIS.
8
+ """
9
+
10
+ from dataclasses import dataclass
11
+ from decimal import Decimal
12
+
13
+ from pint_ae.types import PintAeInvoice
14
+
15
+
16
+ @dataclass
17
+ class RuleViolation:
18
+ """A single rule violation on an invoice."""
19
+
20
+ rule_id: str
21
+ citation: str
22
+ field: str
23
+ message: str
24
+ severity: str = "error"
25
+
26
+
27
+ def rule_line_sum(inv: PintAeInvoice) -> list[RuleViolation]:
28
+ """R-001 / BT-106 / BR-CO-10: line_extension_amount == sum of line totals."""
29
+ expected = sum((item.line_total for item in inv.line_items), start=Decimal("0"))
30
+ if inv.line_extension_amount != expected:
31
+ return [
32
+ RuleViolation(
33
+ rule_id="R-001",
34
+ citation="Peppol BIS Billing 3.0 BR-CO-10 (BT-106)",
35
+ field="line_extension_amount",
36
+ message=(
37
+ f"line_extension_amount {inv.line_extension_amount} does not equal "
38
+ f"sum of line totals {expected}"
39
+ ),
40
+ )
41
+ ]
42
+ return []
43
+
44
+
45
+ def rule_tax_exclusive_sum(inv: PintAeInvoice) -> list[RuleViolation]:
46
+ """R-002 / BT-109 / BR-CO-13: tax_exclusive_amount == sum of taxable amounts."""
47
+ expected = sum(
48
+ (cat.taxable_amount or Decimal("0") for cat in inv.tax_categories),
49
+ start=Decimal("0"),
50
+ )
51
+ if inv.tax_exclusive_amount != expected:
52
+ return [
53
+ RuleViolation(
54
+ rule_id="R-002",
55
+ citation="Peppol BIS Billing 3.0 BR-CO-13 (BT-109)",
56
+ field="tax_exclusive_amount",
57
+ message=(
58
+ f"tax_exclusive_amount {inv.tax_exclusive_amount} does not equal "
59
+ f"sum of taxable amounts {expected}"
60
+ ),
61
+ )
62
+ ]
63
+ return []
64
+
65
+
66
+ def rule_payable_amount(inv: PintAeInvoice) -> list[RuleViolation]:
67
+ """R-003 / BT-115 / BR-CO-15: payable_amount == tax_inclusive_amount."""
68
+ if inv.payable_amount != inv.tax_inclusive_amount:
69
+ return [
70
+ RuleViolation(
71
+ rule_id="R-003",
72
+ citation="Peppol BIS Billing 3.0 BR-CO-15 (BT-115)",
73
+ field="payable_amount",
74
+ message=(
75
+ f"payable_amount {inv.payable_amount} does not equal "
76
+ f"tax_inclusive_amount {inv.tax_inclusive_amount}"
77
+ ),
78
+ )
79
+ ]
80
+ return []
81
+
82
+
83
+ def rule_total_tax(inv: PintAeInvoice) -> list[RuleViolation]:
84
+ """R-004 / BT-110 / BR-CO-14: sum of tax amounts == payable - tax_exclusive."""
85
+ total_tax = sum(
86
+ (cat.tax_amount or Decimal("0") for cat in inv.tax_categories),
87
+ start=Decimal("0"),
88
+ )
89
+ expected = inv.payable_amount - inv.tax_exclusive_amount
90
+ if total_tax != expected:
91
+ return [
92
+ RuleViolation(
93
+ rule_id="R-004",
94
+ citation="Peppol BIS Billing 3.0 BR-CO-14 (BT-110)",
95
+ field="tax_categories",
96
+ message=(
97
+ f"sum of tax_amounts {total_tax} does not equal "
98
+ f"payable - tax_exclusive {expected}"
99
+ ),
100
+ )
101
+ ]
102
+ return []
103
+
104
+
105
+ def rule_vat_rate_consistency(inv: PintAeInvoice) -> list[RuleViolation]:
106
+ """R-005 / BT-152 / BR-S-05: standard rate must be 5.00%, zero rate 0.00%."""
107
+ expected_rates = {
108
+ "S": Decimal("5.00"),
109
+ "Z": Decimal("0.00"),
110
+ }
111
+ violations: list[RuleViolation] = []
112
+ for cat in inv.tax_categories:
113
+ if cat.code in expected_rates and cat.percent != expected_rates[cat.code]:
114
+ violations.append(
115
+ RuleViolation(
116
+ rule_id="R-005",
117
+ citation="Peppol BIS Billing 3.0 BR-S-05 (BT-152)",
118
+ field=f"tax_category[{cat.code}].percent",
119
+ message=(
120
+ f"VAT category {cat.code} requires percent "
121
+ f"{expected_rates[cat.code]}, found {cat.percent}"
122
+ ),
123
+ )
124
+ )
125
+ return violations
126
+
127
+
128
+ _RULES = [
129
+ rule_line_sum,
130
+ rule_tax_exclusive_sum,
131
+ rule_payable_amount,
132
+ rule_total_tax,
133
+ rule_vat_rate_consistency,
134
+ ]
135
+
136
+
137
+ def validate(inv: PintAeInvoice) -> list[RuleViolation]:
138
+ """Run all registered rules against an invoice; return all violations."""
139
+ violations: list[RuleViolation] = []
140
+ for rule in _RULES:
141
+ violations.extend(rule(inv))
142
+ return violations
@@ -0,0 +1,62 @@
1
+ """Pydantic models for PINT AE invoices (UBL 2.1 subset).
2
+
3
+ Field names follow the PINT AE BIS 1.0.4 business terms in snake_case.
4
+ All monetary and quantity values use Decimal for tax-grade precision.
5
+
6
+ See: docs/learning/03_pydantic_for_tax_data.md
7
+ Spec: PINT AE Billing 1.0.4 - https://docs.peppol.eu/poac/ae/upcoming/pint-ae/bis/
8
+ """
9
+
10
+ from datetime import date
11
+ from decimal import Decimal
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+
16
+ class Party(BaseModel):
17
+ """A supplier or customer in a PINT AE invoice."""
18
+
19
+ name: str = Field(..., min_length=1, max_length=200)
20
+ trn: str = Field(..., min_length=15, max_length=15, pattern=r"^\d{15}$")
21
+ peppol_id: str | None = Field(default=None)
22
+ country_code: str | None = Field(default=None, min_length=2, max_length=2)
23
+
24
+
25
+ class TaxCategory(BaseModel):
26
+ """A VAT tax category (S, Z, E, O, or AE)."""
27
+
28
+ code: str = Field(..., min_length=1, max_length=2, pattern=r"^(S|Z|E|O|AE)$")
29
+ percent: Decimal = Field(..., ge=0, le=100)
30
+ taxable_amount: Decimal | None = Field(default=None, ge=0)
31
+ tax_amount: Decimal | None = Field(default=None, ge=0)
32
+
33
+
34
+ class LineItem(BaseModel):
35
+ """A single invoice line item."""
36
+
37
+ line_id: str = Field(..., min_length=1)
38
+ description: str = Field(..., min_length=1, max_length=500)
39
+ quantity: Decimal = Field(..., gt=0)
40
+ unit_code: str = Field(default="EA", min_length=1, max_length=3)
41
+ unit_price: Decimal = Field(..., ge=0)
42
+ line_total: Decimal = Field(..., ge=0)
43
+ tax_category: TaxCategory
44
+
45
+
46
+ class PintAeInvoice(BaseModel):
47
+ """A PINT AE invoice (subset of the 51 mandatory fields)."""
48
+
49
+ customization_id: str = Field(..., pattern=r"^urn:peppol:pint:billing-1@ae-1$")
50
+ profile_id: str = Field(default="urn:peppol:bis:billing")
51
+ invoice_id: str = Field(..., min_length=1, max_length=50)
52
+ issue_date: date
53
+ invoice_type_code: str = Field(..., pattern=r"^\d{3}$")
54
+ document_currency_code: str = Field(..., min_length=3, max_length=3)
55
+ supplier: Party
56
+ customer: Party
57
+ tax_categories: list[TaxCategory] = Field(..., min_length=1)
58
+ line_items: list[LineItem] = Field(..., min_length=1)
59
+ line_extension_amount: Decimal = Field(..., ge=0)
60
+ tax_exclusive_amount: Decimal = Field(..., ge=0)
61
+ tax_inclusive_amount: Decimal = Field(..., ge=0)
62
+ payable_amount: Decimal = Field(..., ge=0)
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,80 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
3
+ xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
4
+ xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
5
+ <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
6
+ <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
7
+ <cbc:ID>INV-2026-001</cbc:ID>
8
+ <cbc:IssueDate>2026-01-15</cbc:IssueDate>
9
+ <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
10
+ <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
11
+ <cac:AccountingSupplierParty>
12
+ <cac:Party>
13
+ <cac:PartyIdentification>
14
+ <cbc:ID schemeID="0235">100123456700003</cbc:ID>
15
+ </cac:PartyIdentification>
16
+ <cac:PartyName>
17
+ <cbc:Name>Supplier LLC</cbc:Name>
18
+ </cac:PartyName>
19
+ <cac:PostalAddress>
20
+ <cbc:StreetName>Sheikh Zayed Road</cbc:StreetName>
21
+ <cbc:CityName>Dubai</cbc:CityName>
22
+ <cac:Country>
23
+ <cbc:IdentificationCode>AE</cbc:IdentificationCode>
24
+ </cac:Country>
25
+ </cac:PostalAddress>
26
+ <cac:PartyTaxScheme>
27
+ <cbc:CompanyID>100123456700003</cbc:CompanyID>
28
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
29
+ </cac:PartyTaxScheme>
30
+ </cac:Party>
31
+ </cac:AccountingSupplierParty>
32
+ <cac:AccountingCustomerParty>
33
+ <cac:Party>
34
+ <cac:PartyIdentification>
35
+ <cbc:ID schemeID="0235">100987654300003</cbc:ID>
36
+ </cac:PartyIdentification>
37
+ <cac:PartyName>
38
+ <cbc:Name>Customer LLC</cbc:Name>
39
+ </cac:PartyName>
40
+ <cac:PartyTaxScheme>
41
+ <cbc:CompanyID>100987654300003</cbc:CompanyID>
42
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
43
+ </cac:PartyTaxScheme>
44
+ </cac:Party>
45
+ </cac:AccountingCustomerParty>
46
+ <cac:TaxTotal>
47
+ <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
48
+ <cac:TaxSubtotal>
49
+ <cbc:TaxableAmount currencyID="AED">1000.00</cbc:TaxableAmount>
50
+ <cbc:TaxAmount currencyID="AED">50.00</cbc:TaxAmount>
51
+ <cac:TaxCategory>
52
+ <cbc:ID>S</cbc:ID>
53
+ <cbc:Percent>5.00</cbc:Percent>
54
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
55
+ </cac:TaxCategory>
56
+ </cac:TaxSubtotal>
57
+ </cac:TaxTotal>
58
+ <cac:LegalMonetaryTotal>
59
+ <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
60
+ <cbc:TaxExclusiveAmount currencyID="AED">1000.00</cbc:TaxExclusiveAmount>
61
+ <cbc:TaxInclusiveAmount currencyID="AED">1050.00</cbc:TaxInclusiveAmount>
62
+ <cbc:PayableAmount currencyID="AED">1050.00</cbc:PayableAmount>
63
+ </cac:LegalMonetaryTotal>
64
+ <cac:InvoiceLine>
65
+ <cbc:ID>1</cbc:ID>
66
+ <cbc:InvoicedQuantity unitCode="EA">10</cbc:InvoicedQuantity>
67
+ <cbc:LineExtensionAmount currencyID="AED">1000.00</cbc:LineExtensionAmount>
68
+ <cac:Item>
69
+ <cbc:Name>Widget</cbc:Name>
70
+ <cac:ClassifiedTaxCategory>
71
+ <cbc:ID>S</cbc:ID>
72
+ <cbc:Percent>5.00</cbc:Percent>
73
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
74
+ </cac:ClassifiedTaxCategory>
75
+ </cac:Item>
76
+ <cac:Price>
77
+ <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
78
+ </cac:Price>
79
+ </cac:InvoiceLine>
80
+ </Invoice>
@@ -0,0 +1,69 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
3
+ xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
4
+ xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
5
+ <cbc:CustomizationID>urn:peppol:pint:billing-1@ae-1</cbc:CustomizationID>
6
+ <cbc:ProfileID>urn:peppol:bis:billing</cbc:ProfileID>
7
+ <cbc:ID>CN-2026-001</cbc:ID>
8
+ <cbc:IssueDate>2026-01-20</cbc:IssueDate>
9
+ <cbc:InvoiceTypeCode>381</cbc:InvoiceTypeCode>
10
+ <cbc:DocumentCurrencyCode>AED</cbc:DocumentCurrencyCode>
11
+ <cac:AccountingSupplierParty>
12
+ <cac:Party>
13
+ <cac:PartyIdentification>
14
+ <cbc:ID schemeID="0235">100123456700003</cbc:ID>
15
+ </cac:PartyIdentification>
16
+ <cac:PartyName><cbc:Name>Supplier LLC</cbc:Name></cac:PartyName>
17
+ <cac:PartyTaxScheme>
18
+ <cbc:CompanyID>100123456700003</cbc:CompanyID>
19
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
20
+ </cac:PartyTaxScheme>
21
+ </cac:Party>
22
+ </cac:AccountingSupplierParty>
23
+ <cac:AccountingCustomerParty>
24
+ <cac:Party>
25
+ <cac:PartyIdentification>
26
+ <cbc:ID schemeID="0235">100987654300003</cbc:ID>
27
+ </cac:PartyIdentification>
28
+ <cac:PartyName><cbc:Name>Customer LLC</cbc:Name></cac:PartyName>
29
+ <cac:PartyTaxScheme>
30
+ <cbc:CompanyID>100987654300003</cbc:CompanyID>
31
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
32
+ </cac:PartyTaxScheme>
33
+ </cac:Party>
34
+ </cac:AccountingCustomerParty>
35
+ <cac:TaxTotal>
36
+ <cbc:TaxAmount currencyID="AED">25.00</cbc:TaxAmount>
37
+ <cac:TaxSubtotal>
38
+ <cbc:TaxableAmount currencyID="AED">500.00</cbc:TaxableAmount>
39
+ <cbc:TaxAmount currencyID="AED">25.00</cbc:TaxAmount>
40
+ <cac:TaxCategory>
41
+ <cbc:ID>S</cbc:ID>
42
+ <cbc:Percent>5.00</cbc:Percent>
43
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
44
+ </cac:TaxCategory>
45
+ </cac:TaxSubtotal>
46
+ </cac:TaxTotal>
47
+ <cac:LegalMonetaryTotal>
48
+ <cbc:LineExtensionAmount currencyID="AED">500.00</cbc:LineExtensionAmount>
49
+ <cbc:TaxExclusiveAmount currencyID="AED">500.00</cbc:TaxExclusiveAmount>
50
+ <cbc:TaxInclusiveAmount currencyID="AED">525.00</cbc:TaxInclusiveAmount>
51
+ <cbc:PayableAmount currencyID="AED">525.00</cbc:PayableAmount>
52
+ </cac:LegalMonetaryTotal>
53
+ <cac:InvoiceLine>
54
+ <cbc:ID>1</cbc:ID>
55
+ <cbc:InvoicedQuantity unitCode="EA">5</cbc:InvoicedQuantity>
56
+ <cbc:LineExtensionAmount currencyID="AED">500.00</cbc:LineExtensionAmount>
57
+ <cac:Item>
58
+ <cbc:Name>Widget - returned</cbc:Name>
59
+ <cac:ClassifiedTaxCategory>
60
+ <cbc:ID>S</cbc:ID>
61
+ <cbc:Percent>5.00</cbc:Percent>
62
+ <cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
63
+ </cac:ClassifiedTaxCategory>
64
+ </cac:Item>
65
+ <cac:Price>
66
+ <cbc:PriceAmount currencyID="AED">100.00</cbc:PriceAmount>
67
+ </cac:Price>
68
+ </cac:InvoiceLine>
69
+ </Invoice>