Passed
Push — main ( 86ebe2...568503 )
by Sat CFDI
06:02
created

SatCFDI.saldo_pendiente()   B

Complexity

Conditions 5

Size

Total Lines 29
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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