Skip to content

Commit b540909

Browse files
committed
Removed _traslados_incluidos
Added Rounding Tracker
1 parent 0afd082 commit b540909

File tree

94 files changed

+1019
-310
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+1019
-310
lines changed

docs/pages/getting_started/40_cfdi_create_guide.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@ ______________________
6161
tasa_o_cuota=Decimal('0.106667'),
6262
)
6363
],
64-
),
65-
_traslados_incluidos=False # indica si el valor unitario incluye los traslados
64+
)
6665
)
6766
]
6867
)
@@ -302,8 +301,7 @@ _______________________
302301
tasa_o_cuota=Decimal('0.106667'),
303302
)
304303
],
305-
),
306-
_traslados_incluidos=False
304+
)
307305
)
308306
],
309307
addenda=dvz11.Diverza(

readme.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,7 @@ ____________________
179179
tasa_o_cuota=Decimal('0.106667'),
180180
)
181181
],
182-
),
183-
_traslados_incluidos=False # indica si el valor unitario incluye los traslados
182+
)
184183
)
185184
]
186185
)

satcfdi/create/cfd/cfdi33.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,6 @@ class Concepto(ScalarMap):
162162
:param cuenta_predial: Nodo opcional para asentar el número de cuenta predial con el que fue registrado el inmueble, en el sistema catastral de la entidad federativa de que trate, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable.
163163
:param complemento_concepto: Nodo opcional donde se incluyen los nodos complementarios de extensión al concepto definidos por el SAT, de acuerdo con las disposiciones particulares para un sector o actividad específica.
164164
:param parte: Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital por Internet.
165-
:param _traslados_incluidos: si el valor valor_unitario ya incluye traslados.
166165
"""
167166

168167
def __init__(
@@ -179,8 +178,7 @@ def __init__(
179178
informacion_aduanera: str | Sequence[str] = None,
180179
cuenta_predial: str = None,
181180
complemento_concepto: Sequence[CFDI] = None,
182-
parte: Sequence[Parte | dict] = None,
183-
_traslados_incluidos: bool = False
181+
parte: Sequence[Parte | dict] = None
184182
):
185183
super().__init__({
186184
'ClaveProdServ': clave_prod_serv,
@@ -195,8 +193,7 @@ def __init__(
195193
'InformacionAduanera': informacion_aduanera,
196194
'CuentaPredial': cuenta_predial,
197195
'ComplementoConcepto': complemento_concepto,
198-
'Parte': parte,
199-
'_traslados_incluidos': _traslados_incluidos
196+
'Parte': parte
200197
})
201198

202199

satcfdi/create/cfd/cfdi40.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from satcfdi.create.cfd.catalogos import Impuesto as CatImpuesto
88
from . import pago20
99
from ..compute import make_impuestos, rounder, make_impuesto, \
10-
make_impuestos_dr_parcial
10+
make_impuestos_dr_parcial, m_decimals, RoundTracker
1111
from ...cfdi import CFDI
1212
from ...transform import get_timezone
1313
from ...utils import ScalarMap
@@ -217,7 +217,6 @@ class Concepto(ScalarMap):
217217
:param cuenta_predial: Nodo opcional para asentar el número de cuenta predial con el que fue registrado el inmueble, en el sistema catastral de la entidad federativa de que trate, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable.
218218
:param complemento_concepto: Nodo opcional donde se incluyen los nodos complementarios de extensión al concepto definidos por el SAT, de acuerdo con las disposiciones particulares para un sector o actividad específica.
219219
:param parte: Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital por Internet.
220-
:param _traslados_incluidos: si el valor valor_unitario ya incluye traslados.
221220
"""
222221

