Passed
Push — main ( 33d1e2...88e831 )
by Sat CFDI
14:31 queued 09:19
created

satcfdi.create.cfd.cfdi40.Traslado.parse()   A

Complexity

Conditions 2

Size

Total Lines 7
Code Lines 7

Duplication

Lines 7
Ratio 100 %

Code Coverage

Tests 2
CRAP Score 2.5

Importance

Changes 0
Metric Value
cc 2
eloc 7
nop 2
dl 7
loc 7
ccs 2
cts 4
cp 0.5
crap 2.5
rs 10
c 0
b 0
f 0
1
"""cfdi http://www.sat.gob.mx/cfd/4"""
2 1
from collections.abc import Sequence
3 1
from dataclasses import dataclass
4 1
from datetime import datetime
5 1
from decimal import Decimal
6
7 1
from satcfdi.create.cfd.catalogos import Impuesto as CatImpuesto
8 1
from . import pago20
9 1
from .. import Signer
10 1
from ..compute import make_impuestos, rounder, make_impuesto, \
11
    make_impuestos_dr_parcial
12 1
from ...cfdi import CFDI
13 1
from ...transform import get_timezone
14 1
from ...utils import ScalarMap
15 1
from ...utils import iterate
16
17
18 1
class CfdiRelacionados(ScalarMap):
19
    """
20
    Nodo opcional para precisar la información de los comprobantes relacionados.
21
22
    :param tipo_relacion: Atributo requerido para indicar la clave de la relación que existe entre éste que se está generando y el o los CFDI previos.
23
    :param cfdi_relacionado: Nodo requerido para precisar la información de los comprobantes relacionados.
24
    """
25
26 1
    def __init__(
27
            self,
28
            tipo_relacion: str,
29
            cfdi_relacionado: str | Sequence[str],
30
    ):
31
        super().__init__({
32
            'TipoRelacion': tipo_relacion,
33
            'CfdiRelacionado': cfdi_relacionado,
34
        })
35
36
37 1
class InformacionGlobal(ScalarMap):
38
    """
39
    Nodo condicional para precisar la información relacionada con el comprobante global.
40
41
    :param periodicidad: Atributo requerido para expresar el período al que corresponde la información del comprobante global.
42
    :param meses: Atributo requerido para expresar el mes o los meses al que corresponde la información del comprobante global.
43
    :param ano: Atributo requerido para expresar el año al que corresponde la información del comprobante global.
44
    """
45
46 1
    def __init__(
47
            self,
48
            periodicidad: str,
49
            meses: str,
50
            ano: int,
51
    ):
52
        super().__init__({
53
            'Periodicidad': periodicidad,
54
            'Meses': meses,
55
            'Año': ano,
56
        })
57
58
59 1 View Code Duplication
class Parte(ScalarMap):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
60
    """
61
    Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital por Internet.
62
63
    :param clave_prod_serv: Atributo requerido para expresar la clave del producto o del servicio amparado por la presente parte. Es requerido y deben utilizar las claves del catálogo de productos y servicios, cuando los conceptos que registren por sus actividades correspondan con dichos conceptos.
64
    :param cantidad: Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por la presente parte.
65
    :param descripcion: Atributo requerido para precisar la descripción del bien o servicio cubierto por la presente parte.
66
    :param no_identificacion: Atributo opcional para expresar el número de serie, número de parte del bien o identificador del producto o del servicio amparado por la presente parte. Opcionalmente se puede utilizar claves del estándar GTIN.
67
    :param unidad: Atributo opcional para precisar la unidad de medida propia de la operación del emisor, aplicable para la cantidad expresada en la parte. La unidad debe corresponder con la descripción de la parte.
68
    :param valor_unitario: Atributo opcional para precisar el valor o precio unitario del bien o servicio cubierto por la presente parte. No se permiten valores negativos.
69
    :param importe: Atributo opcional para precisar el importe total de los bienes o servicios de la presente parte. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario expresado en la parte. No se permiten valores negativos.
70
    :param informacion_aduanera: Nodo opcional para introducir la información aduanera aplicable cuando se trate de ventas de primera mano de mercancías importadas o se trate de operaciones de comercio exterior con bienes o servicios.
71
    """
72
73 1
    def __init__(
74
            self,
75
            clave_prod_serv: str,
76
            cantidad: Decimal | int,
77
            descripcion: str,
78
            no_identificacion: str = None,
79
            unidad: str = None,
80
            valor_unitario: Decimal | int = None,
81
            importe: Decimal | int = None,
82
            informacion_aduanera: str | Sequence[str] = None,
83
    ):
84
        super().__init__({
85
            'ClaveProdServ': clave_prod_serv,
86
            'Cantidad': cantidad,
87
            'Descripcion': descripcion,
88
            'NoIdentificacion': no_identificacion,
89
            'Unidad': unidad,
90
            'ValorUnitario': valor_unitario,
91
            'Importe': importe,
92
            'InformacionAduanera': informacion_aduanera,
93
        })
