pint-ae-toolkit 0.1.0__py3-none-any.whl
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.
- pint_ae/__init__.py +1 -0
- pint_ae/cli.py +74 -0
- pint_ae/parser.py +127 -0
- pint_ae/rules.py +142 -0
- pint_ae/types.py +62 -0
- pint_ae_toolkit-0.1.0.dist-info/METADATA +19 -0
- pint_ae_toolkit-0.1.0.dist-info/RECORD +9 -0
- pint_ae_toolkit-0.1.0.dist-info/WHEEL +4 -0
- pint_ae_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
pint_ae/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
pint_ae/cli.py
ADDED
|
@@ -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()
|
pint_ae/parser.py
ADDED
|
@@ -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
|
+
)
|
pint_ae/rules.py
ADDED
|
@@ -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
|
pint_ae/types.py
ADDED
|
@@ -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,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,9 @@
|
|
|
1
|
+
pint_ae/__init__.py,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
|
|
2
|
+
pint_ae/cli.py,sha256=8CtmT9Ll82Z9zI_Hx08v2O-EYuZRgxzoSsAAurd1a-0,2111
|
|
3
|
+
pint_ae/parser.py,sha256=lpw9IIUgwnhkVgNaxzEOjp5SeJE9M_FE48HwwpvBIlE,5091
|
|
4
|
+
pint_ae/rules.py,sha256=0knGcMt6c4BfpmcOqet8kkng3ZEo_simq8-TVYweAbI,4705
|
|
5
|
+
pint_ae/types.py,sha256=SNmrqTbvjqci44HLDuTSLBsEmMghKOvPhB6mh_3wB4A,2341
|
|
6
|
+
pint_ae_toolkit-0.1.0.dist-info/METADATA,sha256=R8PuXIKcmJo0lKNoj1L4jTZBEbfo-umrNh8EViLsiCc,672
|
|
7
|
+
pint_ae_toolkit-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
pint_ae_toolkit-0.1.0.dist-info/entry_points.txt,sha256=l66GNbxaGkpla7naFnLOW892PnxJjXBG4SPTaNhbhLA,44
|
|
9
|
+
pint_ae_toolkit-0.1.0.dist-info/RECORD,,
|