digifact-sdk 2.0.1__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.
@@ -0,0 +1,76 @@
1
+ """Exceptions for the Digifact FEL SDK."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class DigifactError(Exception):
6
+ """Base exception for all Digifact SDK errors."""
7
+
8
+ def __init__(self, message: str, code: int | None = None, raw: dict | None = None) -> None:
9
+ super().__init__(message)
10
+ self.code = code
11
+ self.raw = raw or {}
12
+
13
+ def __repr__(self) -> str:
14
+ return f"DigifactError(message={str(self)!r}, code={self.code!r})"
15
+
16
+
17
+ class DigifactAuthError(DigifactError):
18
+ """Authentication / token error."""
19
+
20
+
21
+ class DigifactApiError(DigifactError):
22
+ """API returned a non-success response."""
23
+
24
+
25
+ class DigifactValidationError(DigifactError):
26
+ """Payload validation error (SAT or Digifact rejection)."""
27
+
28
+
29
+ class DigifactNitNotFoundError(DigifactError):
30
+ """NIT lookup returned no result."""
31
+
32
+
33
+ # ── SAT hint classifier ──────────────────────────────────────────────────────
34
+
35
+ _HINTS: list[tuple[list[str], str]] = [
36
+ (
37
+ ["AfiliacionIVA", "MINIRTU", "RTU"],
38
+ "El NIT del emisor no tiene la afiliación de IVA indicada en su RTU (AfiliacionIVA). "
39
+ "Verifica que AfiliacionIVA sea GEN, PEQ o EXE según el RTU del emisor.",
40
+ ),
41
+ (
42
+ ["TipoPersoneria"],
43
+ "El valor de TipoPersoneria debe coincidir exactamente con el código registrado en el "
44
+ "RTU del emisor. Consulta el RTU en portal.sat.gob.gt.",
45
+ ),
46
+ (
47
+ ["SECCION 3.5", "9015", "ID Receptor"],
48
+ "NCRE/NDEB: el receptor del documento origen debe coincidir con el receptor del "
49
+ "documento de nota. Asegúrate de referenciar una factura cuyo comprador sea el mismo "
50
+ "que el de la nota.",
51
+ ),
52
+ (
53
+ ["NAMESPACE_UNKNOWN", "5035"],
54
+ "El complemento CCA requiere que el emisor tenga habilitado el namespace CCA en "
55
+ "Digifact. Contacta a soporte@digifact.com.gt.",
56
+ ),
57
+ (
58
+ ["FRASES"],
59
+ "Este tipo de DTE no permite las frases indicadas. Para FESP no incluyas "
60
+ "TipoFrase/Escenario en la sección Seller.",
61
+ ),
62
+ (
63
+ ["FechaEmision", "fecha"],
64
+ "Verifica el formato de fecha: debe ser 'YYYY-MM-DD HH:MM:SS' para cancelaciones y "
65
+ "ncredtotal, o 'YYYY-MM-DDTHH:MM:SS-06:00' para IssuedDateTime.",
66
+ ),
67
+ ]
68
+
69
+
70
+ def classify_error(message: str) -> str | None:
71
+ """Return a developer hint for a known SAT/Digifact error pattern, or None."""
72
+ upper = message.upper()
73
+ for keywords, hint in _HINTS:
74
+ if any(kw.upper() in upper for kw in keywords):
75
+ return hint
76
+ return None
digifact_sdk/tax.py ADDED
@@ -0,0 +1,133 @@
1
+ """IVA calculations, money formatting, and Guatemala time helpers."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from datetime import datetime, timezone, timedelta
6
+ from decimal import Decimal, ROUND_HALF_UP, getcontext
7
+
8
+ # Use 28-digit precision to avoid rounding drift
9
+ getcontext().prec = 28
10
+
11
+ _GT_TZ = timezone(timedelta(hours=-6))
12
+ _IVA_RATE = Decimal("12")
13
+ _IVA_DIVISOR = Decimal("112")
14
+
15
+
16
+ # ── Time helpers ─────────────────────────────────────────────────────────────
17
+
18
+ def gt_now() -> tuple[str, str, str]:
19
+ """Return (iso_with_offset, space_datetime, date_only) in Guatemala time (UTC-6)."""
20
+ gt = datetime.now(_GT_TZ)
21
+ dt_offset = gt.strftime("%Y-%m-%dT%H:%M:%S-06:00")
22
+ dt_space = gt.strftime("%Y-%m-%d %H:%M:%S")
23
+ date_only = gt.strftime("%Y-%m-%d")
24
+ return dt_offset, dt_space, date_only
25
+
26
+
27
+ def gt_now_dt() -> datetime:
28
+ """Return current datetime in Guatemala timezone."""
29
+ return datetime.now(_GT_TZ)
30
+
31
+
32
+ # ── Tax ID helpers ────────────────────────────────────────────────────────────
33
+
34
+ def pad_taxid(taxid: str) -> str:
35
+ """Strip non-digits and left-pad to 12 characters with zeros.
36
+
37
+ Example: '44653948' → '000044653948'
38
+ """
39
+ digits = re.sub(r"\D", "", taxid)
40
+ return digits.rjust(12, "0")
41
+
42
+
43
+ # ── Money helpers ─────────────────────────────────────────────────────────────
44
+
45
+ def fmt(value: Decimal | float | str, decimals: int = 6) -> str:
46
+ """Format a Decimal (or float/str) as a string with *decimals* decimal places."""
47
+ d = Decimal(str(value))
48
+ quantize_str = Decimal("1." + "0" * decimals)
49
+ return str(d.quantize(quantize_str, rounding=ROUND_HALF_UP))
50
+
51
+
52
+ def calc_iva(line_total: "Decimal | float | str") -> tuple[Decimal, Decimal]:
53
+ """Return (taxable_amount, iva_amount) for an IVA-inclusive line_total.
54
+
55
+ taxable_amount = line_total / 1.12
56
+ iva_amount = line_total - taxable_amount
57
+ """
58
+ line_total = Decimal(str(line_total))
59
+ taxable = (line_total * 100 / _IVA_DIVISOR).quantize(
60
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
61
+ )
62
+ iva = (line_total - taxable).quantize(
63
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
64
+ )
65
+ return taxable, iva
66
+
67
+
68
+ class LineCalc:
69
+ """Holds all calculated values for a single invoice line item."""
70
+
71
+ def __init__(
72
+ self,
73
+ qty: Decimal,
74
+ unit_price: Decimal,
75
+ *,
76
+ taxable: bool = True,
77
+ discount: Decimal | None = None,
78
+ ) -> None:
79
+ self.qty = qty
80
+ self.unit_price = unit_price
81
+ self.gross = (qty * unit_price).quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)
82
+ self.discount = (discount or Decimal("0")).quantize(
83
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
84
+ )
85
+ self.line_total = (self.gross - self.discount).quantize(
86
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
87
+ )
88
+ if taxable:
89
+ self.taxable_amount, self.iva_amount = calc_iva(self.line_total)
90
+ else:
91
+ self.taxable_amount = Decimal("0")
92
+ self.iva_amount = Decimal("0")
93
+
94
+ # Convenience string formatters
95
+ def f_qty(self) -> str:
96
+ return fmt(self.qty)
97
+
98
+ def f_price(self) -> str:
99
+ return fmt(self.unit_price)
100
+
101
+ def f_line_total(self) -> str:
102
+ return fmt(self.line_total)
103
+
104
+ def f_taxable(self) -> str:
105
+ return fmt(self.taxable_amount)
106
+
107
+ def f_iva(self) -> str:
108
+ return fmt(self.iva_amount)
109
+
110
+ def f_discount(self) -> str:
111
+ return fmt(self.discount, decimals=2)
112
+
113
+
114
+ class InvoiceTotals:
115
+ """Aggregate totals for a full invoice."""
116
+
117
+ def __init__(self, lines: list[LineCalc]) -> None:
118
+ self.lines = lines
119
+ self.grand_total = sum(ln.line_total for ln in lines).quantize(
120
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
121
+ )
122
+ self.total_iva = sum(ln.iva_amount for ln in lines).quantize(
123
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
124
+ )
125
+ self.total_taxable = sum(ln.taxable_amount for ln in lines).quantize(
126
+ Decimal("0.000001"), rounding=ROUND_HALF_UP
127
+ )
128
+
129
+ def f_grand_total(self) -> str:
130
+ return fmt(self.grand_total)
131
+
132
+ def f_total_iva(self) -> str:
133
+ return fmt(self.total_iva)
@@ -0,0 +1,207 @@
1
+ Metadata-Version: 2.4
2
+ Name: digifact-sdk
3
+ Version: 2.0.1
4
+ Summary: Python SDK for Digifact FEL Guatemala e-invoicing API
5
+ Author-email: AALONZOLU <noreply@github.com>
6
+ License: MIT
7
+ Keywords: digifact,fel,guatemala,factura,e-invoicing,sat
8
+ Classifier: Development Status :: 5 - Production/Stable
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Office/Business :: Financial
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: requests>=2.28
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=7; extra == "dev"
21
+ Requires-Dist: pytest-cov; extra == "dev"
22
+ Requires-Dist: pytest-timeout; extra == "dev"
23
+ Requires-Dist: python-dotenv; extra == "dev"
24
+
25
+ # Digifact FEL Guatemala — Python SDK
26
+
27
+ Python SDK for the [Digifact](https://www.digifact.com.gt/) FEL (Factura Electrónica en Línea) Guatemala e-invoicing API.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install digifact-sdk
33
+ ```
34
+
35
+ Or from source:
36
+
37
+ ```bash
38
+ pip install -e sdk/python/
39
+ ```
40
+
41
+ ## Quick start
42
+
43
+ ```python
44
+ from digifact_sdk import DigifactClient
45
+
46
+ client = DigifactClient(
47
+ taxid="12345678",
48
+ username="FELUSER",
49
+ password="secret",
50
+ environment="test", # or "production"
51
+ )
52
+
53
+ # FACT CF — consumer final, IVA calculated automatically
54
+ result = client.invoice(
55
+ buyer="CF",
56
+ items=[{"description": "Consultoría", "qty": 1, "price": 100.00}]
57
+ )
58
+ print(result.auth_number)
59
+
60
+ # FACT to NIT — buyer name fetched from SAT automatically
61
+ result = client.invoice(
62
+ buyer="12345678",
63
+ items=[
64
+ {"description": "Laptop", "qty": 1, "price": 5000.00, "type": "Bien"},
65
+ {"description": "Soporte anual", "qty": 1, "price": 500.00},
66
+ ]
67
+ )
68
+
69
+ # FACT to CUI buyer
70
+ result = client.invoice(
71
+ buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
72
+ items=[{"description": "Producto", "qty": 2, "price": 50.00}]
73
+ )
74
+
75
+ # FCAM (Factura Cambiaria)
76
+ result = client.invoice(
77
+ buyer="12345678",
78
+ items=[{"description": "Servicio", "qty": 1, "price": 500.00}],
79
+ doc_type="FCAM",
80
+ payment_terms=[{"date": "2026-04-18", "amount": 500.00}]
81
+ )
82
+
83
+ # Credit note (NCRE)
84
+ result = client.credit_note(
85
+ buyer="12345678",
86
+ items=[{"description": "Devolución", "qty": 1, "price": 100.00}],
87
+ origin={
88
+ "auth_number": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
89
+ "date": "2026-03-18",
90
+ "series": "XXXXXXXX",
91
+ "number": "123456",
92
+ },
93
+ reason="Producto defectuoso"
94
+ )
95
+
96
+ # Debit note (NDEB)
97
+ result = client.debit_note(
98
+ buyer="12345678",
99
+ items=[{"description": "Cargo adicional", "qty": 1, "price": 50.00}],
100
+ origin={...},
101
+ reason="Cargo por entrega express"
102
+ )
103
+
104
+ # Cancel a DTE
105
+ result = client.cancel(
106
+ auth_number="XXXXXXXX-...",
107
+ receiver_id="CF",
108
+ issue_datetime="2026-03-18 21:40:14",
109
+ reason="Error en monto"
110
+ )
111
+
112
+ # Total credit note
113
+ result = client.credit_note_total(
114
+ auth_number="XXXXXXXX-...",
115
+ issue_datetime="2026-03-18 21:40:14",
116
+ reason="Nota de crédito total"
117
+ )
118
+
119
+ # NIT lookup
120
+ info = client.lookup_nit("12345678")
121
+ print(info["name"])
122
+
123
+ # Retrieve DTE
124
+ doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
125
+ ```
126
+
127
+ ## Document types
128
+
129
+ | Type | Description | IVA |
130
+ |------|-------------|-----|
131
+ | `FACT` | Standard invoice | Yes |
132
+ | `FCAM` | Factura Cambiaria (installments) | Yes |
133
+ | `NDEB` | Debit note | Yes |
134
+ | `NCRE` | Credit note | Yes |
135
+ | `NABN` | Nota de Abono | No |
136
+ | `FESP` | Factura Especial | Yes |
137
+ | `RDON` | Recibo por Donación | No |
138
+ | `FPEQ` | Factura Pequeño Contribuyente | No |
139
+ | `RECI` | Recibo universitario | No |
140
+ | `CCA` | Cobro por Cuenta Ajena | Yes |
141
+
142
+ ## IVA calculation
143
+
144
+ Prices are IVA-inclusive (what the customer pays):
145
+
146
+ ```
147
+ line_total = qty × price
148
+ taxable_amount = line_total / 1.12
149
+ iva_amount = line_total − taxable_amount
150
+ ```
151
+
152
+ All money values are formatted as strings with 6 decimal places.
153
+
154
+ ## Item dict keys
155
+
156
+ ```python
157
+ {
158
+ "description": str, # required
159
+ "price": float | Decimal, # required — unit price, IVA-inclusive
160
+ "qty": float | Decimal, # optional, default 1
161
+ "type": str, # optional: "Servicio" (default) | "Bien"
162
+ "unit_of_measure": str, # optional, default "UNI"
163
+ "discount": float | None, # optional — line discount amount
164
+ }
165
+ ```
166
+
167
+ ## Running tests
168
+
169
+ ```bash
170
+ # Unit tests (no credentials needed)
171
+ python -m pytest tests/ -v
172
+
173
+ # Integration tests
174
+ export DIGIFACT_TAXID=12345678
175
+ export DIGIFACT_USERNAME=FELUSER
176
+ export DIGIFACT_PASSWORD=your_password
177
+ python -m pytest tests/ -v
178
+ ```
179
+
180
+ ## Environment variables
181
+
182
+ | Variable | Description |
183
+ |----------|-------------|
184
+ | `DIGIFACT_TAXID` | Fiscal ID (e.g. `12345678`) |
185
+ | `DIGIFACT_USERNAME` | Username (e.g. `FELUSER`) |
186
+ | `DIGIFACT_PASSWORD` | Account password |
187
+
188
+ ## Error handling
189
+
190
+ ```python
191
+ from digifact_sdk import (
192
+ DigifactError, # base
193
+ DigifactAuthError, # auth failure
194
+ DigifactApiError, # HTTP / API error
195
+ DigifactValidationError, # SAT rejection
196
+ DigifactNitNotFoundError, # NIT not found
197
+ )
198
+
199
+ try:
200
+ result = client.invoice("CF", [...])
201
+ except DigifactValidationError as exc:
202
+ print(f"SAT rejected: {exc}")
203
+ print(f"Code: {exc.code}")
204
+ print(f"Raw: {exc.raw}")
205
+ except DigifactError as exc:
206
+ print(f"SDK error: {exc}")
207
+ ```
@@ -0,0 +1,9 @@
1
+ digifact_sdk/__init__.py,sha256=P_rGntB3SvBSk5lrwARtjajCig0cbZeccLQVDoy_n5s,459
2
+ digifact_sdk/builder.py,sha256=z8ALSyniR0fHN_bAsPCv_VvnS424GfOYU9pK3lnDbgo,27980
3
+ digifact_sdk/client.py,sha256=a5q9m0_QZNjU2b8yzk312xVS3lopE3o9eBFUAyM5BN8,24560
4
+ digifact_sdk/exceptions.py,sha256=5TYB70O13McAlA1GjpcpQZ36SZVO8AQytdvJkg-WRzY,2659
5
+ digifact_sdk/tax.py,sha256=fYw-5tt-Iblt5g6rfGhTrterBuCzek7jGHtTQyGoiNE,4581
6
+ digifact_sdk-2.0.1.dist-info/METADATA,sha256=5JzDDco0cHBEmHdeTL1wjZPmGHptWEe5Gm9RHe7U-lY,5473
7
+ digifact_sdk-2.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ digifact_sdk-2.0.1.dist-info/top_level.txt,sha256=AkYqgzigsWPcxc8Rft51PtPMcr4NEi6daW2fXNpsBZE,13
9
+ digifact_sdk-2.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ digifact_sdk