Passed
Push — main ( 44031f...fd93eb )
by Sat CFDI
05:07
created

satcfdi.accounting.models.SatCFDI.name()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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