Passed
Push — main ( 44cd99...acb936 )
by Sat CFDI
04:53
created

ACuentaTerceros.__init__()   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

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