satcfdi.accounting.models.SatCFDI.__new__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 3
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1 1
from dataclasses import dataclass
2 1
from datetime import datetime
3 1
from decimal import Decimal
4 1
from uuid import UUID
5
6 1
from satcfdi.utils import iterate
7
8 1
from ..create.catalogos import EstadoComprobante
9 1
from ..create.cfd.catalogos import MetodoPago, TipoDeComprobante, TipoRelacion
10 1
from ..create.compute import make_impuestos_dr_parcial, rounder, group_impuestos, encode_impuesto, calculate_partial
11 1
from ..xelement import XElement
12 1
from ..cfdi import CFDI
13
14
15 1
class SatCFDI(CFDI):
16
    """
17
    SatCFDI is an extension of a CFDI to represent a CFDI that has been sent to SAT
18
    """
19
20 1
    def __new__(cls, *args, **kwargs):
21 1
        return super().__new__(cls, *args, **kwargs)
22
23 1
    def __init__(self, *args, **kwargs):
24 1
        super().__init__(*args, **kwargs)
25 1
        self.relations = []  # type: list[Relation]
26 1
        self.payments = []  # type: list[Payment]
27
28 1
    @property
29 1
    def uuid(self):
30 1
        return UUID(self["Complemento"]["TimbreFiscalDigital"]["UUID"])
31
32 1
    @property
33 1
    def name(self):
34 1
        return self.get("Serie", "") + self.get("Folio", "")
35
36 1
    def saldo_pendiente(self, date=None) -> Decimal | None:
37 1
        if self["TipoDeComprobante"] == TipoDeComprobante.INGRESO:
38
            # Nota de crédito de los documentos relacionados
39 1
            credit_notes = sum(
40
                c.comprobante["Total"]
41
                for c in self.relations
42
                if c.cfdi_relacionados["TipoRelacion"] == TipoRelacion.NOTA_DE_CREDITO_DE_LOS_DOCUMENTOS_RELACIONADOS
43
                and c.comprobante['TipoDeComprobante'] == TipoDeComprobante.EGRESO
44
                and c.comprobante.estatus() == EstadoComprobante.VIGENTE
45
                and (not date or c.comprobante['Fecha'] <= date)
46
            )
47 1
            insoluto = min(
48
                (c.docto_relacionado['ImpSaldoInsoluto']
49
                 for c in self.payments
50
                 if c.comprobante.estatus() == EstadoComprobante.VIGENTE
51
                 and (not date or c.pago['FechaPago'] <= date)
52
                 ),
53
                default=None
54
            )
55 1
            if insoluto is not None:
56 1
                return insoluto - credit_notes
57
58 1
            insoluto = self["Total"] - credit_notes
59 1
            if self["MetodoPago"] == MetodoPago.PAGO_EN_PARCIALIDADES_O_DIFERIDO:
60 1
                return insoluto
61 1
            if self["MetodoPago"] == MetodoPago.PAGO_EN_UNA_SOLA_EXHIBICION:
62 1
                return Decimal(0)
63
64 1
        return None
65
66 1
    @property
67 1
    def ultima_num_parcialidad(self) -> int:
68 1
        return max((c.docto_relacionado['NumParcialidad'] for c in self.payments if c.comprobante.estatus() == EstadoComprobante.VIGENTE), default=0)
69
70 1
    def consulta_estado(self) -> dict:
71
        raise NotImplementedError()
72
73 1
    def estatus(self) -> EstadoComprobante:
74
        raise NotImplementedError()
75
76 1
    @property
77 1
    def fecha_cancelacion(self) -> datetime | None:
78
        raise NotImplementedError()
79
80 1
    def cfdi_relacionados(self, tipo_relacion: TipoRelacion = None):
81
        for cfdi_rel in iterate(self.get("CfdiRelacionados")):
82
            if cfdi_rel["TipoRelacion"] == tipo_relacion:
83
                for uuid in cfdi_rel["CfdiRelacionado"]:
84
                    yield UUID(uuid)
85
86
87 1
@dataclass(slots=True, init=True)
88 1
class Relation:
89 1
    cfdi_relacionados: XElement
90 1
    comprobante: SatCFDI
91
92
93 1
@dataclass(slots=True, init=True)
94 1
class Payment:
95 1
    comprobante: SatCFDI
96 1
    pago: XElement = None
97 1
    docto_relacionado: XElement = None
98
99
100 1
@dataclass
101 1
class PaymentsDetails(Payment):
102 1
    comprobante_pagado: SatCFDI = None
103
104 1
    def __post_init__(self):
105 1
        if self.pago:
106
            self.impuestos = self.docto_relacionado.get('ImpuestosDR')
107
            if self.impuestos is None and self.comprobante['Version'] == '3.3':
108
                self.impuestos = make_impuestos_dr_parcial(
109
                    conceptos=self.comprobante_pagado['Conceptos'],
110
                    imp_saldo_ant=self.docto_relacionado['ImpSaldoAnt'],
111
                    imp_pagado=self.docto_relacionado['ImpPagado'],
112
                    total=self.comprobante_pagado['Total'],
113
                    rnd_fn=rounder(self.comprobante_pagado['Moneda'])
114
                )
115
116
            self.impuestos = group_impuestos([{
117
                "ImpuestosDR": self.impuestos
118
            }], pfx="DR", ofx="")
119
120
            for imp, imps in self.impuestos.items():
121
                self.impuestos[imp] = {
122
                    encode_impuesto(
123
                        impuesto=v['Impuesto'],
124
                        tipo_factor=v.get("TipoFactor"),
125
                        tasa_cuota=v.get('TasaOCuota')
126
                    ): v
127
                    for v in imps
128
                }
129
130
            # def calc_parcial(field):
131
            #     return calculate_partial(
132
            #         value=self.comprobante_pagado.get(field),
133
            #         imp_saldo_ant=self.docto_relacionado['ImpSaldoAnt'],
134
            #         imp_pagado=self.docto_relacionado["ImpPagado"],
135
            #         total=self.comprobante_pagado["Total"],
136
            #         rnd_fn=rounder(self.comprobante_pagado['Moneda'])
137
            #     )
138
139
            # self.sub_total = calc_parcial("SubTotal")
140
            # self.descuento = calc_parcial("Descuento")
141
            self.total = self.docto_relacionado["ImpPagado"]
142
            self.sub_total_desc = self.docto_relacionado["ImpPagado"]
143
            for t in self.impuestos.get('Traslados', {}).values():
144
                self.sub_total_desc -= t['Importe']
145
            for t in self.impuestos.get('Retenciones', {}).values():
146
                self.sub_total_desc += t['Importe']
147
148
        else:
149 1
            self.impuestos = self.comprobante.get("Impuestos", {})
150
            # self.sub_total = self.comprobante["SubTotal"]
151
            # self.descuento = self.comprobante.get("Descuento")
152 1
            self.total = self.comprobante["Total"]
153 1
            self.sub_total_desc = self.comprobante["SubTotal"] - self.comprobante.get("Descuento", 0)
154
155