94
95
96 1
class ACuentaTerceros(ScalarMap):
97
    """
98
    Nodo opcional para registrar información del contribuyente Tercero, a cuenta del que se realiza la operación.
99
100
    :param rfc_a_cuenta_terceros: Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes del contribuyente Tercero, a cuenta del que se realiza la operación.
101
    :param nombre_a_cuenta_terceros: Atributo requerido para registrar el nombre, denominación o razón social del contribuyente Tercero correspondiente con el Rfc, a cuenta del que se realiza la operación.
102
    :param regimen_fiscal_a_cuenta_terceros: Atributo requerido para incorporar la clave del régimen del contribuyente Tercero, a cuenta del que se realiza la operación.
103
    :param domicilio_fiscal_a_cuenta_terceros: Atributo requerido para incorporar el código postal del domicilio fiscal del Tercero, a cuenta del que se realiza la operación.
104
    """
105
106 1
    def __init__(
107
            self,
108
            rfc_a_cuenta_terceros: str,
109
            nombre_a_cuenta_terceros: str,
110
            regimen_fiscal_a_cuenta_terceros: str,
111
            domicilio_fiscal_a_cuenta_terceros: str,
112
    ):
113
        super().__init__({
114
            'RfcACuentaTerceros': rfc_a_cuenta_terceros,
115
            'NombreACuentaTerceros': nombre_a_cuenta_terceros,
116
            'RegimenFiscalACuentaTerceros': regimen_fiscal_a_cuenta_terceros,
117
            'DomicilioFiscalACuentaTerceros': domicilio_fiscal_a_cuenta_terceros,
118
        })
119
120
121 1 View Code Duplication
class Traslado(ScalarMap):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
122
    """
123
    Nodo requerido para la información detallada de un traslado de impuesto específico.
124
125
    :param base: Atributo requerido para señalar la suma de los atributos Base de los conceptos del impuesto trasladado. No se permiten valores negativos.
126
    :param impuesto: Atributo requerido para señalar la clave del tipo de impuesto retencion.
127
    :param tipo_factor: Atributo requerido para señalar la clave del tipo de factor que se aplica a la base del impuesto.
128
    :param tasa_o_cuota: Atributo condicional para señalar el valor de la tasa o cuota del impuesto que se traslada por los conceptos amparados en el comprobante.
129
    :param importe: Atributo condicional para señalar la suma del importe del impuesto trasladado, agrupado por impuesto, TipoFactor y TasaOCuota. No se permiten valores negativos.
130
    """
131
132 1
    def __init__(
133
            self,
134
            impuesto: str,
135
            tipo_factor: str,
136
            tasa_o_cuota: Decimal | int = None,
137
            importe: Decimal | int = None,
138
            base: Decimal | int = None,
139
    ):
140 1
        super().__init__({
141
            'Base': base,
142
            'Impuesto': CatImpuesto.get(impuesto, impuesto),
143
            'TipoFactor': tipo_factor,
144
            'TasaOCuota': tasa_o_cuota,
145
            'Importe': importe,
146
        })
147
148 1
    @classmethod  # obsolete
149 1
    def parse(cls, impuesto: str):
150
        parts = impuesto.split("|")
151
        return cls(
152
            impuesto=parts[0],
153
            tipo_factor=parts[1],
154
            tasa_o_cuota=Decimal(parts[2]) if len(parts) > 2 else None,
155
        )
156
157
158 1 View Code Duplication
class Retencion(ScalarMap):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
159
    """
160
    Nodo requerido para la información detallada de un traslado de impuesto específico.
161
162
    :param base: Atributo requerido para señalar la suma de los atributos Base de los conceptos del impuesto trasladado. No se permiten valores negativos.
163
    :param impuesto: Atributo requerido para señalar la clave del tipo de impuesto retencion.
164
    :param tipo_factor: Atributo requerido para señalar la clave del tipo de factor que se aplica a la base del impuesto.
165
    :param tasa_o_cuota: Atributo condicional para señalar el valor de la tasa o cuota del impuesto que se traslada por los conceptos amparados en el comprobante.
166
    :param importe: Atributo condicional para señalar la suma del importe del impuesto trasladado, agrupado por impuesto, TipoFactor y TasaOCuota. No se permiten valores negativos.
167
    """
168
169 1
    def __init__(
170
            self,
171
            impuesto: str,
172
            tipo_factor: str,
173
            tasa_o_cuota: Decimal | int = None,
174
            importe: Decimal | int = None,
175
            base: Decimal | int = None,
176
    ):
177 1
        super().__init__({
178
            'Base': base,
179
            'Impuesto': CatImpuesto.get(impuesto, impuesto),
180
            'TipoFactor': tipo_factor,
181
            'TasaOCuota': tasa_o_cuota,
182
            'Importe': importe,
183
        })
184
185 1
    @classmethod  # obsolete
186 1
    def parse(cls, impuesto: str):
187
        parts = impuesto.split("|")
188
        return cls(
189
            impuesto=parts[0],
190
            tipo_factor=parts[1],
191
            tasa_o_cuota=Decimal(parts[2]) if len(parts) > 2 else None,
192
        )
193
194
195 1 View Code Duplication
class Impuestos(ScalarMap):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
196
    """
197
    Nodo condicional para expresar el resumen de los impuestos aplicables.
198
199
    :param retenciones: Nodo condicional para capturar los impuestos retenidos aplicables. Es requerido cuando en los conceptos se registre algún impuesto retenido.
200
    :param traslados: Nodo condicional para capturar los impuestos trasladados aplicables. Es requerido cuando en los conceptos se registre un impuesto trasladado.
201
    """
