Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
702b399
requirements: Update 'cryptography'
jtrh Dec 16, 2020
cba4233
libs.crypto_utils: Fix test broken by 'cryptography' update
jtrh Dec 16, 2020
2d9ba1e
Merge pull request #166 from fyntex/feature/update-cryptography
jtrh Dec 16, 2020
31911d5
build(deps): bump coverage from 4.5.3 to 5.3
dependabot[bot] Dec 16, 2020
a7a78e1
Merge pull request #169 from fyntex/dependabot/pip/coverage-5.3
jtrh Dec 16, 2020
694e260
rtc.data_models_cesiones_periodo: Add `CesionesPeriodoEntry`
jtrh Dec 22, 2020
0f7a3ba
Merge pull request #172 from fyntex/feature/add-rtc-cesiones-periodo-…
jtrh Dec 22, 2020
e9dc07b
requirements: Add 'pydantic'
jtrh Jan 5, 2021
1f4ec01
Merge pull request #173 from fyntex/feature/add-pydantic
jtrh Jan 5, 2021
fe58daa
libs.tz_utils: Add checks to validate_dt_tz
jtrh Jan 6, 2021
8d1e0d3
Merge pull request #175 from fyntex/feature/add-checks-to-tz-utils-va…
jtrh Jan 6, 2021
3e620ec
rtc.constants: Add 'monto cedido' and 'sequence number'
jtrh Jan 5, 2021
fbc5bb2
rtc.data_models: Add "cesión" natural key and alternative natural key
jtrh Jan 6, 2021
47775b8
rtc.data_models_cesiones_periodo: Replace hardcoded values w/constants
jtrh Jan 5, 2021
c2449db
Merge pull request #174 from fyntex/feature/add-sii-rtc-constants-and…
jtrh Jan 7, 2021
2b4eef7
build(deps): bump codecov from 2.1.9 to 2.1.11
dependabot[bot] Jan 7, 2021
07d9e4c
Merge pull request #171 from fyntex/dependabot/pip/codecov-2.1.11
jtrh Jan 7, 2021
ef30d01
HISTORY: update for new version
jtrh Jan 11, 2021
1bf1cc8
Bump version: 0.11.1 → 0.11.2
jtrh Jan 11, 2021
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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.11.1
current_version = 0.11.2
commit = True
tag = True

Expand Down
11 changes: 11 additions & 0 deletions HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
History
-------

0.11.2 (2021-01-11)
+++++++++++++++++++++++

