Passed
Push — main ( ee97bc...7c0be5 )
by Sat CFDI
06:41
created

satcfdi.create.cfd.cfdi40.Comprobante.pago()   B

Complexity

Conditions 2

Size

Total Lines 55
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 37
nop 11
dl 0
loc 55
ccs 6
cts 6
cp 1
crap 2
rs 8.9919
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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