Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
273 changes: 273 additions & 0 deletions cl_sii/rtc/data_models_cesiones_periodo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
from __future__ import annotations

import dataclasses
import logging
from datetime import date, datetime
from typing import Optional

import cl_sii.dte.data_models
from cl_sii.base.constants import SII_OFFICIAL_TZ
from cl_sii.dte.constants import TipoDteEnum
from cl_sii.libs import tz_utils
from cl_sii.rut import Rut

from .constants import TIPO_DTE_CEDIBLES


logger = logging.getLogger(__name__)


@dataclasses.dataclass(frozen=True)
class CesionesPeriodoEntry:
"""
Entry of a list of "cesiones" in a period.

In case of doubts about the concepts (particularly for "vendedor" and
"deudor"), read the documentation of :mod:`cl_sii.dte.data_models`.
"""

###########################################################################
# fields of DTE
###########################################################################

# In case of doubts about the concepts (particularly for 'vendedor' and 'deudor'),
# see 'cl_sii.dte.data_models'.

dte_vendedor_rut: Rut
"""
RUT of the DTE's "vendedor".
"""

dte_deudor_rut: Rut
"""
RUT of the DTE's "deudor".
"""

dte_tipo_dte: TipoDteEnum
"""
The DTE's "tipo DTE" (sighs).
"""

dte_folio: int
"""
The sequential number of the DTE of given kind issued by 'emisor_rut'.
"""

dte_fecha_emision: date
"""
Field 'fecha_emision' of the DTE.

.. warning:: It may not match the **real date** on which the DTE was issued
or received/processed by SII.
"""

dte_monto_total: int
"""
Total amount of the DTE.
"""

###########################################################################
# fields of "cesion"
###########################################################################

cedente_rut: Rut
"""
RUT of the "cedente".
"""

cedente_razon_social: str
"""
"Razón social" (legal name) of the "cedente".
"""

cedente_email: Optional[str]
"""
Email address of the "cedente".
"""

cesionario_rut: Rut
"""
RUT of the "cesionario".
"""

cesionario_razon_social: str
"""
"Razón social" (legal name) of the "cesionario".
"""

cesionario_emails: Optional[str]
"""
Email address(es) of the "cesionario".
"""

# note: this is not a field of the DTE even though 'dte_deudor_rut' is.
deudor_email: Optional[str]
"""
Email address of the "deudor".
"""

# TODO: verify to what we are referring to exactly:
# digitally signed? received by the SII? processed by the SII?
fecha_cesion_dt: datetime
"""
Datetime on which the "cesion" happened.

Must be consistent with ``fecha_cesion`` (considering timezone).
"""

fecha_cesion: date
"""
Date on which the "cesion" happened.

Must be consistent with ``fecha_cesion_dt`` (considering timezone).
"""

# TODO: find out if there is a valid case for which it can be different from 'dte_monto_total'.
monto_cedido: int
"""
Amount of the "cesion" ("monto del crédito cedido").
"""

fecha_ultimo_vencimiento: date
"""
"Fecha del último vencimiento del pago".

Even though the DTE field ``fecha_vencimiento`` is optional, this field
of the "cesión" is mandatory.
"""

estado: str
"""
"Estado" of the "cesion".
"""

def __post_init__(self) -> None:
"""
Run validation automatically after setting the fields values.

:raises TypeError, ValueError:

"""
#######################################################################
# fields of DTE
#######################################################################

if not isinstance(self.dte_vendedor_rut, Rut):
raise TypeError("Inappropriate type of 'dte_vendedor_rut'.")

if not isinstance(self.dte_deudor_rut, Rut):
raise TypeError("Inappropriate type of 'dte_deudor_rut'.")

if not isinstance(self.dte_tipo_dte, TipoDteEnum):
raise TypeError("Inappropriate type of 'dte_tipo_dte'.")
if self.dte_tipo_dte not in TIPO_DTE_CEDIBLES:
raise ValueError(
"The \"tipo DTE\" in 'dte_tipo_dte' is not \"cedible\".",
self.dte_tipo_dte)

if not isinstance(self.dte_folio, int):
raise TypeError("Inappropriate type of 'dte_folio'.")
if not self.dte_folio > 0:
raise ValueError("Inappropriate value of 'dte_folio'.")

if not isinstance(self.dte_fecha_emision, date):
raise TypeError("Inappropriate type of 'dte_fecha_emision'.")

# TODO: figure out validation rules of 'dte_monto_total'
if not isinstance(self.dte_monto_total, int):
raise TypeError("Inappropriate type of 'dte_monto_total'.")

#######################################################################
# fields of "cesion"
#######################################################################

if not isinstance(self.cedente_rut, Rut):
raise TypeError("Inappropriate type of 'cedente_rut'.")