202
203 1
    def __init__(
204
            self,
205
            retenciones: Retencion | dict | str | Sequence[Retencion | dict | str] = None,
206
            traslados: Traslado | dict | str | Sequence[Traslado | dict | str] = None,
207
    ):
208 1
        super().__init__({
209
            'Retenciones': retenciones,
210
            'Traslados': traslados,
211
        })
212
213
214 1
class Concepto(ScalarMap):
215
    """
216
    Nodo requerido para registrar la información detallada de un bien o servicio amparado en el comprobante.
217
218
    :param clave_prod_serv: Atributo requerido para expresar la clave del producto o del servicio amparado por el presente concepto. Es requerido y deben utilizar las claves del catálogo de productos y servicios, cuando los conceptos que registren por sus actividades correspondan con dichos conceptos.
219
    :param cantidad: Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por el presente concepto.
220
    :param clave_unidad: Atributo requerido para precisar la clave de unidad de medida estandarizada aplicable para la cantidad expresada en el concepto. La unidad debe corresponder con la descripción del concepto.
221
    :param descripcion: Atributo requerido para precisar la descripción del bien o servicio cubierto por el presente concepto.
222
    :param valor_unitario: Atributo requerido para precisar el valor o precio unitario del bien o servicio cubierto por el presente concepto.
223
    :param objeto_imp: Atributo requerido para expresar si la operación comercial es objeto o no de impuesto.
224
    :param no_identificacion: Atributo opcional para expresar el número de parte, identificador del producto o del servicio, la clave de producto o servicio, SKU o equivalente, propia de la operación del emisor, amparado por el presente concepto. Opcionalmente se puede utilizar claves del estándar GTIN.
225
    :param unidad: Atributo opcional para precisar la unidad de medida propia de la operación del emisor, aplicable para la cantidad expresada en el concepto. La unidad debe corresponder con la descripción del concepto.
226
    :param descuento: Atributo opcional para representar el importe de los descuentos aplicables al concepto. No se permiten valores negativos.
227
    :param a_cuenta_terceros: Nodo opcional para registrar información del contribuyente Tercero, a cuenta del que se realiza la operación.
228
    :param informacion_aduanera: Nodo opcional para introducir la información aduanera aplicable cuando se trate de ventas de primera mano de mercancías importadas o se trate de operaciones de comercio exterior con bienes o servicios.
229
    :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.
230
    :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.
231
    :param parte: Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital por Internet.
232
    :param _traslados_incluidos: si el valor valor_unitario ya incluye traslados.
233
    """
234
235 1
    def __init__(
236
            self,
237
            clave_prod_serv: str,
238
            cantidad: Decimal | int,
239
            clave_unidad: str,
240
            descripcion: str,
241
            valor_unitario: Decimal | int,
242
            objeto_imp: str = None,
243
            no_identificacion: str = None,
244
            unidad: str = None,
245
            descuento: Decimal | int = None,
246
            impuestos: Impuestos | dict = None,
247
            a_cuenta_terceros: ACuentaTerceros | dict = None,
248
            informacion_aduanera: str | Sequence[str] = None,
249
            cuenta_predial: str | Sequence[str] = None,
250
            complemento_concepto: CFDI | Sequence[CFDI] = None,
251
            parte: Parte | Sequence[Parte | dict] = None,
252
            _traslados_incluidos: bool = False
253
    ):
254 1
        super().__init__({
255
            'ClaveProdServ': clave_prod_serv,
256
            'Cantidad': cantidad,
257
            'ClaveUnidad': clave_unidad,
258
            'Descripcion': descripcion,
259
            'ValorUnitario': valor_unitario,
260
            'ObjetoImp': objeto_imp,
261
            'NoIdentificacion': no_identificacion,
262
            'Unidad': unidad,
263
            'Descuento': descuento,
264
            'Impuestos': impuestos,
265
            'ACuentaTerceros': a_cuenta_terceros,
266
            'InformacionAduanera': informacion_aduanera,
267
            'CuentaPredial': cuenta_predial,
268
            'ComplementoConcepto': complemento_concepto,
269
            'Parte': parte,
270
            '_traslados_incluidos': _traslados_incluidos
271
        })
272
273
274 1
class Receptor(ScalarMap):
275
    """
276
    Nodo requerido para precisar la información del contribuyente receptor del comprobante.
277
278
    :param rfc: Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente receptor del comprobante.
279
    :param nombre: Atributo requerido para registrar el nombre(s), primer apellido, segundo apellido, según corresponda, denominación o razón social del contribuyente, inscrito en el RFC, del receptor del comprobante.
280
    :param domicilio_fiscal_receptor: Atributo requerido para registrar el código postal del domicilio fiscal del receptor del comprobante.
281
    :param regimen_fiscal_receptor: Atributo requerido para incorporar la clave del régimen fiscal del contribuyente receptor al que aplicará el efecto fiscal de este comprobante.
282
    :param uso_cfdi: Atributo requerido para expresar la clave del uso que dará a esta factura el receptor del CFDI.
283
    :param residencia_fiscal: Atributo condicional para registrar la clave del país de residencia para efectos fiscales del receptor del comprobante, cuando se trate de un extranjero, y que es conforme con la especificación ISO 3166-1 alpha-3. Es requerido cuando se incluya el complemento de comercio exterior o se registre el atributo NumRegIdTrib.
284
    :param num_reg_id_trib: Atributo condicional para expresar el número de registro de identidad fiscal del receptor cuando sea residente en el extranjero. Es requerido cuando se incluya el complemento de comercio exterior.
285
    """
