verifactu-validator 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.
Files changed (28) hide show
  1. verifactu_validator-0.1.0/LICENSE +21 -0
  2. verifactu_validator-0.1.0/MANIFEST.in +5 -0
  3. verifactu_validator-0.1.0/PKG-INFO +152 -0
  4. verifactu_validator-0.1.0/README.md +115 -0
  5. verifactu_validator-0.1.0/pyproject.toml +86 -0
  6. verifactu_validator-0.1.0/setup.cfg +4 -0
  7. verifactu_validator-0.1.0/src/verifactu_validator/__init__.py +35 -0
  8. verifactu_validator-0.1.0/src/verifactu_validator/cli.py +201 -0
  9. verifactu_validator-0.1.0/src/verifactu_validator/hash.py +105 -0
  10. verifactu_validator-0.1.0/src/verifactu_validator/nif.py +120 -0
  11. verifactu_validator-0.1.0/src/verifactu_validator/py.typed +0 -0
  12. verifactu_validator-0.1.0/src/verifactu_validator/qr.py +96 -0
  13. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/PKG-INFO +152 -0
  14. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/SOURCES.txt +26 -0
  15. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/dependency_links.txt +1 -0
  16. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/entry_points.txt +2 -0
  17. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/requires.txt +7 -0
  18. verifactu_validator-0.1.0/src/verifactu_validator.egg-info/top_level.txt +1 -0
  19. verifactu_validator-0.1.0/tests/__init__.py +0 -0
  20. verifactu_validator-0.1.0/tests/fixtures/invoice_01_simple.xml +43 -0
  21. verifactu_validator-0.1.0/tests/fixtures/invoice_02_individual.xml +28 -0
  22. verifactu_validator-0.1.0/tests/fixtures/invoice_03_bad_nif.xml +23 -0
  23. verifactu_validator-0.1.0/tests/fixtures/invoice_04_missing_total.xml +20 -0
  24. verifactu_validator-0.1.0/tests/fixtures/invoice_05_malformed.xml +7 -0
  25. verifactu_validator-0.1.0/tests/test_cli.py +114 -0
  26. verifactu_validator-0.1.0/tests/test_hash.py +82 -0
  27. verifactu_validator-0.1.0/tests/test_nif.py +124 -0
  28. verifactu_validator-0.1.0/tests/test_qr.py +56 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FlexiGoTech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include LICENSE