if not isinstance(self.cedente_razon_social, str):
raise TypeError("Inappropriate type of 'cedente_razon_social'.")
cl_sii.dte.data_models.validate_contribuyente_razon_social(self.cedente_razon_social)

if self.cedente_email is not None:
if not isinstance(self.cedente_email, str):
raise TypeError("Inappropriate type of 'cedente_email'.")
cl_sii.dte.data_models.validate_clean_str(self.cedente_email)
cl_sii.dte.data_models.validate_non_empty_str(self.cedente_email)

if not isinstance(self.cesionario_rut, Rut):
raise TypeError("Inappropriate type of 'cesionario_rut'.")

if not isinstance(self.cesionario_razon_social, str):
raise TypeError("Inappropriate type of 'cesionario_razon_social'.")
cl_sii.dte.data_models.validate_contribuyente_razon_social(self.cesionario_razon_social)

if self.cesionario_emails is not None:
if not isinstance(self.cesionario_emails, str):
raise TypeError("Inappropriate type of 'cesionario_emails'.")
cl_sii.dte.data_models.validate_clean_str(self.cesionario_emails)
cl_sii.dte.data_models.validate_non_empty_str(self.cesionario_emails)

if self.deudor_email is not None:
if not isinstance(self.deudor_email, str):
raise TypeError("Inappropriate type of 'deudor_email'.")
cl_sii.dte.data_models.validate_clean_str(self.deudor_email)
cl_sii.dte.data_models.validate_non_empty_str(self.deudor_email)

if not isinstance(self.fecha_cesion_dt, datetime):
raise TypeError("Inappropriate type of 'fecha_cesion_dt'.")
tz_utils.validate_dt_tz(self.fecha_cesion_dt, SII_OFFICIAL_TZ)

if not isinstance(self.fecha_cesion, date):
raise TypeError("Inappropriate type of 'fecha_cesion'.")
if self.fecha_cesion_dt.date() != self.fecha_cesion:
raise ValueError(
"Date of 'fecha_cesion_dt' (considering timezone) does not match 'fecha_cesion'.",
self.fecha_cesion_dt, self.fecha_cesion)

if not isinstance(self.monto_cedido, int):
raise TypeError("Inappropriate type of 'monto_cedido'.")
if not self.monto_cedido >= 0:
raise ValueError(
"Amount 'monto_cedido' must be >= 0.", self.monto_cedido)
if not self.monto_cedido <= self.dte_monto_total:
raise ValueError(
"Amount 'monto_cedido' must be <= 'dte_monto_total'.",
self.monto_cedido, self.dte_monto_total)

if not isinstance(self.fecha_ultimo_vencimiento, date):
raise TypeError("Inappropriate type of 'fecha_ultimo_vencimiento'.")

if not isinstance(self.estado, str):
raise TypeError("Inappropriate type of 'estado'.")
cl_sii.dte.data_models.validate_clean_str(self.estado)
cl_sii.dte.data_models.validate_non_empty_str(self.estado)

@property
def monto_cedido_eq_dte_monto_total(self) -> bool:
return self.monto_cedido == self.dte_monto_total

def as_dte_data_l1(self) -> cl_sii.dte.data_models.DteDataL1:
if self.dte_tipo_dte.emisor_is_vendedor:
dte_emisor_rut = self.dte_vendedor_rut
dte_receptor_rut = self.dte_deudor_rut
elif self.dte_tipo_dte.receptor_is_vendedor:
dte_emisor_rut = self.dte_deudor_rut
dte_receptor_rut = self.dte_vendedor_rut
else:
raise ValueError(
'Programming error: the "vendedor" is neither the "emisor" nor the "vendedor".',
self)

try:
dte_data = cl_sii.dte.data_models.DteDataL1(
emisor_rut=dte_emisor_rut,
tipo_dte=self.dte_tipo_dte,
folio=self.dte_folio,
fecha_emision_date=self.dte_fecha_emision,
receptor_rut=dte_receptor_rut,
monto_total=self.dte_monto_total,
)
except (TypeError, ValueError):
raise

return dte_data
117 changes: 117 additions & 0 deletions tests/test_rtc_data_models_cesiones_periodo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import annotations

import unittest
from datetime import date, datetime

import cl_sii.dte.data_models
from cl_sii.base.constants import SII_OFFICIAL_TZ
from cl_sii.dte.constants import TipoDteEnum
from cl_sii.libs.tz_utils import convert_naive_dt_to_tz_aware
from cl_sii.rtc.data_models_cesiones_periodo import CesionesPeriodoEntry
from cl_sii.rut import Rut


class CesionesPeriodoEntryTest(unittest.TestCase):
def setUp(self) -> None:
super().setUp()

