from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from uuid import UUID
from .. import CFDI, XElement, CFDIError
from ..create.compute import make_impuestos_dr_parcial, rounder, group_impuestos, encode_impuesto, calculate_partial
from ..pacs.sat import SAT
from ..transform.helpers import fmt_decimal, strcode
PPD = "PPD"
PUE = "PUE"
sat = SAT()
[docs]class SatCFDI(CFDI):
relations = None # type: list[Relation]
payments = None # type: list[Payment]
"""
SatCFDI is an extension of a CFDI to represent a CFDI that has been sent to SAT
"""
@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:
if self["TipoDeComprobante"] == "I":
# Nota de crédito de los documentos relacionados
credit_notes = sum(
c.comprobante["Total"]
for c in self.relations
if c.cfdi_relacionados["TipoRelacion"] == "01" and c.comprobante['TipoDeComprobante'] == "E" and c.comprobante.estatus != '0'
)
insoluto = min(
(c.docto_relacionado['ImpSaldoInsoluto']
for c in self.payments
if c.comprobante.estatus != '0'),
default=None
)
if insoluto is not None:
return insoluto - credit_notes
insoluto = self["Total"] - credit_notes
if self["MetodoPago"] == PPD:
return insoluto
if self["MetodoPago"] == PUE:
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):
raise NotImplementedError()
@property
def metadata(self) -> str:
match self.tag:
case '{http://www.sat.gob.mx/cfd/3}Comprobante' | '{http://www.sat.gob.mx/cfd/4}Comprobante':
# Uuid~RfcEmisor~NombreEmisor~RfcReceptor~NombreReceptor~RfcPac~FechaEmision~FechaCertificacionSat~Monto~EfectoComprobante~Estatus~FechaCancelacion
return "{uuid}~{rfc_emisor}~{nombre_emisor}~{rfc_receptor}~{nombre_receptor}~{rfc_pac}~{fecha_emision}~{fecha_certificacion_sat}~{monto}~{efecto_comprobante}~{estatus}~{fecha_cancelacion}".format(
uuid=self["Complemento"]["TimbreFiscalDigital"]["UUID"],
rfc_emisor=self['Emisor']["Rfc"],
nombre_emisor=self['Emisor']['Nombre'],
rfc_receptor=self['Receptor']['Rfc'],
nombre_receptor=self['Receptor'].get('Nombre', ''),
rfc_pac=self["Complemento"]["TimbreFiscalDigital"]["RfcProvCertif"],
fecha_emision=self["Fecha"],
fecha_certificacion_sat=self["Complemento"]["TimbreFiscalDigital"]["FechaTimbrado"],
monto=fmt_decimal(self["Total"]),
efecto_comprobante=strcode(self["TipoDeComprobante"]),
estatus=self.estatus,
fecha_cancelacion=self.fecha_cancelacion or ''
)
case '{http://www.sat.gob.mx/esquemas/retencionpago/1}Retenciones':
# Uuid~RfcEmisor~NombreEmisor~RfcReceptor~NombreReceptor~RfcPac~FechaEmision~FechaCertificacionSat~MontoOp~MontoRet~Estatus~FechaCancelacion
cert_sat = sat.recover_certificate(
no_certificado=self["Complemento"]["TimbreFiscalDigital"]["NoCertificadoSAT"]
)
return "{uuid}~{rfc_emisor}~{nombre_emisor}~{rfc_receptor}~{nombre_receptor}~{rfc_pac}~{fecha_emision}~{fecha_certificacion_sat}~{monto_op}~{monto_ret}~{estatus}~{fecha_cancelacion}".format(
uuid=self["Complemento"]["TimbreFiscalDigital"]["UUID"],
rfc_emisor=self["Emisor"]["RFCEmisor"],
nombre_emisor=self['Emisor']['NomDenRazSocE'],
rfc_receptor=self['Receptor']['Nacional']['RFCRecep'],
nombre_receptor=self['Receptor']['Nacional']['NomDenRazSocR'],
rfc_pac=cert_sat.rfc_pac,
fecha_emision=self["FechaExp"],
fecha_certificacion_sat=self["Complemento"]["TimbreFiscalDigital"]["FechaTimbrado"],
monto_op=fmt_decimal(self["Totales"]["MontoTotOperacion"]),
monto_ret=fmt_decimal(self["Totales"]["MontoTotRet"]),
estatus=self.estatus,
fecha_cancelacion=self.fecha_cancelacion or ''
)
case '{http://www.sat.gob.mx/esquemas/retencionpago/2}Retenciones':
# Uuid~RfcEmisor~NombreEmisor~RfcReceptor~NombreReceptor~RfcPac~FechaEmision~FechaCertificacionSat~MontoOp~MontoRet~Estatus~FechaCancelacion
return "{uuid}~{rfc_emisor}~{nombre_emisor}~{rfc_receptor}~{nombre_receptor}~{rfc_pac}~{fecha_emision}~{fecha_certificacion_sat}~{monto_op}~{monto_ret}~{estatus}~{fecha_cancelacion}".format(
uuid=self["Complemento"]["TimbreFiscalDigital"]["UUID"],
rfc_emisor=self["Emisor"]["RfcE"],
nombre_emisor=self['Emisor']['NomDenRazSocE'],
rfc_receptor=self['Receptor']['Nacional']['RfcR'],
nombre_receptor=self['Receptor']['Nacional']['NomDenRazSocR'],
rfc_pac=self["Complemento"]["TimbreFiscalDigital"]["RfcProvCertif"],
fecha_emision=self["FechaExp"],
fecha_certificacion_sat=self["Complemento"]["TimbreFiscalDigital"]["FechaTimbrado"],
monto_op=fmt_decimal(self["Totales"]["MontoTotOperacion"]),
monto_ret=fmt_decimal(self["Totales"]["MontoTotRet"]),
estatus=self.estatus,
fecha_cancelacion=self.fecha_cancelacion or ''
)
case _:
raise CFDIError("No metadata")
@property
def estatus(self) -> str:
"""
'0' = 'Cancelado'
'1' = 'Vigente'
:return:
"""
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 in ('Traslados', 'Retenciones'):
if imps := self.impuestos.get(imp):
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")
else:
self.impuestos = self.comprobante.get("Impuestos", {})
self.sub_total = self.comprobante["SubTotal"]
self.descuento = self.comprobante.get("Descuento")