3
+ include pyproject.toml
4
+ recursive-include src/verifactu_validator py.typed
5
+ recursive-include tests *.py *.xml
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: verifactu-validator
3
+ Version: 0.1.0
4
+ Summary: Spanish e-invoice (Veri*Factu / AEAT) validator: NIF/CIF/NIE checksum, SHA-256 chained hash, and QR URL builder per Spanish Tax Agency spec.
5
+ Author-email: FlexiGoTech <comercial@flexigobe.com>
6
+ Maintainer-email: FlexiGoTech <comercial@flexigobe.com>
7
+ License: MIT
8
+ Project-URL: Homepage, https://flexigotech.com/verifactu
9
+ Project-URL: Documentation, https://flexigotech.com/verifactu
10
+ Project-URL: Repository, https://github.com/Flexigobe/verifactu-validator
11
+ Project-URL: Bug Tracker, https://github.com/Flexigobe/verifactu-validator/issues
12
+ Project-URL: Commercial Odoo Module, https://flexigotech.com/verifactu
13
+ Keywords: aeat,verifactu,spain,einvoice,odoo,facturae,compliance,invoice,sii,tributaria
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Financial and Insurance Industry
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Natural Language :: Spanish
26
+ Classifier: Natural Language :: English
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.4; extra == "dev"
32
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
33
+ Requires-Dist: ruff>=0.4; extra == "dev"
34
+ Requires-Dist: build>=1.0; extra == "dev"
35
+ Requires-Dist: twine>=4.0; extra == "dev"
36
+ Dynamic: license-file
37
+
38
+ # verifactu-validator — Spanish AEAT Veri*Factu Invoice Validator
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/verifactu-validator.svg)](https://pypi.org/project/verifactu-validator/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/verifactu-validator.svg)](https://pypi.org/project/verifactu-validator/)
42
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
+ [![CI](https://github.com/Flexigobe/verifactu-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/Flexigobe/verifactu-validator/actions/workflows/ci.yml)
44
+
45
+ > **Days until Veri\*Factu becomes mandatory: 29** (target date: **2026-07-01**)
46
+ >
47
+ > Spanish anti-fraud invoicing law (RD 1007/2023) requires every B2B/B2C invoice
48
+ > issued in Spain to be signed with a chained SHA-256 hash and to embed a QR
49
+ > code pointing to the AEAT verification endpoint.
50
+
51
+ `verifactu-validator` is a tiny, **zero-dependency** Python library that lets you:
52
+
53
+ 1. Validate Spanish **NIF / CIF / NIE** identifiers (real checksum algorithm).
54
+ 2. Build the **SHA-256 chained hash** of an invoice exactly as the AEAT spec
55
+ requires (previous-hash + invoice fields, hex output).
56
+ 3. Build the **Veri\*Factu QR URL** pointing to the AEAT TIKE-CONT endpoint.
57
+ 4. Run a **CLI**: `verifactu validate invoice.xml` → prints OK/FAIL per rule.
58
+
59
+ ## Quick start
60
+
61
+ ```python
62
+ from verifactu_validator import validate_nif, build_invoice_hash, build_qr_url
63
+
64
+ # 1. Validate a Spanish tax ID
65
+ assert validate_nif("12345678Z") # individual NIF
66
+ assert validate_nif("B12345678") # company CIF (example)
67
+
68
+ # 2. Build a chained SHA-256 hash (AEAT Veri*Factu spec)
69
+ h = build_invoice_hash(
70
+ previous_hash="0" * 64,
71
+ issuer_nif="B12345678",
72
+ invoice_number="F2026/0001",
73
+ issue_date="2026-07-01",
74
+ total_amount="121.00",
75
+ )
76
+
77
+ # 3. Build the QR URL embedded in the printed invoice
78
+ url = build_qr_url(
79
+ issuer_nif="B12345678",
80
+ invoice_number="F2026/0001",
81
+ issue_date="2026-07-01",
82
+ total_amount="121.00",
83
+ )
84
+ print(url)
85
+ # https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345678&numserie=F2026%2F0001&fecha=01-07-2026&importe=121.00
86
+ ```
87
+
88
+ CLI usage:
89
+
90
+ ```bash
91
+ verifactu --help
92
+ verifactu validate path/to/invoice.xml
93
+ verifactu nif 12345678Z
94
+ verifactu hash --prev 000...0 --nif B12345678 --num F2026/0001 --date 2026-07-01 --total 121.00
95
+ verifactu qr --nif B12345678 --num F2026/0001 --date 2026-07-01 --total 121.00
96
+ ```
97
+
98
+ ## Why this library exists
99
+
100
+ The Veri\*Factu mandate is the biggest compliance shake-up Spanish SMBs have
101
+ seen in a decade. Most accounting / ERP teams need a **drop-in helper** to:
102
+
103
+ - Pre-flight customer master data (NIF/CIF columns are full of typos).
104
+ - Compute the chained hash before posting an invoice.
105
+ - Generate the QR PNG that goes on the printed PDF.
106
+
107
+ This library covers exactly that surface. It is **deliberately small** so it
108
+ can be vendored, audited, and certified without pulling a giant dependency
109
+ tree into your ERP.
110
+
111
+ ## Full Odoo module: €369 one-off
112
+
113
+ This validator is the open-source nucleus of our commercial Odoo app:
114
+
115
+ > **Veri\*Factu for Odoo 17 / 18 / 19** — full submission to AEAT,
116
+ > chained hash storage, QR on every PDF report, audit log, declarative
117
+ > re-send, multi-company, ready for July 2026.
118
+ >
119
+ > **€369 one-off — flexigotech.com/verifactu**
120
+
121
+ If you just need the algorithms, this PyPI package is MIT-licensed and free
122
+ forever. If you need the full ERP integration, buy the Odoo module.
123
+
124
+ ## Installation
125
+
126
+ ```bash
127
+ pip install verifactu-validator
128
+ ```
129
+
130
+ Requires Python **3.10+**. Zero runtime dependencies.
131
+
132
+ ## Publishing (for maintainers)
133
+
134
+ ```bash
135
+ pip install build twine
136
+ python -m build
137
+ twine upload dist/*
138
+ ```
139
+
140
+ A GitHub Actions workflow (`.github/workflows/ci.yml`) runs the test suite on
141
+ every push and pull request.
142
+
143
+ ## License
144
+
145
+ MIT © 2026 [FlexiGoTech](https://flexigotech.com)
146
+
147
+ ---
148
+
149
+ Made by **FlexiGoTech** in Barcelona. We build Odoo App Store modules for
150
+ Spanish, French, German and EU compliance: Veri\*Factu, France PDP,
151
+ XRechnung, Whistleblowing, Mirakl/Decathlon connectors, AliExpress
152
+ connector. See the full catalogue at [flexigotech.com](https://flexigotech.com).
@@ -0,0 +1,115 @@
1
+ # verifactu-validator — Spanish AEAT Veri*Factu Invoice Validator
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/verifactu-validator.svg)](https://pypi.org/project/verifactu-validator/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/verifactu-validator.svg)](https://pypi.org/project/verifactu-validator/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![CI](https://github.com/Flexigobe/verifactu-validator/actions/workflows/ci.yml/badge.svg)](https://github.com/Flexigobe/verifactu-validator/actions/workflows/ci.yml)
7
+
8
+ > **Days until Veri\*Factu becomes mandatory: 29** (target date: **2026-07-01**)
9
+ >
10
+ > Spanish anti-fraud invoicing law (RD 1007/2023) requires every B2B/B2C invoice
11
+ > issued in Spain to be signed with a chained SHA-256 hash and to embed a QR
12
+ > code pointing to the AEAT verification endpoint.
13
+
14
+ `verifactu-validator` is a tiny, **zero-dependency** Python library that lets you:
15
+
16
+ 1. Validate Spanish **NIF / CIF / NIE** identifiers (real checksum algorithm).
17
+ 2. Build the **SHA-256 chained hash** of an invoice exactly as the AEAT spec
18
+ requires (previous-hash + invoice fields, hex output).
19
+ 3. Build the **Veri\*Factu QR URL** pointing to the AEAT TIKE-CONT endpoint.
20
+ 4. Run a **CLI**: `verifactu validate invoice.xml` → prints OK/FAIL per rule.
21
+
22
+ ## Quick start
23
+
24
+ ```python
25
+ from verifactu_validator import validate_nif, build_invoice_hash, build_qr_url
26
+
27
+ # 1. Validate a Spanish tax ID
28
+ assert validate_nif("12345678Z") # individual NIF
29
+ assert validate_nif("B12345678") # company CIF (example)
30
+
31
+ # 2. Build a chained SHA-256 hash (AEAT Veri*Factu spec)
32
+ h = build_invoice_hash(
33
+ previous_hash="0" * 64,
34
+ issuer_nif="B12345678",
35
+ invoice_number="F2026/0001",
36
+ issue_date="2026-07-01",
37
+ total_amount="121.00",
38
+ )
39
+
40
+ # 3. Build the QR URL embedded in the printed invoice
41
+ url = build_qr_url(
42
+ issuer_nif="B12345678",
43
+ invoice_number="F2026/0001",
44
+ issue_date="2026-07-01",
45
+ total_amount="121.00",
46
+ )
47
+ print(url)
48
+ # https://prewww2.aeat.es/wlpl/TIKE-CONT/ValidarQR?nif=B12345678&numserie=F2026%2F0001&fecha=01-07-2026&importe=121.00
49
+ ```
50
+
51
+ CLI usage:
52
+
53
+ ```bash
54
+ verifactu --help
55
+ verifactu validate path/to/invoice.xml
56
+ verifactu nif 12345678Z
57
+ verifactu hash --prev 000...0 --nif B12345678 --num F2026/0001 --date 2026-07-01 --total 121.00
58
+ verifactu qr --nif B12345678 --num F2026/0001 --date 2026-07-01 --total 121.00
59
+ ```
60
+
61
+ ## Why this library exists
62
+
63
+ The Veri\*Factu mandate is the biggest compliance shake-up Spanish SMBs have
64
+ seen in a decade. Most accounting / ERP teams need a **drop-in helper** to:
65
+
66
+ - Pre-flight customer master data (NIF/CIF columns are full of typos).
67
+ - Compute the chained hash before posting an invoice.
68
+ - Generate the QR PNG that goes on the printed PDF.
69
+
70
+ This library covers exactly that surface. It is **deliberately small** so it
71
+ can be vendored, audited, and certified without pulling a giant dependency
72
+ tree into your ERP.
73
+
74
+ ## Full Odoo module: €369 one-off
75
+
76
+ This validator is the open-source nucleus of our commercial Odoo app:
77
+
78
+ > **Veri\*Factu for Odoo 17 / 18 / 19** — full submission to AEAT,
79
+ > chained hash storage, QR on every PDF report, audit log, declarative
80
+ > re-send, multi-company, ready for July 2026.
81
+ >
82
+ > **€369 one-off — flexigotech.com/verifactu**
83
+
84
+ If you just need the algorithms, this PyPI package is MIT-licensed and free
85
+ forever. If you need the full ERP integration, buy the Odoo module.
86
+
87
+ ## Installation
88
+
89
+ ```bash
90
+ pip install verifactu-validator
91
+ ```
92
+
93
+ Requires Python **3.10+**. Zero runtime dependencies.
94
+
95
+ ## Publishing (for maintainers)
96
+
97
+ ```bash
98
+ pip install build twine
99
+ python -m build
100
+ twine upload dist/*
101
+ ```
102
+
103
+ A GitHub Actions workflow (`.github/workflows/ci.yml`) runs the test suite on
104
+ every push and pull request.
105
+
106
+ ## License
107
+
108
+ MIT © 2026 [FlexiGoTech](https://flexigotech.com)
109
+
110
+ ---
111
+
112
+ Made by **FlexiGoTech** in Barcelona. We build Odoo App Store modules for
113
+ Spanish, French, German and EU compliance: Veri\*Factu, France PDP,
114
+ XRechnung, Whistleblowing, Mirakl/Decathlon connectors, AliExpress
115
+ connector. See the full catalogue at [flexigotech.com](https://flexigotech.com).
@@ -0,0 +1,86 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "verifactu-validator"
7
+ version = "0.1.0"
8
+ description = "Spanish e-invoice (Veri*Factu / AEAT) validator: NIF/CIF/NIE checksum, SHA-256 chained hash, and QR URL builder per Spanish Tax Agency spec."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "FlexiGoTech", email = "comercial@flexigobe.com" },
14
+ ]
15
+ maintainers = [
16
+ { name = "FlexiGoTech", email = "comercial@flexigobe.com" },
17
+ ]
18
+ keywords = [
19
+ "aeat",
20
+ "verifactu",
21
+ "spain",
22
+ "einvoice",
23
+ "odoo",
24
+ "facturae",
25
+ "compliance",
26
+ "invoice",
27
+ "sii",
28
+ "tributaria",
29
+ ]
30
+ classifiers = [
31
+ "Development Status :: 4 - Beta",
32
+ "Intended Audience :: Developers",
33
+ "Intended Audience :: Financial and Insurance Industry",
34
+ "License :: OSI Approved :: MIT License",
35
+ "Operating System :: OS Independent",
36
+ "Programming Language :: Python :: 3",
37
+ "Programming Language :: Python :: 3.10",
38
+ "Programming Language :: Python :: 3.11",
39
+ "Programming Language :: Python :: 3.12",
40
+ "Topic :: Office/Business :: Financial :: Accounting",
41
+ "Topic :: Software Development :: Libraries :: Python Modules",
42
+ "Natural Language :: Spanish",
43
+ "Natural Language :: English",
44
+ ]
45
+ dependencies = []
46
+
47
+ [project.optional-dependencies]
48
+ dev = [
49
+ "pytest>=7.4",
50
+ "pytest-cov>=4.1",
51
+ "ruff>=0.4",
52
+ "build>=1.0",
53
+ "twine>=4.0",
54
+ ]
55
+
56
+ [project.urls]
57
+ Homepage = "https://flexigotech.com/verifactu"
58
+ Documentation = "https://flexigotech.com/verifactu"
59
+ Repository = "https://github.com/Flexigobe/verifactu-validator"
60
+ "Bug Tracker" = "https://github.com/Flexigobe/verifactu-validator/issues"
61
+ "Commercial Odoo Module" = "https://flexigotech.com/verifactu"
62
+
63
+ [project.scripts]
64
+ verifactu = "verifactu_validator.cli:main"
65
+
66
+ [tool.setuptools]
67
+ package-dir = { "" = "src" }
68
+
69
+ [tool.setuptools.packages.find]
70
+ where = ["src"]
71
+
72
+ [tool.setuptools.package-data]
73
+ verifactu_validator = ["py.typed"]
74
+
75
+ [tool.pytest.ini_options]
76
+ minversion = "7.0"
77
+ testpaths = ["tests"]
78
+ addopts = "-ra -q"
79
+
80
+ [tool.ruff]
81
+ line-length = 100
82
+ target-version = "py310"
83
+
84
+ [tool.ruff.lint]
85
+ select = ["E", "F", "W", "I", "B", "UP"]
86
+ ignore = ["E501"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ """verifactu_validator — Spanish AEAT Veri*Factu invoice validator.
2
+
3
+ Public API surface kept deliberately tiny so the library can be vendored,
4
+ audited, and certified by Spanish accountants and ERP integrators.
5
+
6
+ Exports
7
+ -------
8
+ - validate_nif(value) -> bool
9
+ - validate_cif(value) -> bool
10
+ - validate_nie(value) -> bool
11
+ - validate_tax_id(value) -> bool (auto-detects NIF / CIF / NIE)
12
+ - build_invoice_hash(...) -> str (SHA-256 chained hash, hex)
13
+ - build_qr_url(...) -> str (AEAT TIKE-CONT verification URL)
14
+ """
15
+
16
+ from .hash import build_invoice_hash
17
+ from .nif import (
18
+ validate_cif,
19
+ validate_nie,
20
+ validate_nif,
21
+ validate_tax_id,
22
+ )
23
+ from .qr import build_qr_url
24
+
25
+ __all__ = [
26
+ "validate_nif",
27
+ "validate_cif",
28
+ "validate_nie",
29
+ "validate_tax_id",
30
+ "build_invoice_hash",
31
+ "build_qr_url",
32
+ "__version__",
33
+ ]
34
+
35
+ __version__ = "0.1.0"
@@ -0,0 +1,201 @@
1
+ """Command-line interface for verifactu-validator.
2
+
3
+ Usage examples
4
+ --------------
5
+ verifactu --help
6
+ verifactu nif 12345678Z
7
+ verifactu hash --prev 000...0 --nif B12345678 --num F2026/0001 \
8
+ --date 2026-07-01 --total 121.00
9
+ verifactu qr --nif B12345678 --num F2026/0001 \
10
+ --date 2026-07-01 --total 121.00 [--production]
11
+ verifactu validate path/to/invoice.xml
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import sys
18
+ from pathlib import Path
19
+ from xml.etree import ElementTree as ET
20
+
21
+ from . import __version__
22
+ from .hash import build_invoice_hash, zero_hash
23
+ from .nif import validate_tax_id
24
+ from .qr import build_qr_url
25
+
26
+
27
+ def _cmd_nif(args: argparse.Namespace) -> int:
28
+ ok = validate_tax_id(args.value)
29
+ print(f"{args.value}: {'OK' if ok else 'FAIL'}")
30
+ return 0 if ok else 1
31
+
32
+
33
+ def _cmd_hash(args: argparse.Namespace) -> int:
34
+ prev = args.prev or zero_hash()
35
+ h = build_invoice_hash(
36
+ previous_hash=prev,
37
+ issuer_nif=args.nif,
38
+ invoice_number=args.num,
39
+ issue_date=args.date,
40
+ total_amount=args.total,
41
+ )
42
+ print(h)
43
+ return 0
44
+
45
+
46
+ def _cmd_qr(args: argparse.Namespace) -> int:
47
+ url = build_qr_url(
48
+ issuer_nif=args.nif,
49
+ invoice_number=args.num,
50
+ issue_date=args.date,
51
+ total_amount=args.total,
52
+ production=args.production,
53
+ )
54
+ print(url)
55
+ return 0
56
+
57
+
58
+ _NS_FACTURAE = {
59
+ "fe": "http://www.facturae.es/Facturae/2014/v3.2.2/Facturae",
60
+ "fe322": "http://www.facturae.gob.es/formato/Versiones/Facturaev3_2_2.xml",
61
+ }
62
+
63
+
64
+ def _findtext(root: ET.Element, *paths: str) -> str | None:
65
+ for p in paths:
66
+ node = root.find(p, _NS_FACTURAE)
67
+ if node is not None and node.text:
68
+ return node.text.strip()
69
+ return None
70
+
71
+
72
+ def _cmd_validate(args: argparse.Namespace) -> int:
73
+ path = Path(args.xml)
74
+ if not path.is_file():
75
+ print(f"FAIL: file not found: {path}", file=sys.stderr)
76
+ return 2
77
+
78
+ try:
79
+ tree = ET.parse(path)
80
+ except ET.ParseError as exc:
81
+ print(f"FAIL: malformed XML — {exc}")
82
+ return 1
83
+
84
+ root = tree.getroot()
85
+
86
+ results: list[tuple[str, bool, str]] = []
87
+
88
+ # Rule 1: file is parseable.
89
+ results.append(("xml.parseable", True, "XML parsed successfully"))
90
+
91
+ # Rule 2: issuer NIF present and valid.
92
+ issuer_nif = _findtext(
93
+ root,
94
+ ".//SellerParty//TaxIdentificationNumber",
95
+ ".//SellerParty//{*}TaxIdentificationNumber",
96
+ ".//TaxIdentificationNumber",
97
+ )
98
+ if issuer_nif:
99
+ ok = validate_tax_id(issuer_nif)
100
+ results.append(("issuer.tax_id", ok, f"value={issuer_nif}"))
101
+ else:
102
+ results.append(("issuer.tax_id", False, "not found in XML"))
103
+
104
+ # Rule 3: at least one invoice number.
105
+ inv_num = _findtext(
106
+ root,
107
+ ".//InvoiceNumber",
108
+ ".//{*}InvoiceNumber",
109
+ )
110
+ results.append(
111
+ ("invoice.number", bool(inv_num), f"value={inv_num}" if inv_num else "missing")
112
+ )
113
+
114
+ # Rule 4: issue date present.
115
+ issue_date = _findtext(
116
+ root,
117
+ ".//IssueDate",
118
+ ".//{*}IssueDate",
119
+ )
120
+ results.append(
121
+ ("invoice.issue_date", bool(issue_date), f"value={issue_date}" if issue_date else "missing")
122
+ )
123
+
124
+ # Rule 5: total amount present.
125
+ total = _findtext(
126
+ root,
127
+ ".//TotalExecutableAmount",
128
+ ".//InvoiceTotal",
129
+ ".//{*}TotalExecutableAmount",
130
+ ".//{*}InvoiceTotal",
131
+ )
132
+ results.append(
133
+ ("invoice.total", bool(total), f"value={total}" if total else "missing")
134
+ )
135
+
136
+ width = max(len(name) for name, _, _ in results)
137
+ failed = 0
138
+ for name, ok, detail in results:
139
+ status = "OK " if ok else "FAIL"
140
+ print(f" [{status}] {name.ljust(width)} {detail}")
141
+ if not ok:
142
+ failed += 1
143
+
144
+ print()
145
+ if failed == 0:
146
+ print(f"PASS: {len(results)} checks succeeded — {path.name}")
147
+ return 0
148
+ print(f"FAIL: {failed}/{len(results)} checks failed — {path.name}")
149
+ return 1
150
+
151
+
152
+ def build_parser() -> argparse.ArgumentParser:
153
+ p = argparse.ArgumentParser(
154
+ prog="verifactu",
155
+ description=(
156
+ "Spanish AEAT Veri*Factu invoice validator. "
157
+ "Full Odoo module: https://flexigotech.com/verifactu"
158
+ ),
159
+ )
160
+ p.add_argument("--version", action="version", version=f"verifactu-validator {__version__}")
161
+ sub = p.add_subparsers(dest="command", required=True)
162
+
163
+ p_nif = sub.add_parser("nif", help="Validate a Spanish NIF / CIF / NIE.")
164
+ p_nif.add_argument("value", help="Tax ID to validate")
165
+ p_nif.set_defaults(func=_cmd_nif)
166
+
167
+ p_hash = sub.add_parser("hash", help="Compute the SHA-256 chained invoice hash.")
168
+ p_hash.add_argument("--prev", default=None, help="Previous SHA-256 hex (default: all zeros).")
169
+ p_hash.add_argument("--nif", required=True, help="Issuer NIF / CIF / NIE.")
170
+ p_hash.add_argument("--num", required=True, help="Invoice serial+number.")
171
+ p_hash.add_argument("--date", required=True, help="Issue date (YYYY-MM-DD or DD-MM-YYYY).")
172
+ p_hash.add_argument("--total", required=True, help="Invoice total with VAT.")
173
+ p_hash.set_defaults(func=_cmd_hash)
174
+
175
+ p_qr = sub.add_parser("qr", help="Build the AEAT QR URL embedded in the PDF.")
176
+ p_qr.add_argument("--nif", required=True)
177
+ p_qr.add_argument("--num", required=True)
178
+ p_qr.add_argument("--date", required=True)
179
+ p_qr.add_argument("--total", required=True)
180
+ p_qr.add_argument(
181
+ "--production",
182
+ action="store_true",
183
+ help="Use the production AEAT endpoint instead of pre-production.",
184
+ )
185
+ p_qr.set_defaults(func=_cmd_qr)
186
+
187
+ p_val = sub.add_parser("validate", help="Validate a Facturae XML invoice file.")
188
+ p_val.add_argument("xml", help="Path to invoice .xml")
189
+ p_val.set_defaults(func=_cmd_validate)
190
+
191
+ return p
192
+
193
+
194
+ def main(argv: list[str] | None = None) -> int:
195
+ parser = build_parser()
196
+ args = parser.parse_args(argv)
197
+ return int(args.func(args) or 0)
198
+
199
+
200
+ if __name__ == "__main__": # pragma: no cover
201
+ raise SystemExit(main())