* (PR #166, 2020-12-15) requirements: Update 'cryptography'
* (PR #169, 2020-12-16) build(deps): bump coverage from 4.5.3 to 5.3
* (PR #172, 2020-12-22) rtc: Add data model for "Cesiones Periodo" entries
* (PR #173, 2021-01-05) requirements: Add 'pydantic'
* (PR #175, 2021-01-06) libs.tz_utils: Add checks to validate_dt_tz
* (PR #174, 2021-01-07) rtc: Add constants and "cesión" natural keys
* (PR #171, 2021-01-07) build(deps): bump codecov from 2.1.9 to 2.1.11

0.11.1 (2020-12-15)
+++++++++++++++++++++++

Expand Down
2 changes: 1 addition & 1 deletion cl_sii/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"""


__version__ = '0.11.1'
__version__ = '0.11.2'
7 changes: 7 additions & 0 deletions cl_sii/libs/tz_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,5 +166,12 @@ def validate_dt_tz(value: datetime, tz: PytzTimezone) -> None:
"""
if not dt_is_aware(value):
raise ValueError("Value must be a timezone-aware datetime object.")

# The 'zone' attribute is not defined in the abstract base class 'datetime.tzinfo'. We need to
# check that it is there before using it below to prevent unexpected exceptions when dealing
# with Python Standard Library time zones that are instances of class 'datetime.timezone'.
assert hasattr(value.tzinfo, 'zone'), f"Object {value.tzinfo!r} must have 'zone' attribute."
assert hasattr(tz, 'zone'), f"Object {tz!r} must have 'zone' attribute."

if value.tzinfo.zone != tz.zone: # type: ignore
raise ValueError(f"Timezone of datetime value must be '{tz.zone!s}'.", value)
38 changes: 37 additions & 1 deletion cl_sii/rtc/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import enum
from typing import FrozenSet

from cl_sii.dte.constants import TipoDteEnum
from cl_sii.dte.constants import DTE_MONTO_TOTAL_FIELD_MAX_VALUE, TipoDteEnum


# The collection of "tipo DTE" for which it is possible to "ceder" a "DTE".
Expand All @@ -25,6 +25,42 @@
})


###############################################################################
# Cesion Fields / "Monto Cedido"
###############################################################################

# Amount of the "cesión".
#
# Ref:
# - https://github.com/fyntex/lib-cl-sii-api-python/blob/v0.4.4/cl_sii_api/rtc/data_models.py#L231
# - Document "Formato Archivo Electrónico de Cesión 2013-02-11" (retrieved on 2019-08-12)
# (https://www.sii.cl/factura_electronica/cesion.pdf)
CESION_MONTO_CEDIDO_FIELD_MIN_VALUE: int = 0
CESION_MONTO_CEDIDO_FIELD_MAX_VALUE: int = DTE_MONTO_TOTAL_FIELD_MAX_VALUE


###############################################################################
# Cesion Fields / "Secuencia"
###############################################################################

# Sequence number of the "cesión"
#
# > Campo: Número de Cesión
# > Descripción: Secuencia de la cesión
# > Tipo: NUM
# > Validación: 1 hasta 40
#
# Source:
# Document "Formato Archivo Electrónico de Cesión 2013-02-11" (retrieved on 2019-08-12)
# (https://www.sii.cl/factura_electronica/cesion.pdf)
CESION_SEQUENCE_NUMBER_MIN_VALUE: int = 1
CESION_SEQUENCE_NUMBER_MAX_VALUE: int = 40


###############################################################################
# Other
###############################################################################

@enum.unique
class RolContribuyenteEnCesion(enum.Enum):

Expand Down
237 changes: 237 additions & 0 deletions cl_sii/rtc/data_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
"""
Data models for RTC
===================

In this domain we care about the data of transactions that consist in:
a "cesión" of a DTE, by a "cedente" to a "cesionario".

Natural key of a cesion
-----------------------

Each transaction can be uniquely identified by the group of fields defined in
:class:`CesionNaturalKey`. However, because of SII's inconsistent systems
implementations, there are several information sources *where the "cesión"'s
sequence number is not available*. Thus the usefulness of that class is
limited, unlike :class:`cl_sii.dte.data_models.DteNaturalKey` for a DTE.
In some cases, the alternative natural key :class:`CesionAltNaturalKey` may
be used as a workaround when the sequence number is not available.
"""

from __future__ import annotations

import dataclasses
from datetime import datetime
from typing import ClassVar, Mapping

import pydantic

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

from . import constants


def validate_cesion_seq(value: int) -> None:
"""
Validate value for sequence number of a "cesión".

:raises ValueError:
"""
if (
value < constants.CESION_SEQUENCE_NUMBER_MIN_VALUE
or value > constants.CESION_SEQUENCE_NUMBER_MAX_VALUE
):
raise ValueError("Value is out of the valid range.", value)


def validate_cesion_dte_tipo_dte(value: TipoDteEnum) -> None:
"""
Validate "tipo DTE" of the "cesión".

:raises ValueError:
"""
if value not in constants.TIPO_DTE_CEDIBLES:
raise ValueError('Value is not "cedible".', value)


@pydantic.dataclasses.dataclass(
frozen=True,
config=type('Config', (), dict(
arbitrary_types_allowed=True,
))
)
class CesionNaturalKey:
"""
Natural key of a "cesión" of a DTE.

The class instances are immutable.

This group of fields uniquely identifies a "cesión".

Example:

>>> instance = CesionNaturalKey(
... dte_data_models.DteNaturalKey(
... Rut('60910000-1'), TipoDteEnum.FACTURA_ELECTRONICA, 2093465,
... ),
... 1,
... )
"""

###########################################################################
# Fields
###########################################################################

dte_key: dte_data_models.DteNaturalKey
"""
Natural key of the "cesión"'s DTE.
"""

seq: int
"""
Sequence number of the "cesión". Must be >= 1.
"""

@property
def slug(self) -> str:
"""
Return an slug representation (that preserves uniquess) of the instance.
"""
# Note: Based on 'cl_sii.dte.data_models.DteNaturalKey.slug'.
return f'{self.dte_key.slug}--{self.seq}'

###########################################################################
# Custom Methods
###########################################################################

def as_dict(self) -> Mapping[str, object]:
return dataclasses.asdict(self)

###########################################################################
# Validators
###########################################################################

@pydantic.validator('dte_key')
def validate_dte_tipo_dte(cls, v: object) -> object:
if isinstance(v, dte_data_models.DteNaturalKey):
validate_cesion_dte_tipo_dte(v.tipo_dte)
return v

@pydantic.validator('seq')
def validate_seq(cls, v: object) -> object:
if isinstance(v, int):
validate_cesion_seq(v)
return v


@pydantic.dataclasses.dataclass(
frozen=True,
config=type('Config', (), dict(
arbitrary_types_allowed=True,
))
)
class CesionAltNaturalKey:
"""
Alternative natural key of a "cesión" of a DTE.

Useful when the sequence number is unavailable, such as in "cesiones periodo".

The class instances are immutable.

.. warning::
It is assumed that it is impossible to "ceder" a given DTE by a given "cedente" to a given
"cesionario" more than once in a particular instant (``fecha_cesion_dt``).

Example:

>>> instance = CesionAltNaturalKey(
... dte_data_models.DteNaturalKey(
... Rut('60910000-1'), TipoDteEnum.FACTURA_ELECTRONICA, 2093465,
... ),
... Rut('76389992-6'),
... Rut('76598556-0'),
... datetime.fromisoformat('2019-04-05T12:57:32-03:00'),
... )
"""

###########################################################################
# Constants
###########################################################################

DATETIME_FIELDS_TZ: ClassVar[tz_utils.PytzTimezone] = SII_OFFICIAL_TZ

###########################################################################
# Fields
###########################################################################

dte_key: dte_data_models.DteNaturalKey
"""
Natural key of the "cesión"'s DTE.
"""

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

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

fecha_cesion_dt: datetime
"""
Date and time when the "cesión" happened.

.. warning:: The value will always be truncated to the minute, even if the
original value has seconds. This has to be done because this field is
part of a key and in some data sources the timestamp has seconds and in
others it has not (e.g. AEC and Cesión Periodo).
"""

@property
def slug(self) -> str:
"""
Return a slug representation (that preserves uniquess) of the instance.
"""
# Note: Based on 'cl_sii.dte.data_models.DteNaturalKey.slug'.

_fecha_cesion_dt = self.fecha_cesion_dt.astimezone(self.DATETIME_FIELDS_TZ)
fecha_cesion_dt: str = _fecha_cesion_dt.isoformat(timespec='minutes')

return f'{self.dte_key.slug}--{self.cedente_rut}--{self.cesionario_rut}--{fecha_cesion_dt}'

###########################################################################
# Custom Methods
###########################################################################

def as_dict(self) -> Mapping[str, object]:
return dataclasses.asdict(self)

###########################################################################
# Validators
###########################################################################

@pydantic.validator('dte_key')
def validate_dte_tipo_dte(cls, v: object) -> object:
if isinstance(v, dte_data_models.DteNaturalKey):
validate_cesion_dte_tipo_dte(v.tipo_dte)
return v

@pydantic.validator('fecha_cesion_dt')
def validate_datetime_tz(cls, v: object) -> object:
if isinstance(v, datetime):
tz_utils.validate_dt_tz(v, cls.DATETIME_FIELDS_TZ)
return v

@pydantic.validator('fecha_cesion_dt')
def truncate_fecha_cesion_dt_to_minutes(cls, v: object) -> object:
if isinstance(v, datetime):
if v.second != 0:
v = v.replace(second=0)
if v.microsecond != 0:
v = v.replace(microsecond=0)
return v
Loading