Passed
Push — main ( dd1fed...7811f0 )
by Sat CFDI
05:19
created

satcfdi.create.cfd.cfdi40.Comprobante.compute()   A

Complexity

Conditions 2

Size

Total Lines 20
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 2

Importance

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