286
287 1
    def __init__(
288
            self,
289
            rfc: str,
290
            nombre: str,
291
            domicilio_fiscal_receptor: str,
292
            regimen_fiscal_receptor: str,
293
            uso_cfdi: str,
294
            residencia_fiscal: str = None,
295
            num_reg_id_trib: str = None,
296
    ):
297 1
        super().__init__({
298
            'Rfc': rfc,
299
            'Nombre': nombre,
300
            'DomicilioFiscalReceptor': domicilio_fiscal_receptor,
301
            'RegimenFiscalReceptor': regimen_fiscal_receptor,
302
            'UsoCFDI': uso_cfdi,
303
            'ResidenciaFiscal': residencia_fiscal,
304
            'NumRegIdTrib': num_reg_id_trib,
305
        })
306
307
308 1
class Emisor(ScalarMap):
309
    """
310
    Nodo requerido para expresar la información del contribuyente emisor del comprobante.
311
312
    :param rfc: Atributo requerido para registrar la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente emisor del comprobante.
313
    :param nombre: Atributo requerido para registrar el nombre, denominación o razón social del contribuyente inscrito en el RFC, del emisor del comprobante.
314
    :param regimen_fiscal: Atributo requerido para incorporar la clave del régimen del contribuyente emisor al que aplicará el efecto fiscal de este comprobante.
315
    :param fac_atr_adquirente: Atributo condicional para expresar el número de operación proporcionado por el SAT cuando se trate de un comprobante a través de un PCECFDI o un PCGCFDISP.
316
    """
317
318 1
    def __init__(
319
            self,
320
            rfc: str,
321
            nombre: str,
322
            regimen_fiscal: str,
323
            fac_atr_adquirente: str = None,
324
    ):
325 1
        super().__init__({
326
            'Rfc': rfc,
327
            'Nombre': nombre,
328
            'RegimenFiscal': regimen_fiscal,
329
            'FacAtrAdquirente': fac_atr_adquirente,
330
        })
331
332
333 1
@dataclass
334 1
class PagoComprobante:
335 1
    comprobante: CFDI
336 1
    num_parcialidad: int = None
337 1
    imp_saldo_ant: Decimal | int = None
338 1
    imp_pagado: Decimal | int = None
339
340 1
    def __post_init__(self):
341 1
        if self.num_parcialidad is None and self.imp_saldo_ant is None and self.imp_pagado is None:
342 1
            self.num_parcialidad = 1
343 1
            self.imp_saldo_ant = self.comprobante['Total']
344 1
            self.imp_pagado = self.comprobante['Total']
345
346 1
        if self.imp_pagado > self.imp_saldo_ant:
347
            raise ValueError('Importe Pagado debe de ser menor o igual al Importe Saldo Anterior')
348
349
350 1
def _make_conceptos(conceptos, rnd_fn):
351 1
    def make_concepto(concepto):
352 1
        impuestos = concepto.get("Impuestos") or {}
353 1
        trasladados = [x if isinstance(x, dict) else Traslado.parse(x) for x in iterate(impuestos.get("Traslados"))]
354 1
        retenciones = [x if isinstance(x, dict) else Retencion.parse(x) for x in iterate(impuestos.get("Retenciones"))]
355
356 1
        if concepto.get('_traslados_incluidos'):
357 1
            s_tasa = sum(c["TasaOCuota"] for c in trasladados if c["TipoFactor"] == "Tasa")
358 1
            s_cuota = sum(c["TasaOCuota"] for c in trasladados if c["TipoFactor"] == "Cuota")
359 1
            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)):
360
                raise ValueError("Not possible to compute '_traslados_incluidos' if any 'trasladados' contains 'Base' or 'Importe'")
361
362 1
            valor_unitario = concepto['ValorUnitario']
363 1
            valor_unitario = (valor_unitario - s_cuota) / (s_tasa + 1)
364 1
            concepto['ValorUnitario'] = rnd_fn(valor_unitario)
365
        else:
366 1
            valor_unitario = concepto['ValorUnitario']
367
368 1
        importe = concepto["Cantidad"] * valor_unitario
369 1
        concepto["Importe"] = rnd_fn(importe)
370
371 1
        if concepto.get("ObjetoImp") in ("01", "03"):
372 1
            concepto['Impuestos'] = None
373
        else:
374 1
            base = importe - (concepto.get("Descuento") or 0)
375 1
            impuestos = {
376
                imp_t: [
377
                    make_impuesto(i, base=base, rnd_fn=rnd_fn) for i in imp
378
                ]
379
                for imp_t, imp in [('Traslados', trasladados), ('Retenciones', retenciones)] if imp
380
            }