self.valid_kwargs = dict(
dte_vendedor_rut=Rut('51532520-4'),
dte_deudor_rut=Rut('75320502-0'),
dte_tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
dte_folio=3608460,
dte_fecha_emision=date(2019, 2, 11),
dte_monto_total=256357,
cedente_rut=Rut('51532520-4'),
cedente_razon_social='MI CAMPITO SA',
cedente_email='mi@campito.cl',
cesionario_rut=Rut('96667560-8'),
cesionario_razon_social='POBRES SERVICIOS FINANCIEROS S.A.',
cesionario_emails='un-poco@pobres.cl,super.ejecutivo@pobres.cl',
deudor_email=None,
fecha_cesion_dt=convert_naive_dt_to_tz_aware(
datetime(2019, 3, 7, 13, 32), tz=SII_OFFICIAL_TZ),
fecha_cesion=date(2019, 3, 7),
monto_cedido=256357,
fecha_ultimo_vencimiento=date(2019, 4, 12),
estado='Cesion Vigente',
)

def test_init_ok_1(self) -> None:
obj = CesionesPeriodoEntry(**self.valid_kwargs)
self.assertTrue(obj.monto_cedido_eq_dte_monto_total)

def test_init_ok_2(self) -> None:
self.valid_kwargs.update(dict(
monto_cedido=self.valid_kwargs['dte_monto_total'] - 1,
))
obj = CesionesPeriodoEntry(**self.valid_kwargs)
self.assertFalse(obj.monto_cedido_eq_dte_monto_total)

def test_init_error_monto_cedido_1(self) -> None:
self.valid_kwargs.update(dict(
monto_cedido=-1,
))
with self.assertRaises(ValueError) as cm:
CesionesPeriodoEntry(**self.valid_kwargs)
self.assertEqual(
cm.exception.args,
("Amount 'monto_cedido' must be >= 0.", -1))

def test_init_error_monto_cedido_2(self) -> None:
self.valid_kwargs.update(dict(
monto_cedido=self.valid_kwargs['dte_monto_total'] + 1,
))
with self.assertRaises(ValueError) as cm:
CesionesPeriodoEntry(**self.valid_kwargs)
self.assertEqual(
cm.exception.args,
("Amount 'monto_cedido' must be <= 'dte_monto_total'.", 256358, 256357))

def test_init_error_dte_tipo_dte_1(self) -> None:
self.valid_kwargs.update(dict(
dte_tipo_dte=TipoDteEnum.NOTA_CREDITO_ELECTRONICA,
))
with self.assertRaises(ValueError) as cm:
CesionesPeriodoEntry(**self.valid_kwargs)
self.assertEqual(
cm.exception.args,
("The \"tipo DTE\" in 'dte_tipo_dte' is not \"cedible\".",
TipoDteEnum.NOTA_CREDITO_ELECTRONICA))

def test_as_dte_data_l1_ok_1(self) -> None:
obj = CesionesPeriodoEntry(**self.valid_kwargs)
dte_obj = cl_sii.dte.data_models.DteDataL1(
emisor_rut=Rut('51532520-4'),
tipo_dte=TipoDteEnum.FACTURA_ELECTRONICA,
folio=3608460,
receptor_rut=Rut('75320502-0'),
fecha_emision_date=date(2019, 2, 11),
monto_total=256357,
)

self.assertEqual(obj.as_dte_data_l1(), dte_obj)
self.assertEqual(dte_obj.emisor_rut, obj.dte_vendedor_rut)
self.assertEqual(dte_obj.vendedor_rut, obj.dte_vendedor_rut)
self.assertEqual(dte_obj.receptor_rut, obj.dte_deudor_rut)
self.assertEqual(dte_obj.comprador_rut, obj.dte_deudor_rut)

def test_as_dte_data_l1_ok_2(self) -> None:
self.valid_kwargs.update(dict(
dte_tipo_dte=TipoDteEnum.FACTURA_COMPRA_ELECTRONICA,
))
obj = CesionesPeriodoEntry(**self.valid_kwargs)
dte_obj = cl_sii.dte.data_models.DteDataL1(
emisor_rut=Rut('75320502-0'),
tipo_dte=TipoDteEnum.FACTURA_COMPRA_ELECTRONICA,
folio=3608460,
receptor_rut=Rut('51532520-4'),
fecha_emision_date=date(2019, 2, 11),
monto_total=256357,
)

self.assertEqual(obj.as_dte_data_l1(), dte_obj)
self.assertEqual(dte_obj.receptor_rut, obj.dte_vendedor_rut)
self.assertEqual(dte_obj.vendedor_rut, obj.dte_vendedor_rut)
self.assertEqual(dte_obj.emisor_rut, obj.dte_deudor_rut)
self.assertEqual(dte_obj.comprador_rut, obj.dte_deudor_rut)