from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from uuid import UUID
from .. import CFDI, XElement
from ..create.catalogos import EstadoComprobante
from ..create.cfd.catalogos import MetodoPago, TipoDeComprobante, TipoRelacion
from ..create.compute import make_impuestos_dr_parcial, rounder, group_impuestos, encode_impuesto, calculate_partial
[docs]class SatCFDI(CFDI):
"""
SatCFDI is an extension of a CFDI to represent a CFDI that has been sent to SAT
"""
def __new__(cls, *args, **kwargs):
return super().__new__(cls, *args, **kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.relations = [] # type: list[Relation]
self.payments = [] # type: list[Payment]
@property
def uuid(self):
return UUID(self["Complemento"]["TimbreFiscalDigital"]["UUID"])
@property
def name(self):
return self.get("Serie", "") + self.get("Folio", "")
@property
def saldo_pendiente(self) -> Decimal | None:
if self["TipoDeComprobante"] == TipoDeComprobante.INGRESO:
# Nota de crédito de los documentos relacionados
credit_notes = sum(
c.comprobante["Total"]
for c in self.relations
if c.cfdi_relacionados["TipoRelacion"] == TipoRelacion.NOTA_DE_CREDITO_DE_LOS_DOCUMENTOS_RELACIONADOS
and c.comprobante['TipoDeComprobante'] == TipoDeComprobante.EGRESO
and c.comprobante.estatus == EstadoComprobante.VIGENTE
)
insoluto = min(
(c.docto_relacionado['ImpSaldoInsoluto']
for c in self.payments
if c.comprobante.estatus == EstadoComprobante.VIGENTE),
default=None
)
if insoluto is not None:
return insoluto - credit_notes
insoluto = self["Total"] - credit_notes
if self["MetodoPago"] == MetodoPago.PAGO_EN_PARCIALIDADES_O_DIFERIDO:
return insoluto
if self["MetodoPago"] == MetodoPago.PAGO_EN_UNA_SOLA_EXHIBICION:
return Decimal(0)
return None
@property
def ultima_num_parcialidad(self) -> int:
return max((c.docto_relacionado['NumParcialidad'] for c in self.payments), default=0)
[docs] def consulta_estado(self) -> dict:
raise NotImplementedError()
@property
def estatus(self) -> EstadoComprobante:
raise NotImplementedError()
@property
def fecha_cancelacion(self) -> datetime | None:
raise NotImplementedError()
[docs]@dataclass(slots=True, init=True)
class Relation:
cfdi_relacionados: XElement
comprobante: SatCFDI
[docs]@dataclass(slots=True, init=True)
class Payment:
comprobante: SatCFDI
pago: XElement = None
docto_relacionado: XElement = None
[docs]@dataclass
class PaymentsDetails(Payment):
comprobante_pagado: SatCFDI = None
def __post_init__(self):
if self.pago:
self.impuestos = self.docto_relacionado.get('ImpuestosDR')
if self.impuestos is None:
self.impuestos = make_impuestos_dr_parcial(
conceptos=self.comprobante_pagado['Conceptos'],
imp_saldo_ant=self.docto_relacionado['ImpSaldoAnt'],
imp_pagado=self.docto_relacionado['ImpPagado'],
total=self.comprobante_pagado['Total'],
rnd_fn=rounder(self.comprobante_pagado['Moneda'])
)
self.impuestos = group_impuestos([{
"ImpuestosDR": self.impuestos
}], pfx="DR", ofx="")
for imp, imps in self.impuestos.items():
self.impuestos[imp] = {
encode_impuesto(
impuesto=v['Impuesto'],
tipo_factor=v.get("TipoFactor"),
tasa_cuota=v.get('TasaOCuota')
): v
for v in imps
}
def calc_parcial(field):
return calculate_partial(
value=self.comprobante_pagado.get(field),
imp_saldo_ant=self.docto_relacionado['ImpSaldoAnt'],
imp_pagado=self.docto_relacionado["ImpPagado"],
total=self.comprobante_pagado["Total"],
rnd_fn=rounder(self.comprobante_pagado['Moneda'])
)
self.sub_total = calc_parcial("SubTotal")
self.descuento = calc_parcial("Descuento")
self.total = self.docto_relacionado["ImpPagado"]
else:
self.impuestos = self.comprobante.get("Impuestos", {})
self.sub_total = self.comprobante["SubTotal"]
self.descuento = self.comprobante.get("Descuento")
self.total = self.comprobante["Total"]