381 1
            concepto['Impuestos'] = impuestos or None
382 1
            concepto["ObjetoImp"] = "02" if impuestos else "01"
383
384 1
        return concepto
385
386 1
    return [make_concepto(c) for c in iterate(conceptos)]
387
388
389
# MAIN #
390 1
class Comprobante(CFDI):
391
    """
392
    Estándar de Comprobante Fiscal Digital por Internet.
393
394
    :param emisor: Nodo requerido para expresar la información del contribuyente emisor del comprobante.
395
    :param lugar_expedicion: Atributo requerido para incorporar el código postal del lugar de expedición del comprobante (domicilio de la matriz o de la sucursal).
396
    :param receptor: Nodo requerido para precisar la información del contribuyente receptor del comprobante.
397
    :param conceptos: Nodo requerido para listar los conceptos cubiertos por el comprobante.
398
    :param moneda: Atributo requerido para identificar la clave de la moneda utilizada para expresar los montos, cuando se usa moneda nacional se registra MXN. Conforme con la especificación ISO 4217.
399
    :param tipo_de_comprobante: Atributo requerido para expresar la clave del efecto del comprobante fiscal para el contribuyente emisor.
400
    :param exportacion: Atributo requerido para expresar si el comprobante ampara una operación de exportación.
401
    :param serie: Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres.
402
    :param folio: Atributo opcional para control interno del contribuyente que expresa el folio del comprobante, acepta una cadena de caracteres.
403
    :param forma_pago: Atributo condicional para expresar la clave de la forma de pago de los bienes o servicios amparados por el comprobante.
404
    :param condiciones_de_pago: Atributo condicional para expresar las condiciones comerciales aplicables para el pago del comprobante fiscal digital por Internet. Este atributo puede ser condicionado mediante atributos o complementos.
405
    :param tipo_cambio: Atributo condicional para representar el tipo de cambio FIX conforme con la moneda usada. Es requerido cuando la clave de moneda es distinta de MXN y de XXX. El valor debe reflejar el número de pesos mexicanos que equivalen a una unidad de la divisa señalada en el atributo moneda. Si el valor está fuera del porcentaje aplicable a la moneda tomado del catálogo c_Moneda, el emisor debe obtener del PAC que vaya a timbrar el CFDI, de manera no automática, una clave de confirmación para ratificar que el valor es correcto e integrar dicha clave en el atributo Confirmacion.
406
    :param metodo_pago: Atributo condicional para precisar la clave del método de pago que aplica para este comprobante fiscal digital por Internet, conforme al Artículo 29-A fracción VII incisos a y b del CFF.
407
    :param confirmacion: Atributo condicional para registrar la clave de confirmación que entregue el PAC para expedir el comprobante con importes grandes, con un tipo de cambio fuera del rango establecido o con ambos casos. Es requerido cuando se registra un tipo de cambio o un total fuera del rango establecido.
408
    :param informacion_global: Nodo condicional para precisar la información relacionada con el comprobante global.
409
    :param cfdi_relacionados: Nodo opcional para precisar la información de los comprobantes relacionados.
410
    :param complemento: Nodo opcional donde se incluye el complemento Timbre Fiscal Digital de manera obligatoria y los nodos complementarios determinados por el SAT, de acuerdo con las disposiciones particulares para un sector o actividad específica.
411
    :param addenda: Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato origen.
412
    :param fecha: Atributo requerido para la expresión de la fecha y hora de expedición del Comprobante Fiscal Digital por Internet. Se expresa en la forma AAAA-MM-DDThh:mm:ss y debe corresponder con la hora local donde se expide el comprobante.
413
    """
414
415 1
    tag = '{http://www.sat.gob.mx/cfd/4}Comprobante'
416 1
    version = '4.0'
417 1
    complemento_pago = pago20.Pagos
418
419 1
    def __init__(
420
            self,
421
            emisor: Emisor | dict,
422
            lugar_expedicion: str,
423
            receptor: Receptor | dict,
424
            conceptos: Concepto | Sequence[Concepto | dict],
425
            moneda: str = "MXN",
426
            tipo_de_comprobante: str = "I",
427
            exportacion: str = "01",
428
            serie: str = None,
429
            folio: str = None,
430
            forma_pago: str = None,
431
            condiciones_de_pago: str = None,
432
            tipo_cambio: Decimal | int = None,
433
            metodo_pago: str = None,
434
            confirmacion: str = None,
435
            informacion_global: InformacionGlobal | dict = None,
436
            cfdi_relacionados: CfdiRelacionados | Sequence[CfdiRelacionados | dict] = None,
437
            complemento: CFDI | Sequence[CFDI] = None,
438
            addenda: CFDI | Sequence[CFDI] = None,
439
            fecha: datetime = None,
440
    ):
