Passed
Push — main ( 859eb4...97ed8a )
by Sat CFDI
05:43
created

satcfdi.create.cfd.cfdi40   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 706
Duplicated Lines 17.42 %

Test Coverage

Coverage 91.24%

Importance

Changes 0
Metric Value
eloc 437
dl 123
loc 706
ccs 125
cts 137
cp 0.9124
rs 8.4
c 0
b 0
f 0
wmc 50

19 Methods

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

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