223222
def __init__(
@@ -236,8 +235,7 @@ def __init__(
236235
informacion_aduanera: str | Sequence[str] = None,
237236
cuenta_predial: str | Sequence[str] = None,
238237
complemento_concepto: CFDI | Sequence[CFDI] = None,
239-
parte: Parte | Sequence[Parte | dict] = None,
240-
_traslados_incluidos: bool = False
238+
parte: Parte | Sequence[Parte | dict] = None
241239
):
242240
super().__init__({
243241
'ClaveProdServ': clave_prod_serv,
@@ -254,8 +252,7 @@ def __init__(
254252
'InformacionAduanera': informacion_aduanera,
255253
'CuentaPredial': cuenta_predial,
256254
'ComplementoConcepto': complemento_concepto,
257-
'Parte': parte,
258-
'_traslados_incluidos': _traslados_incluidos
255+
'Parte': parte
259256
})
260257

261258

@@ -335,36 +332,30 @@ def __post_init__(self):
335332
raise ValueError('Importe Pagado debe de ser menor o igual al Importe Saldo Anterior')
336333

337334

338-
def _make_conceptos(conceptos, rnd_fn):
335+
def _make_conceptos(conceptos, decimals):
336+
rnd_fn = lambda v: round(v, decimals)
337+
rnd_traslados_tracker = RoundTracker(decimals)
338+
rnd_retenciones_tracker = RoundTracker(decimals)
339+
339340
def make_concepto(concepto):
340341
impuestos = concepto.get("Impuestos") or {}
341342
trasladados = [x if isinstance(x, dict) else Traslado.parse(x) for x in iterate(impuestos.get("Traslados"))]
342343
retenciones = [x if isinstance(x, dict) else Retencion.parse(x) for x in iterate(impuestos.get("Retenciones"))]
343344

344-
if concepto.get('_traslados_incluidos'):
345-
s_tasa = sum(c["TasaOCuota"] for c in trasladados if c["TipoFactor"] == "Tasa")
346-
s_cuota = sum(c["TasaOCuota"] for c in trasladados if c["TipoFactor"] == "Cuota")
347-
if any(c for c in trasladados if c["TipoFactor"] in ('Tasa', 'Cuota') and (c.get('Base') is not None or c.get('Importe') is not None)):
348-
raise ValueError("Not possible to compute '_traslados_incluidos' if any 'trasladados' contains 'Base' or 'Importe'")
349-
350-
valor_unitario = concepto['ValorUnitario']
351-
valor_unitario = (valor_unitario - s_cuota) / (s_tasa + 1)
352-
concepto['ValorUnitario'] = rnd_fn(valor_unitario)
353-
else:
354-
valor_unitario = concepto['ValorUnitario']
345+
valor_unitario = concepto['ValorUnitario']
355346

356-
importe = concepto["Cantidad"] * valor_unitario
357-
concepto["Importe"] = rnd_fn(importe)
347+
importe = rnd_fn(concepto["Cantidad"] * valor_unitario)
348+
concepto["Importe"] = importe
349+
base = importe - (concepto.get("Descuento") or 0)
358350

359351
if concepto.get("ObjetoImp") in ("01", "03"):
360352
concepto['Impuestos'] = None
361353
else:
362-
base = importe - (concepto.get("Descuento") or 0)
363354
impuestos = {
364355
imp_t: [
365-
make_impuesto(i, base=base, rnd_fn=rnd_fn) for i in imp
356+
make_impuesto(i, base=base, rnd_tracker=rnd_tracker) for i in imp
366357
]
367-
for imp_t, imp in [('Traslados', trasladados), ('Retenciones', retenciones)] if imp
358+
for imp_t, imp, rnd_tracker in [('Traslados', trasladados, rnd_traslados_tracker), ('Retenciones', retenciones, rnd_retenciones_tracker)] if imp
368359
}
369360
concepto['Impuestos'] = impuestos or None
370361
concepto["ObjetoImp"] = "02" if impuestos else "01"
@@ -458,7 +449,8 @@ def __init__(
458449
self.compute()
459450

460451
def compute(self):
461-
self["Conceptos"] = conceptos = _make_conceptos(self["Conceptos"], rnd_fn=rounder(self["Moneda"]))
452+
round_decimals = m_decimals(self["Moneda"])
453+
self["Conceptos"] = conceptos = _make_conceptos(self["Conceptos"], decimals=round_decimals)
462454
self["SubTotal"] = sub_total = sum(c['Importe'] for c in conceptos)
463455
descuento = sum(c.get('Descuento') or 0 for c in conceptos)
464456
self['Descuento'] = descuento or None

satcfdi/create/compute.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from collections import defaultdict
2-
from decimal import Decimal
2+
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
33

44
from ..catalogs import moneda_decimales
55
from ..transform.helpers import strcode
66
from ..utils import iterate
77

8+
def m_decimals(moneda):
9+
return moneda_decimales(strcode(moneda))
810

911
def rounder(moneda):
1012
decimals = moneda_decimales(strcode(moneda))
@@ -48,7 +50,7 @@ def encode_impuesto(impuesto, tipo_factor, tasa_cuota: Decimal = None):
4850
return impuesto
4951

5052

51-
def make_impuesto(impuesto: dict, base, rnd_fn):
53+
def make_impuesto(impuesto: dict, base, rnd_tracker):
5254
_impuesto = impuesto["Impuesto"]
5355
tipo_factor = impuesto['TipoFactor']
5456
tasa_cuota = impuesto['TasaOCuota']
@@ -60,7 +62,7 @@ def make_impuesto(impuesto: dict, base, rnd_fn):
6062
else:
6163
match tipo_factor:
6264
case "Tasa":
63-
importe = rnd_fn(base * tasa_cuota)
65+
importe = rnd_tracker.round(base * tasa_cuota)
6466
case "Cuota":
6567
importe = tasa_cuota
6668
case "Exento":
@@ -69,13 +71,37 @@ def make_impuesto(impuesto: dict, base, rnd_fn):
6971
raise ValueError("Invalid TipoFactor", tipo_factor)
7072

7173
return {
72-
'Base': rnd_fn(base),
74+
'Base': base,
7375
'Impuesto': _impuesto,
7476
'TipoFactor': tipo_factor,
7577
'TasaOCuota': tasa_cuota,
7678
'Importe': importe
7779
}
7880

81+
class RoundTracker:
82+
def __init__(self, decimals):
83+
self.decimals = decimals
84+
self.offset = Decimal('0.0')
85+
if decimals is 0:
86+
self.exp = Decimal('1')
87+
else:
88+
self.exp = Decimal('0.' + '0' * (decimals - 1) + '1')
89+
self.offset_margin = Decimal('0.' + '0' * decimals + '5')
90+
91+
def round(self, value):
92+
rounded = self.peak(value)
93+
self.offset += value - rounded
94+
return rounded
95+
96+
def peak(self, value):
97+
if self.offset >= self.offset_margin:
98+
rounded = value.quantize(self.exp, rounding=ROUND_CEILING)
99+
elif self.offset <= -self.offset_margin:
100+
rounded = value.quantize(self.exp, rounding=ROUND_FLOOR)
101+
else:
102+
rounded = round(value, self.decimals)
103+
return rounded
104+
79105

80106
def group_impuestos(elements, pfx="", ofx=""):
81107
retenciones = aggregate(

tests/test_create_addenda.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ def test_create_addenda():
8080
tasa_o_cuota=Decimal('0.106667'),
8181
)
8282
],
83-
),
84-
_traslados_incluidos=False
83+
)
8584
)
8685
],
8786
addenda=dvz11.Diverza(
@@ -146,8 +145,7 @@ def test_copy_cfdi():
146145
tasa_o_cuota=Decimal('0.106667'),
147146
)
148147
],
149-
),
150-
_traslados_incluidos=False
148+
)
151149
)
152150
]
153151
)