441 1
        super().__init__({
442
            'Version': None,
443
            'Fecha': fecha,
444
            'Sello': '',
445
            'SubTotal': None,
446
            'Moneda': moneda,
447
            'Total': None,
448
            'TipoDeComprobante': tipo_de_comprobante,
449
            'Exportacion': exportacion,
450
            'LugarExpedicion': lugar_expedicion,
451
            'Serie': serie,
452
            'Folio': folio,
453
            'FormaPago': forma_pago,
454
            'CondicionesDePago': condiciones_de_pago,
455
            'Descuento': None,
456
            'TipoCambio': tipo_cambio,
457
            'MetodoPago': metodo_pago,
458
            'Confirmacion': confirmacion,
459
            'InformacionGlobal': informacion_global,
460
            'CfdiRelacionados': cfdi_relacionados,
461
            'Emisor': emisor,
462
            'Receptor': receptor,
463
            'Conceptos': conceptos,
464
            'Impuestos': None,
465
            'Complemento': complemento,
466
            'Addenda': addenda,
467
            'NoCertificado': '',
468
            'Certificado': '',
469
        })
470 1
        self.compute()
471
472 1
    def compute(self):
473 1
        self['Version'] = self.version
474 1
        self['Fecha'] = self.get('Fecha') or datetime.now(tz=get_timezone(self['LugarExpedicion'])).replace(tzinfo=None)
475 1
        self['Moneda'] = self.get('Moneda') or 'MXN'
476 1
        self['Sello'] = ''
477 1
        self['TipoDeComprobante'] = self.get('TipoDeComprobante') or 'I'
478 1
        self['Exportacion'] = self.get('Exportacion') or '01'
479
480 1
        self["Conceptos"] = conceptos = _make_conceptos(self["Conceptos"], rnd_fn=rounder(self["Moneda"]))
481 1
        self["SubTotal"] = sub_total = sum(c['Importe'] for c in conceptos)
482 1
        descuento = sum(c.get('Descuento') or 0 for c in conceptos)
483 1
        self['Impuestos'] = impuestos = make_impuestos(conceptos)
484
485 1
        total = sub_total - descuento
486 1
        if impuestos:
487 1
            total += impuestos.get('TotalImpuestosTrasladados', 0)
488 1
            total -= impuestos.get('TotalImpuestosRetenidos', 0)
489
490 1
        self['Total'] = total
491 1
        self['Descuento'] = descuento or None
492
493 1
    def sign(self, signer: Signer):
494 1
        self['NoCertificado'] = signer.certificate_number
495 1
        self['Certificado'] = signer.certificate_base64()
496 1
        self['Sello'] = signer.sign_sha256(
497
            self.cadena_original().encode()
498
        )
499
500 1
    @classmethod
501 1
    def pago(
502
            cls,
503
            emisor: Emisor | dict,
504
            lugar_expedicion: str,
505
            receptor: Receptor | dict,
506
            complemento_pago: CFDI,
507
            cfdi_relacionados: CfdiRelacionados | Sequence[CfdiRelacionados | dict] = None,
508
            confirmacion: str = None,
509
            serie: str = None,
510
            folio: str = None,
511
            addenda: CFDI | Sequence[CFDI] = None,
512
            fecha: datetime = None) -> 'Comprobante':
513
        """
514
        Estándar de Comprobante Fiscal Digital por Internet de Tipo Pago.
515
516
        :param emisor: Nodo requerido para expresar la información del contribuyente emisor del comprobante.
517
        :param lugar_expedicion: Atributo requerido para incorporar el código postal del lugar de expedición del comprobante (domicilio de la matriz o de la sucursal).
518
        :param receptor: Nodo requerido para precisar la información del contribuyente receptor del comprobante.
519
        :param complemento_pago: Pago
520
        :param serie: Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres.
521
        :param folio: Atributo opcional para control interno del contribuyente que expresa el folio del comprobante, acepta una cadena de caracteres.
522
        :param confirmacion: Atributo condicional para registrar la clave de confirmación que entregue el PAC para expedir el comprobante con importes grandes, con un tipo de cambio fuera del rango establecido o con ambos casos. Es requerido cuando se registra un tipo de cambio o un total fuera del rango establecido.
523
        :param cfdi_relacionados: Nodo opcional para precisar la información de los comprobantes relacionados.
524
        :param addenda: Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato origen.
525
        :param fecha: Atributo requerido para la expresión de la fecha y hora de expedición del Comprobante Fiscal Digital por Internet. Se expresa en la forma AAAA-MM-DDThh:mm:ss y debe corresponder con la hora local donde se expide el comprobante.
526
        :return: Comprobante
527
        """
528 1
        if cls.version == "3.3":
529 1
            receptor["UsoCFDI"] = "P01"
530
        else:
531 1
            receptor["UsoCFDI"] = "CP01"
532
533 1
        return cls(
534
            emisor=emisor,
535
            lugar_expedicion=lugar_expedicion,
536
            receptor=receptor,
537
            conceptos=Concepto(
538
                clave_prod_serv='84111506',
539
                cantidad=1,
540
                clave_unidad='ACT',
541
                descripcion='Pago',
542
                valor_unitario=Decimal(0),
543
                objeto_imp="01"
544
            ),
545
            complemento=complemento_pago,
546
            serie=serie,
547
            folio=folio,
548
            moneda='XXX',
549
            tipo_de_comprobante='P',
550
            cfdi_relacionados=cfdi_relacionados,
551
            confirmacion=confirmacion,
552
            exportacion="01",
553
            addenda=addenda,
554
            fecha=fecha
555
        )
