Passed
Push — main ( f626f8...ecf6c7 )
by Sat CFDI
04:48
created

satcfdi.create.cfd.cfdi40   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 674
Duplicated Lines 5.79 %

Test Coverage

Coverage 93.39%

Importance

Changes 0
Metric Value
eloc 402
dl 39
loc 674
ccs 113
cts 121
cp 0.9339
rs 8.72
c 0
b 0
f 0
wmc 46

14 Methods

Rating   Name   Duplication   Size   Complexity  
A Impuesto.__init__() 0 24 1
A Impuesto.parse() 0 7 2
B Concepto.__init__() 0 62 1
A Receptor.__init__() 0 30 1
A PagoComprobante.__post_init__() 0 8 5
A InformacionGlobal.__init__() 0 18 1
A CfdiRelacionados.__init__() 0 15 1
A ACuentaTerceros.__init__() 0 21 1
A Parte.__init__() 33 33 1
C Comprobante.pago_comprobantes() 0 88 9
B Comprobante._pago_tipo_cambio() 0 10 6
B Comprobante.__init__() 0 95 4
B Comprobante.pago() 0 55 2
B Comprobante.nomina() 0 59 2

1 Function

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