tests/test_create_addenda/cfdi_addenda.pretty.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@
4646
'ObjetoImp': '02',
4747
'Parte': None,
4848
'Unidad': None,
49-
'ValorUnitario': Decimal('15390.30'),
50-
'_traslados_incluidos': False}],
49+
'ValorUnitario': Decimal('15390.30')}],
5150
'CondicionesDePago': None,
5251
'Confirmacion': None,
5352
'Descuento': None,

tests/test_create_addenda/cfdi_copy.pretty.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
'ObjetoImp': '02',
3232
'Parte': None,
3333
'Unidad': None,
34-
'ValorUnitario': Decimal('15390.30'),
35-
'_traslados_incluidos': False}],
34+
'ValorUnitario': Decimal('15390.30')}],
3635
'CondicionesDePago': None,
3736
'Confirmacion': None,
3837
'Descuento': None,

tests/test_create_cfdi33/h&e951128469_ingreso_iva16.pretty.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
'ObjetoImp': '02',
2121
'Parte': None,
2222
'Unidad': None,
23-
'ValorUnitario': Decimal('15390.30'),
24-
'_traslados_incluidos': False}],
23+
'ValorUnitario': Decimal('15390.30')}],
2524
'CondicionesDePago': None,
2625
'Confirmacion': None,
2726
'Descuento': None,

tests/test_create_cfdi33/h&e951128469_ingreso_iva16_stamped.pretty.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
'ObjetoImp': '02',
2828
'Parte': None,
2929
'Unidad': None,
30-
'ValorUnitario': Decimal('15390.30'),
31-
'_traslados_incluidos': False}],
30+
'ValorUnitario': Decimal('15390.30')}],
3231
'CondicionesDePago': None,
3332
'Confirmacion': None,
3433
'Descuento': None,

0 commit comments

Comments
 (0)