556
557 1
    @classmethod
558 1
    def _pago_tipo_cambio(cls, moneda, tipo_cambio):
559
        # CRP204: El campo TipoCambioP no debe estar presente cuando el campo Moneda contenga ^MXN$ en el nodo Pago
560 1
        if cls.complemento_pago.version == "1.0":
561 1
            if moneda == 'MXN' and tipo_cambio == 1:
562
                tipo_cambio = None
563
        else:
564 1
            if moneda == 'MXN' and tipo_cambio is None:
565 1
                tipo_cambio = 1
566 1
        return tipo_cambio
567
568 1
    @classmethod
569 1
    def pago_comprobantes(
570
            cls,
571
            comprobantes: CFDI | PagoComprobante | Sequence[CFDI | PagoComprobante],
572
            fecha_pago: datetime,
573
            forma_pago: str,
574
            emisor: Emisor | dict = None,
575
            lugar_expedicion: str = None,
576
            receptor: Receptor | dict = None,
577
            tipo_cambio: Decimal | int = None,
578
            cfdi_relacionados: CfdiRelacionados | Sequence[CfdiRelacionados | dict] = None,
579
            confirmacion: str = None,
580
            serie: str = None,
581
            folio: str = None,
582
            addenda: CFDI | Sequence[CFDI] = None,
583
            fecha: datetime = None) -> 'Comprobante':
584
        """
585
        Estándar de Comprobante Fiscal Digital por Internet de Tipo Pago. Generado a partir de una lista de Comprobantes
586
        Se asume que los comprobantes se pagan en su totalidad en una sola exhibición
587
588
        :param emisor: Nodo requerido para expresar la información del contribuyente emisor del comprobante.
589
        :param lugar_expedicion: Atributo requerido para incorporar el código postal del lugar de expedición del comprobante (domicilio de la matriz o de la sucursal).
590
        :param receptor: Nodo requerido para precisar la información del contribuyente receptor del comprobante.
591
        :param comprobantes: CFDI(s) de Comprobante de Ingreso para generar el pago por su monto total o parcial usando PagoComprobante
592
        :param fecha_pago: Atributo requerido para expresar la fecha y hora en la que el beneficiario recibe el pago. Se expresa en la forma aaaa-mm-ddThh:mm:ss, de acuerdo con la especificación ISO 8601.En caso de no contar con la hora se debe registrar 12:00:00.
593
        :param serie: Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres.
594
        :param folio: Atributo opcional para control interno del contribuyente que expresa el folio del comprobante, acepta una cadena de caracteres.
595
        :param forma_pago: Atributo condicional para expresar la clave de la forma de pago de los bienes o servicios amparados por el comprobante.
596
        :param tipo_cambio: Atributo condicional para representar el tipo de cambio FIX conforme con la moneda usada. Es requerido cuando la clave de moneda es distinta de MXN y de XXX. El valor debe reflejar el número de pesos mexicanos que equivalen a una unidad de la divisa señalada en el atributo moneda. Si el valor está fuera del porcentaje aplicable a la moneda tomado del catálogo c_Moneda, el emisor debe obtener del PAC que vaya a timbrar el CFDI, de manera no automática, una clave de confirmación para ratificar que el valor es correcto e integrar dicha clave en el atributo Confirmacion.
597
        :param confirmacion: Atributo condicional para registrar la clave de confirmación que entregue el PAC para expedir el comprobante con importes grandes, con un tipo de cambio fuera del rango establecido o con ambos casos. Es requerido cuando se registra un tipo de cambio o un total fuera del rango establecido.
598
        :param cfdi_relacionados: Nodo opcional para precisar la información de los comprobantes relacionados.
599
        :param addenda: Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato origen.
600
        :param fecha: Atributo requerido para la expresión de la fecha y hora de expedición del Comprobante Fiscal Digital por Internet. Se expresa en la forma AAAA-MM-DDThh:mm:ss y debe corresponder con la hora local donde se expide el comprobante.
601
        :return: Comprobante
602
        """
603 1
        comprobantes = [c if isinstance(c, PagoComprobante) else PagoComprobante(comprobante=c) for c in iterate(comprobantes)]
604 1
        first_cfdi = comprobantes[0].comprobante
605 1
        moneda = first_cfdi['Moneda']
606
607 1
        emisor = emisor or first_cfdi['Emisor'].copy()
608 1
        lugar_expedicion = lugar_expedicion or first_cfdi['LugarExpedicion']
609 1
        receptor = receptor or first_cfdi['Receptor'].copy()
610
611 1
        tipo_cambio = cls._pago_tipo_cambio(moneda, tipo_cambio)
612
613 1
        if not all(
614
                c.comprobante["Moneda"] == moneda
615
                and c.comprobante["Emisor"]["Rfc"] == emisor["Rfc"]
616
                and c.comprobante["Emisor"]["RegimenFiscal"] == emisor["RegimenFiscal"]
617
                and c.comprobante["Receptor"]["Rfc"] == receptor["Rfc"]
618
                and c.comprobante["Receptor"].get("RegimenFiscalReceptor") == receptor.get("RegimenFiscalReceptor")
619
                for c in comprobantes
620
        ):
