Comprobante._pago_tipo_cambio()   B
last analyzed

Complexity

Conditions 6

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.0702

Importance

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