Passed
Push — main ( 44031f...fd93eb )
by Sat CFDI
05:07
created

satcfdi.create.cfd.cfdi40   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 741
Duplicated Lines 7.42 %

Test Coverage

Coverage 93.15%

Importance

Changes 0
Metric Value
eloc 455
dl 55
loc 741
ccs 136
cts 146
cp 0.9315
rs 8.4
c 0
b 0
f 0
wmc 50

20 Methods

Rating   Name   Duplication   Size   Complexity  
A Impuesto.__init__() 0 14 1
A Concepto.__init__() 0 36 1
A Receptor.__init__() 0 18 1
B Comprobante.nomina() 0 59 2
A Parte.__init__() 20 20 1
A Retencion.__init__() 0 14 1
A Traslado.__init__() 0 14 1
A Impuesto.parse() 0 7 2
A Impuestos.__init__() 8 8 1
A Comprobante.compute() 0 20 2
C Comprobante.pago_comprobantes() 0 94 9
A PagoComprobante.__post_init__() 0 8 5
B Comprobante._pago_tipo_cambio() 0 10 6
A InformacionGlobal.__init__() 0 10 1
B Comprobante.__init__() 0 52 1
A Comprobante.sign() 0 5 1
A CfdiRelacionados.__init__() 0 8 1
B Comprobante.pago() 0 55 2
A ACuentaTerceros.__init__() 0 12 1
A Emisor.__init__() 0 12 1

1 Function

Rating   Name   Duplication   Size   Complexity  
C _make_conceptos() 0 37 9

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like satcfdi.create.cfd.cfdi40 often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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