621
            raise ValueError("CFDIS are of different RFC's Emisor/Receptor o Moneda")
622
623 1
        return cls.pago(
624
            emisor=emisor,
625
            lugar_expedicion=lugar_expedicion,
626
            receptor=receptor,
627
            complemento_pago=cls.complemento_pago(
628
                pago=[{
629
                    'DoctoRelacionado': [
630
                        {
631
                            'IdDocumento': c.comprobante["Complemento"]["TimbreFiscalDigital"]["UUID"],
632
                            'Serie': c.comprobante.get("Serie"),
633
                            'Folio': c.comprobante.get("Folio"),
634
                            'MonedaDR': c.comprobante["Moneda"],
635
                            'EquivalenciaDR': 1,
636
                            'MetodoDePagoDR': c.comprobante["MetodoPago"],
637
                            'NumParcialidad': c.num_parcialidad,
638
                            'ImpSaldoAnt': c.imp_saldo_ant,
639
                            'ImpPagado': c.imp_pagado,
640
                            'ObjetoImpDR': '02' if 'Impuestos' in c.comprobante else '01',
641
                            'ImpuestosDR': make_impuestos_dr_parcial(
642
                                conceptos=c.comprobante['Conceptos'],
643
                                imp_saldo_ant=c.imp_saldo_ant,
644
                                imp_pagado=c.imp_pagado,
645
                                total=c.comprobante["Total"],
646
                                rnd_fn=rounder(c.comprobante["Moneda"])
647
                            ) if 'Impuestos' in c.comprobante else None
648
                        } for c in comprobantes
649
                    ],
650
                    'FechaPago': fecha_pago,
651
                    'FormaDePagoP': forma_pago,
652
                    'MonedaP': moneda,
653
                    'TipoCambioP': tipo_cambio
654
                }]
655
            ),
656
            cfdi_relacionados=cfdi_relacionados,
657
            confirmacion=confirmacion,
658
            serie=serie,
659
            folio=folio,
660
            addenda=addenda,
661
            fecha=fecha
662
        )
663
664 1
    @classmethod
665 1
    def nomina(
666
            cls,
667
            emisor: Emisor | dict,
668
            lugar_expedicion: str,
669
            receptor: Receptor | dict,
670
            complemento_nomina: CFDI,
671
            cfdi_relacionados: CfdiRelacionados | Sequence[CfdiRelacionados | dict] = None,
672
            confirmacion: str = None,
673
            serie: str = None,
674
            folio: str = None,
675
            addenda: CFDI | Sequence[CFDI] = None,
676
            fecha: datetime = None) -> 'Comprobante':
677
        """
678
        Estándar de Comprobante Fiscal Digital por Internet de Tipo Pago.
679
680
        :param emisor: Nodo requerido para expresar la información del contribuyente emisor del comprobante.
681
        :param lugar_expedicion: Atributo requerido para incorporar el código postal del lugar de expedición del comprobante (domicilio de la matriz o de la sucursal).
682
        :param receptor: Nodo requerido para precisar la información del contribuyente receptor del comprobante.
683
        :param complemento_nomina: Pago
684
        :param serie: Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres.
685
        :param folio: Atributo opcional para control interno del contribuyente que expresa el folio del comprobante, acepta una cadena de caracteres.
686
        :param confirmacion: Atributo condicional para registrar la clave de confirmación que entregue el PAC para expedir el comprobante con importes grandes, con un tipo de cambio fuera del rango establecido o con ambos casos. Es requerido cuando se registra un tipo de cambio o un total fuera del rango establecido.
687
        :param cfdi_relacionados: Nodo opcional para precisar la información de los comprobantes relacionados.
688
        :param addenda: Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato origen.
689
        :param fecha: Atributo requerido para la expresión de la fecha y hora de expedición del Comprobante Fiscal Digital por Internet. Se expresa en la forma AAAA-MM-DDThh:mm:ss y debe corresponder con la hora local donde se expide el comprobante.
690
        :return: Comprobante
691
        """
692 1
        if cls.version == "3.3":
693 1
            receptor["UsoCFDI"] = "P01"
694
        else:
695 1
            receptor["UsoCFDI"] = "CN01"
696
697 1
        concepto = Concepto(
698
            clave_prod_serv='84111505',
699
            cantidad=1,
700
            clave_unidad='ACT',
701
            descripcion='Pago de nómina',
702
            valor_unitario=complemento_nomina.get('TotalPercepciones', 0) + complemento_nomina.get('TotalOtrosPagos', 0),
703
            descuento=complemento_nomina.get('TotalDeducciones'),
704
            objeto_imp="03"
705
        )
706 1
        return cls(
707
            emisor=emisor,
708
            lugar_expedicion=lugar_expedicion,
709
            receptor=receptor,
710
            conceptos=concepto,
711
            complemento=complemento_nomina,
712
            serie=serie,
713
            folio=folio,
714
            moneda='MXN',
715
            tipo_de_comprobante='N',
716
            metodo_pago="PUE",
717
            forma_pago="99",
718
            cfdi_relacionados=cfdi_relacionados,
719
            confirmacion=confirmacion,
720
            exportacion="01",
721
            addenda=addenda,
722
            fecha=fecha,
723
        )
724