From 4e366c85fcc6eab2625d27923ce407163ed5c441 Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 6 Sep 2022 10:28:48 -0300 Subject: [PATCH 01/59] set enviroment and ignore files --- .gitignore | 7 +++++++ django_afip/__init__.py | 7 +++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0105e6ed..16eb853e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,10 @@ django_afip/version.py testapp/test_ticket.yaml test.csr test2.csr + +#Branch files +/afi +Pipfile +Pipfile.lock +manage.py +django_afip/__init__.py diff --git a/django_afip/__init__.py b/django_afip/__init__.py index 54657ca5..6ce7e18e 100644 --- a/django_afip/__init__.py +++ b/django_afip/__init__.py @@ -1,5 +1,8 @@ -from . import version # type: ignore +try: + from . import version # type: ignore -__version__ = version.version + __version__ = version.version +except: + pass default_app_config = "django_afip.apps.AfipConfig" From fdf20c0a8fee9a7eb05d4cdbc330ab001c087d4d Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 6 Sep 2022 12:39:24 -0300 Subject: [PATCH 02/59] Caea model created --- .gitignore | 1 + django_afip/admin.py | 1 + django_afip/migrations/0011_caea.py | 31 ++++++++ .../migrations/0012_alter_caea_caea_code.py | 19 +++++ .../migrations/0013_alter_caea_caea_code.py | 19 +++++ django_afip/models.py | 75 +++++++++++++++++++ 6 files changed, 146 insertions(+) create mode 100644 django_afip/migrations/0011_caea.py create mode 100644 django_afip/migrations/0012_alter_caea_caea_code.py create mode 100644 django_afip/migrations/0013_alter_caea_caea_code.py diff --git a/.gitignore b/.gitignore index 16eb853e..ed38f834 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ test2.csr #Branch files /afi +/afip Pipfile Pipfile.lock manage.py diff --git a/django_afip/admin.py b/django_afip/admin.py index d427f6f6..24d2fa4d 100644 --- a/django_afip/admin.py +++ b/django_afip/admin.py @@ -498,3 +498,4 @@ def successful(self, obj): admin.site.register(models.VatType) admin.site.register(models.TaxType) admin.site.register(models.Observation) +admin.site.register(models.Caea) diff --git a/django_afip/migrations/0011_caea.py b/django_afip/migrations/0011_caea.py new file mode 100644 index 00000000..681767f3 --- /dev/null +++ b/django_afip/migrations/0011_caea.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.1 on 2022-09-06 15:02 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0010_alter_authticket_service'), + ] + + operations = [ + migrations.CreateModel( + name='Caea', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('caea_code', models.BigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}')])), + ('period', models.IntegerField(help_text='Period to send in the CAEA request (yyyymm)')), + ('order', models.IntegerField(choices=[(1, '1'), (2, '2')], help_text='Month is divided in 1st quarter or 2nd quarter')), + ('valid_since', models.DateTimeField(verbose_name='valid_to')), + ('expires', models.DateTimeField(verbose_name='expires')), + ('generated', models.DateTimeField(verbose_name='generated')), + ('final_date_inform', models.DateTimeField(verbose_name='final_date_inform')), + ('service', models.CharField(help_text='Service for which this ticket has been authorized.', max_length=34, verbose_name='service')), + ('active', models.BooleanField(default=False)), + ('taxpayer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='caea_tickets', to='afip.taxpayer', verbose_name='taxpayer')), + ], + ), + ] diff --git a/django_afip/migrations/0012_alter_caea_caea_code.py b/django_afip/migrations/0012_alter_caea_caea_code.py new file mode 100644 index 00000000..afaac32b --- /dev/null +++ b/django_afip/migrations/0012_alter_caea_caea_code.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.1 on 2022-09-06 15:15 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0011_caea'), + ] + + operations = [ + migrations.AlterField( + model_name='caea', + name='caea_code', + field=models.BigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}'), django.core.validators.MinLengthValidator(14), django.core.validators.MaxLengthValidator(14)]), + ), + ] diff --git a/django_afip/migrations/0013_alter_caea_caea_code.py b/django_afip/migrations/0013_alter_caea_caea_code.py new file mode 100644 index 00000000..328c5bbe --- /dev/null +++ b/django_afip/migrations/0013_alter_caea_caea_code.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.1 on 2022-09-06 15:24 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0012_alter_caea_caea_code'), + ] + + operations = [ + migrations.AlterField( + model_name='caea', + name='caea_code', + field=models.PositiveBigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}'), django.core.validators.MaxValueValidator(99999999999999)]), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 95769652..4f3317f0 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -26,6 +26,7 @@ from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django_renderpdf.helpers import render_pdf +from django.core.validators import MaxValueValidator, RegexValidator from lxml import etree from lxml.builder import E from OpenSSL.crypto import FILETYPE_PEM @@ -1680,3 +1681,77 @@ def __repr__(self) -> str: class Meta: verbose_name = _("receipt validation") verbose_name_plural = _("receipt validations") + +class Caea(models.Model): + """ Represents a CAEA code to continue operating when AFIP is offline. + + The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached to the Queryset o the TaxPayer model as appropiate. + + Save() was overraided to ensure that once a CAEA was created the CAEA_code cannot be changed. + + """ + + __original_CAEA = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_CAEA = self.caea_code + + caea_code = models.PositiveBigIntegerField( + validators=[RegexValidator(regex="[0-9]{14}"),MaxValueValidator(99999999999999)], + help_text=_("CAEA code to operate offline AFIP"), + unique=True, + ) + + period = models.IntegerField( + help_text=_('Period to send in the CAEA request (yyyymm)') + ) + + order = models.IntegerField(choices=[(1,'1'),(2,'2')], + help_text=_('Month is divided in 1st quarter or 2nd quarter') + ) + + valid_since = models.DateTimeField( + _("valid_to"), + ) + expires = models.DateTimeField( + _("expires"), + ) + + generated = models.DateTimeField( + _("generated"), + ) + final_date_inform = models.DateTimeField( + _("final_date_inform"), + ) + + taxpayer = models.ForeignKey( + TaxPayer, + verbose_name=_("taxpayer"), + related_name="caea_tickets", + on_delete=models.CASCADE, + ) + + service = models.CharField( + _("service"), + max_length=34, + help_text=_("Service for which this ticket has been authorized."), + ) + + active = models.BooleanField(default=False) + + def __str__(self) -> str: + return str(self.caea_code) + + + def save(self, force_insert=False, force_update=False, *args, **kwargs): + if self.caea_code != '': + if self.__original_CAEA == None: #prevent to create a CAEA without code + self.generated = default_generated() + super().save(force_insert, force_update, *args, **kwargs) + self.__original_CAEA = self.caea_code + else: + if self.caea_code == self.__original_CAEA: #Allow modify the data only if the CAEA deosn't change. + super().save(force_insert, force_update, *args, **kwargs) + + From aff409678d449411e028fa7b2b68c39270b2a54d Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 6 Sep 2022 19:54:00 -0300 Subject: [PATCH 03/59] added get_caea and consult_caea methods to TaxPayer --- .../migrations/0014_remove_caea_service.py | 17 ++++ ...s_alter_caea_final_date_inform_and_more.py | 28 ++++++ django_afip/models.py | 87 +++++++++++++++++-- django_afip/serializers.py | 15 ++++ 4 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 django_afip/migrations/0014_remove_caea_service.py create mode 100644 django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py diff --git a/django_afip/migrations/0014_remove_caea_service.py b/django_afip/migrations/0014_remove_caea_service.py new file mode 100644 index 00000000..38e1b9ed --- /dev/null +++ b/django_afip/migrations/0014_remove_caea_service.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.1 on 2022-09-06 21:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0013_alter_caea_caea_code'), + ] + + operations = [ + migrations.RemoveField( + model_name='caea', + name='service', + ), + ] diff --git a/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py new file mode 100644 index 00000000..aa5bcf94 --- /dev/null +++ b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.1 on 2022-09-06 21:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0014_remove_caea_service'), + ] + + operations = [ + migrations.AlterField( + model_name='caea', + name='expires', + field=models.DateField(verbose_name='expires'), + ), + migrations.AlterField( + model_name='caea', + name='final_date_inform', + field=models.DateField(verbose_name='final_date_inform'), + ), + migrations.AlterField( + model_name='caea', + name='valid_since', + field=models.DateField(verbose_name='valid_to'), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 4f3317f0..59600bf6 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -115,6 +115,16 @@ def _get_storage_from_settings(setting_name: str) -> Storage: return import_string(settings.DEFAULT_FILE_STORAGE)() return import_string(path) +def caea_is_active(valid_since, valid_to) -> bool: + valid_since = parsers.parse_date(valid_since) + valid_to = parsers.parse_date(valid_to) + today = datetime.now().date() + + if valid_since <= today <= valid_to: + return True + else: + return False + class GenericAfipTypeManager(models.Manager): """Default Manager for GenericAfipType.""" @@ -536,6 +546,71 @@ def fetch_points_of_sales(self, ticket: AuthTicket = None) -> list[PointOfSales] ) return results + + def get_caea(self,period:int=None , order:int = None, ticket: AuthTicket = None,) -> Caea: + """ + Get a CAEA code for the TaxPayer, if a CAEA alredy exists the check_response will raise and exception + + Returns a caea object. + """ + ticket = ticket or self.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", self.is_sandboxed) + + response = client.service.FECAEASolicitar( + serializers.serialize_ticket(ticket), + Periodo = serializers.serialize_caea_period(period), + Orden = serializers.serialize_caea_order(order), + ) + check_response(response) #be aware that this func raise an error if it's present + + caea = Caea.objects.create( + caea_code = int(response.ResultGet.CAEA), + period = int(response.ResultGet.Periodo), + order = int(response.ResultGet.Orden), + valid_since = parsers.parse_date(response.ResultGet.FchVigDesde), + expires = parsers.parse_date(response.ResultGet.FchVigHasta), + generated = parsers.parse_datetime(response.ResultGet.FchProceso), + final_date_inform = parsers.parse_date(response.ResultGet.FchTopeInf), + taxpayer = self, + active = caea_is_active(valid_since=response.ResultGet.FchVigDesde, valid_to=response.ResultGet.FchVigHasta), + ) + return caea + + def consult_caea(self,period: str = None, order:int = None, ticket: AuthTicket = None,) -> Caea: + """ + Consult the CAEA code given by AFIP, if for some reason the code it's not saved in the db it will be created + + Returns a CAEA model. + """ + ticket = ticket or self.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", self.is_sandboxed) + response = client.service.FECAEAConsultar( + serializers.serialize_ticket(ticket), + Periodo = serializers.serialize_caea_period(period), + Orden = serializers.serialize_caea_order(order), + ) + + check_response(response) #be aware that this func raise an error if it's present + update = { + 'caea_code': int(response.ResultGet.CAEA), + 'period' : int(response.ResultGet.Periodo), + 'order' : int(response.ResultGet.Orden), + 'valid_since' : parsers.parse_date(response.ResultGet.FchVigDesde), + 'expires' : parsers.parse_date(response.ResultGet.FchVigHasta), + 'generated' : parsers.parse_datetime(response.ResultGet.FchProceso), + 'final_date_inform' : parsers.parse_date(response.ResultGet.FchTopeInf), + 'taxpayer' : self, + 'active' : caea_is_active(valid_since=response.ResultGet.FchVigDesde, valid_to=response.ResultGet.FchVigHasta), + } + + caea = Caea.objects.update_or_create( + caea_code = int(response.ResultGet.CAEA), + defaults=update) + + + return caea def __repr__(self) -> str: return "".format( @@ -1711,17 +1786,17 @@ def __init__(self, *args, **kwargs): help_text=_('Month is divided in 1st quarter or 2nd quarter') ) - valid_since = models.DateTimeField( + valid_since = models.DateField( _("valid_to"), ) - expires = models.DateTimeField( + expires = models.DateField( _("expires"), ) generated = models.DateTimeField( _("generated"), ) - final_date_inform = models.DateTimeField( + final_date_inform = models.DateField( _("final_date_inform"), ) @@ -1732,12 +1807,6 @@ def __init__(self, *args, **kwargs): on_delete=models.CASCADE, ) - service = models.CharField( - _("service"), - max_length=34, - help_text=_("Service for which this ticket has been authorized."), - ) - active = models.BooleanField(default=False) def __str__(self) -> str: diff --git a/django_afip/serializers.py b/django_afip/serializers.py index ee1250a9..3e1b5e04 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -1,3 +1,4 @@ +from datetime import datetime from django.utils.functional import LazyObject from django_afip.clients import get_client @@ -125,3 +126,17 @@ def serialize_receipt_data(receipt_type, receipt_number, point_of_sales): return f.FECompConsultaReq( CbteTipo=receipt_type, CbteNro=receipt_number, PtoVta=point_of_sales ) + +def serialize_caea_period(period:str = None): + if period: + return period + else: + date = datetime.now() + return date.strftime("%Y%m") + +def serialize_caea_order(order:int = None): + if order: + return order + else: + order = 1 + return order \ No newline at end of file From ce686b5be0a6d38d7c14b813f06ee70554c95b0a Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 7 Sep 2022 10:01:58 -0300 Subject: [PATCH 04/59] Added field generetad to Receipt to ensure compability with CAEA --- .../migrations/0016_receipt_generated.py | 20 +++++++++++++++++++ django_afip/models.py | 5 +++++ 2 files changed, 25 insertions(+) create mode 100644 django_afip/migrations/0016_receipt_generated.py diff --git a/django_afip/migrations/0016_receipt_generated.py b/django_afip/migrations/0016_receipt_generated.py new file mode 100644 index 00000000..02e19ad9 --- /dev/null +++ b/django_afip/migrations/0016_receipt_generated.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.1 on 2022-09-07 13:01 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0015_alter_caea_expires_alter_caea_final_date_inform_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='receipt', + name='generated', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Time when the receipt was created'), + preserve_default=False, + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 59600bf6..7eec5d88 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1238,6 +1238,11 @@ class Receipt(models.Model): blank=True, ) + generated = models.DateTimeField( + _('Time when the receipt was created'), + auto_now_add=True, + ) + objects = ReceiptManager() # TODO: Not implemented: optionals From 2af368f08262ebc6ca36e2488491c4d91f25a515 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 8 Sep 2022 12:02:00 -0300 Subject: [PATCH 05/59] Modified _validate method to add CAEA support, added serializers and an exepcion, added caea(boolen) field to ReceiptValidation --- django_afip/exceptions.py | 3 + django_afip/models.py | 127 +++++++++++++++++++++++++++---------- django_afip/serializers.py | 82 +++++++++++++++++++++++- 3 files changed, 174 insertions(+), 38 deletions(-) diff --git a/django_afip/exceptions.py b/django_afip/exceptions.py index 349ea926..bb895764 100644 --- a/django_afip/exceptions.py +++ b/django_afip/exceptions.py @@ -63,3 +63,6 @@ class CannotValidateTogether(DjangoAfipException): class ValidationError(DjangoAfipException): """Raised when a single Receipt failed to validate with AFIP's WS.""" + +class CaeaCountError(DjangoAfipException): + """Raised when query the caea to obtain the number but 0 or 2 or more.""" diff --git a/django_afip/models.py b/django_afip/models.py index 7eec5d88..0c3f4723 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -970,6 +970,7 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: # Skip any already-validated ones: qs = self.filter(validation__isnull=True).check_groupable() if qs.count() == 0: + print('paso por aca') return [] qs.order_by("issued_date", "id")._assign_numbers() @@ -978,46 +979,95 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: def _validate(self, ticket=None) -> list[str]: first = self.first() ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") + type_point = first.point_of_sales.issuance_type client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - response = client.service.FECAESolicitar( - serializers.serialize_ticket(ticket), - serializers.serialize_multiple_receipts(self), - ) - check_response(response) - errs = [] - for cae_data in response.FeDetResp.FECAEDetResponse: - if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: - validation = ReceiptValidation.objects.create( - result=cae_data.Resultado, - cae=cae_data.CAE, - cae_expiration=parsers.parse_date(cae_data.CAEFchVto), - receipt=self.get( - receipt_number=cae_data.CbteDesde, - ), - processed_date=parsers.parse_datetime( - response.FeCabResp.FchProceso, - ), - ) - if cae_data.Observaciones: + print('\n',type_point,'\n') + + if first.point_of_sales.issuance_type == 'CAE': + + response = client.service.FECAESolicitar( + serializers.serialize_ticket(ticket), + serializers.serialize_multiple_receipts(self), + ) + check_response(response) + errs = [] + for cae_data in response.FeDetResp.FECAEDetResponse: + if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: + validation = ReceiptValidation.objects.create( + result=cae_data.Resultado, + cae=cae_data.CAE, + cae_expiration=parsers.parse_date(cae_data.CAEFchVto), + receipt=self.get( + receipt_number=cae_data.CbteDesde, + ), + processed_date=parsers.parse_datetime( + response.FeCabResp.FchProceso, + ), + ) + if cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + observation = Observation.objects.create( + code=obs.Code, + message=obs.Msg, + ) + validation.observations.add(observation) + elif cae_data.Observaciones: for obs in cae_data.Observaciones.Obs: - observation = Observation.objects.create( - code=obs.Code, - message=obs.Msg, - ) - validation.observations.add(observation) - elif cae_data.Observaciones: - for obs in cae_data.Observaciones.Obs: - errs.append( - "Error {}: {}".format( - obs.Code, - parsers.parse_string(obs.Msg), + errs.append( + "Error {}: {}".format( + obs.Code, + parsers.parse_string(obs.Msg), + ) ) - ) - # Remove the number from ones that failed to validate: - self.filter(validation__isnull=True).update(receipt_number=None) + # Remove the number from ones that failed to validate: + self.filter(validation__isnull=True).update(receipt_number=None) + return errs + else: + valor = True + if valor: #Eliminar esto + print('nothing to validate yet') + return None + else: + response = client.service.FECAEARegInformativo( + serializers.serialize_ticket(ticket), + serializers.serialize_multiple_receipts_caea(self), + ) + check_response(response) + for cae_data in response.FeDetResp.FECAEADetResponse: + if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: + validation = ReceiptValidation.objects.create( + result=cae_data.Resultado, + cae=cae_data.CAEA, + #cae_expiration=parsers.parse_date(cae_data.CAEFchVto), + receipt=self.get( + receipt_number=cae_data.CbteDesde, + ), + processed_date=parsers.parse_datetime( + response.FeCabResp.FchProceso, + ), + caea = True, + ) + if cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + observation = Observation.objects.create( + code=obs.Code, + message=obs.Msg, + ) + validation.observations.add(observation) + elif cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + errs.append( + "Error {}: {}".format( + obs.Code, + parsers.parse_string(obs.Msg), + ) + ) + + # Remove the number from ones that failed to validate: + self.filter(validation__isnull=True).update(receipt_number=None) + return errs - return errs class ReceiptManager(models.Manager): @@ -1744,6 +1794,12 @@ class ReceiptValidation(models.Model): on_delete=models.PROTECT, ) + caea = models.BooleanField( + default=False, + help_text=_('Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number'), + verbose_name=_("is_caea") + ) + def __str__(self) -> str: return _("Validation for %s. Result: %s") % ( self.receipt, @@ -1827,5 +1883,6 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): else: if self.caea_code == self.__original_CAEA: #Allow modify the data only if the CAEA deosn't change. super().save(force_insert, force_update, *args, **kwargs) + diff --git a/django_afip/serializers.py b/django_afip/serializers.py index 3e1b5e04..f18617e7 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -2,6 +2,8 @@ from django.utils.functional import LazyObject from django_afip.clients import get_client +from .models import Caea +from .exceptions import CaeaCountError class _LazyFactory(LazyObject): @@ -37,11 +39,79 @@ def serialize_ticket(ticket): Cuit=ticket.owner.cuit, ) +def serialize_multiple_receipts_caea(receipts): + + receipts = receipts.all().order_by("receipt_number") + + first = receipts.first() + receipts = [serialize_receipt_caea(receipt) for receipt in receipts] + + serialised = f.FeCAEARegInfReq( + FeCabReq=f.FECAECabRequest( + CantReg=len(receipts), + PtoVta=first.point_of_sales.number, + CbteTipo=first.receipt_type.code, + ), + FeDetReq=f.ArrayOfFECAEDetRequest(receipts), + ) + + return serialised + +def serialize_receipt_caea(receipt): + taxes = receipt.taxes.all() + vats = receipt.vat.all() + + serialized = f.FECAEADetRequest( + Concepto=receipt.concept.code, + DocTipo=receipt.document_type.code, + DocNro=receipt.document_number, + # TODO: Check that this is not None!, + CbteDesde=receipt.receipt_number, + CbteHasta=receipt.receipt_number, + CbteFch=serialize_date(receipt.issued_date), + ImpTotal=receipt.total_amount, + ImpTotConc=receipt.net_untaxed, + ImpNeto=receipt.net_taxed, + ImpOpEx=receipt.exempt_amount, + ImpIVA=sum(vat.amount for vat in vats), + ImpTrib=sum(tax.amount for tax in taxes), + MonId=receipt.currency.code, + MonCotiz=receipt.currency_quote, + ) + if int(receipt.concept.code) in (2, 3): + serialized.FchServDesde = serialize_date(receipt.service_start) + serialized.FchServHasta = serialize_date(receipt.service_end) + serialized.FchVtoPago = serialize_date(receipt.expiration_date) + + if taxes: + serialized.Tributos = f.ArrayOfTributo([serialize_tax(tax) for tax in taxes]) + + if vats: + serialized.Iva = f.ArrayOfAlicIva([serialize_vat(vat) for vat in vats]) + + related_receipts = receipt.related_receipts.all() + if related_receipts: + serialized.CbtesAsoc = f.ArrayOfCbteAsoc( + [ + f.CbteAsoc( + r.receipt_type.code, + r.point_of_sales.number, + r.receipt_number, + ) + for r in related_receipts + ] + ) + + serialized.CAEA = serialize_caea() + + return serialized def serialize_multiple_receipts(receipts): + receipts = receipts.all().order_by("receipt_number") first = receipts.first() + is_caea = first.point_of_sales.issuance_type == 'CAEA' receipts = [serialize_receipt(receipt) for receipt in receipts] serialised = f.FECAERequest( @@ -55,7 +125,6 @@ def serialize_multiple_receipts(receipts): return serialised - def serialize_receipt(receipt): taxes = receipt.taxes.all() vats = receipt.vat.all() @@ -138,5 +207,12 @@ def serialize_caea_order(order:int = None): if order: return order else: - order = 1 - return order \ No newline at end of file + return 1 + +def serialize_caea(): + caea = Caea.objects.all().filter(active=True) + + if caea.count() != 1: + raise CaeaCountError + else: + return caea.caea_code From 039b1ffd62135be12e5785e9344699913a6cc0d0 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 8 Sep 2022 20:26:36 -0300 Subject: [PATCH 06/59] add field to ReceiptValidation, create CaeaCounter to store correlations numbers and modify arguments in serializers _caea to connect to afip --- ...017_receipt_caea_receiptvalidation_caea.py | 24 +++ .../migrations/0018_caeacounter_and_more.py | 27 +++ ...019_rename_value_caeacounter_next_value.py | 18 ++ django_afip/models.py | 197 +++++++++++------- django_afip/serializers.py | 20 +- 5 files changed, 195 insertions(+), 91 deletions(-) create mode 100644 django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py create mode 100644 django_afip/migrations/0018_caeacounter_and_more.py create mode 100644 django_afip/migrations/0019_rename_value_caeacounter_next_value.py diff --git a/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py new file mode 100644 index 00000000..2066679c --- /dev/null +++ b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.1 on 2022-09-08 19:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0016_receipt_generated'), + ] + + operations = [ + migrations.AddField( + model_name='receipt', + name='caea', + field=models.ForeignKey(blank=True, help_text='CAEA in case that the receipt must contain it', null=True, on_delete=django.db.models.deletion.PROTECT, to='afip.caea'), + ), + migrations.AddField( + model_name='receiptvalidation', + name='caea', + field=models.BooleanField(default=False, help_text='Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number', verbose_name='is_caea'), + ), + ] diff --git a/django_afip/migrations/0018_caeacounter_and_more.py b/django_afip/migrations/0018_caeacounter_and_more.py new file mode 100644 index 00000000..505f5c57 --- /dev/null +++ b/django_afip/migrations/0018_caeacounter_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.1.1 on 2022-09-08 20:18 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0017_receipt_caea_receiptvalidation_caea'), + ] + + operations = [ + migrations.CreateModel( + name='CaeaCounter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('value', models.BigIntegerField(default=1)), + ('pos', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='counter', to='afip.pointofsales')), + ('receipt_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='counter', to='afip.receipttype')), + ], + ), + migrations.AddConstraint( + model_name='caeacounter', + constraint=models.UniqueConstraint(fields=('pos', 'receipt_type'), name='unique_migration_pos_receipt_combination'), + ), + ] diff --git a/django_afip/migrations/0019_rename_value_caeacounter_next_value.py b/django_afip/migrations/0019_rename_value_caeacounter_next_value.py new file mode 100644 index 00000000..b4b6c545 --- /dev/null +++ b/django_afip/migrations/0019_rename_value_caeacounter_next_value.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-08 20:34 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0018_caeacounter_and_more'), + ] + + operations = [ + migrations.RenameField( + model_name='caeacounter', + old_name='value', + new_name='next_value', + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 0c3f4723..babdc8d4 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -626,6 +626,71 @@ class Meta: verbose_name = _("taxpayer") verbose_name_plural = _("taxpayers") +class Caea(models.Model): + """ Represents a CAEA code to continue operating when AFIP is offline. + + The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached to the Queryset o the TaxPayer model as appropiate. + + Save() was overraided to ensure that once a CAEA was created the CAEA_code cannot be changed. + + """ + + __original_CAEA = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__original_CAEA = self.caea_code + + caea_code = models.PositiveBigIntegerField( + validators=[RegexValidator(regex="[0-9]{14}"),MaxValueValidator(99999999999999)], + help_text=_("CAEA code to operate offline AFIP"), + unique=True, + ) + + period = models.IntegerField( + help_text=_('Period to send in the CAEA request (yyyymm)') + ) + + order = models.IntegerField(choices=[(1,'1'),(2,'2')], + help_text=_('Month is divided in 1st quarter or 2nd quarter') + ) + + valid_since = models.DateField( + _("valid_to"), + ) + expires = models.DateField( + _("expires"), + ) + + generated = models.DateTimeField( + _("generated"), + ) + final_date_inform = models.DateField( + _("final_date_inform"), + ) + + taxpayer = models.ForeignKey( + TaxPayer, + verbose_name=_("taxpayer"), + related_name="caea_tickets", + on_delete=models.CASCADE, + ) + + active = models.BooleanField(default=False) + + def __str__(self) -> str: + return str(self.caea_code) + + + def save(self, force_insert=False, force_update=False, *args, **kwargs): + if self.caea_code != '': + if self.__original_CAEA == None: #prevent to create a CAEA without code + self.generated = default_generated() + super().save(force_insert, force_update, *args, **kwargs) + self.__original_CAEA = self.caea_code + else: + if self.caea_code == self.__original_CAEA: #Allow modify the data only if the CAEA deosn't change. + super().save(force_insert, force_update, *args, **kwargs) class PointOfSales(models.Model): """ @@ -970,18 +1035,21 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: # Skip any already-validated ones: qs = self.filter(validation__isnull=True).check_groupable() if qs.count() == 0: - print('paso por aca') return [] - qs.order_by("issued_date", "id")._assign_numbers() + + pos = qs[0].point_of_sales.issuance_type == 'CAEA' + + if pos: + qs.order_by("issued_date", "id") + else: + qs.order_by("issued_date", "id")._assign_numbers() return qs._validate(ticket) def _validate(self, ticket=None) -> list[str]: first = self.first() ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") - type_point = first.point_of_sales.issuance_type client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - print('\n',type_point,'\n') if first.point_of_sales.issuance_type == 'CAE': @@ -1024,17 +1092,13 @@ def _validate(self, ticket=None) -> list[str]: self.filter(validation__isnull=True).update(receipt_number=None) return errs else: - valor = True - if valor: #Eliminar esto - print('nothing to validate yet') - return None - else: - response = client.service.FECAEARegInformativo( + response = client.service.FECAEARegInformativo( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts_caea(self), ) - check_response(response) - for cae_data in response.FeDetResp.FECAEADetResponse: + check_response(response) + errs = [] + for cae_data in response.FeDetResp.FECAEADetResponse: if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: validation = ReceiptValidation.objects.create( result=cae_data.Resultado, @@ -1063,10 +1127,7 @@ def _validate(self, ticket=None) -> list[str]: parsers.parse_string(obs.Msg), ) ) - - # Remove the number from ones that failed to validate: - self.filter(validation__isnull=True).update(receipt_number=None) - return errs + return errs @@ -1292,6 +1353,14 @@ class Receipt(models.Model): _('Time when the receipt was created'), auto_now_add=True, ) + + caea = models.ForeignKey( + Caea, + on_delete=models.PROTECT, + help_text= _('CAEA in case that the receipt must contain it'), + blank=True, + null=True, + ) objects = ReceiptManager() @@ -1342,6 +1411,26 @@ def is_validated(self) -> bool: return self.validation.result == ReceiptValidation.RESULT_APPROVED except ReceiptValidation.DoesNotExist: return False + + def save(self, force_insert=False, force_update=False, *args, **kwargs): + + if self.point_of_sales.issuance_type == 'CAEA': + if self.receipt_number == None or self.receipt_number == "": + counter = CaeaCounter.objects.get_or_create(pos=self.point_of_sales, receipt_type = self.receipt_type)[0] + self.receipt_number = counter.next_value + counter.next_value +=1 + counter.save() + + caea = Caea.objects.all().filter(active=True) + if caea.count() != 1: + raise exceptions.CaeaCountError + else: + if self.caea == None or self.caea == "": + self.caea = caea[0] + + super().save(force_insert, force_update, *args, **kwargs) + + def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: """Validates this receipt. @@ -1818,71 +1907,27 @@ class Meta: verbose_name = _("receipt validation") verbose_name_plural = _("receipt validations") -class Caea(models.Model): - """ Represents a CAEA code to continue operating when AFIP is offline. - - The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached to the Queryset o the TaxPayer model as appropiate. - - Save() was overraided to ensure that once a CAEA was created the CAEA_code cannot be changed. - - """ - - __original_CAEA = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_CAEA = self.caea_code - - caea_code = models.PositiveBigIntegerField( - validators=[RegexValidator(regex="[0-9]{14}"),MaxValueValidator(99999999999999)], - help_text=_("CAEA code to operate offline AFIP"), - unique=True, - ) - - period = models.IntegerField( - help_text=_('Period to send in the CAEA request (yyyymm)') - ) - - order = models.IntegerField(choices=[(1,'1'),(2,'2')], - help_text=_('Month is divided in 1st quarter or 2nd quarter') - ) +class CaeaCounter(models.Model): - valid_since = models.DateField( - _("valid_to"), - ) - expires = models.DateField( - _("expires"), - ) - - generated = models.DateTimeField( - _("generated"), - ) - final_date_inform = models.DateField( - _("final_date_inform"), - ) + pos = models.ForeignKey( + PointOfSales, + related_name='counter', + on_delete=models.PROTECT) - taxpayer = models.ForeignKey( - TaxPayer, - verbose_name=_("taxpayer"), - related_name="caea_tickets", - on_delete=models.CASCADE, - ) + receipt_type = models.ForeignKey( + ReceiptType, + related_name='counter', + on_delete=models.PROTECT) - active = models.BooleanField(default=False) + next_value = models.BigIntegerField(default=1) - def __str__(self) -> str: - return str(self.caea_code) + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['pos', 'receipt_type'], name='unique_migration_pos_receipt_combination' + ) + ] - def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.caea_code != '': - if self.__original_CAEA == None: #prevent to create a CAEA without code - self.generated = default_generated() - super().save(force_insert, force_update, *args, **kwargs) - self.__original_CAEA = self.caea_code - else: - if self.caea_code == self.__original_CAEA: #Allow modify the data only if the CAEA deosn't change. - super().save(force_insert, force_update, *args, **kwargs) - diff --git a/django_afip/serializers.py b/django_afip/serializers.py index f18617e7..f2af9b96 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -2,7 +2,6 @@ from django.utils.functional import LazyObject from django_afip.clients import get_client -from .models import Caea from .exceptions import CaeaCountError @@ -40,19 +39,19 @@ def serialize_ticket(ticket): ) def serialize_multiple_receipts_caea(receipts): - + receipts = receipts.all().order_by("receipt_number") first = receipts.first() receipts = [serialize_receipt_caea(receipt) for receipt in receipts] - serialised = f.FeCAEARegInfReq( - FeCabReq=f.FECAECabRequest( + serialised = f.FECAEARequest( + FeCabReq=f.FECAEACabRequest( CantReg=len(receipts), PtoVta=first.point_of_sales.number, CbteTipo=first.receipt_type.code, ), - FeDetReq=f.ArrayOfFECAEDetRequest(receipts), + FeDetReq=f.ArrayOfFECAEADetRequest(receipts), ) return serialised @@ -102,7 +101,7 @@ def serialize_receipt_caea(receipt): ] ) - serialized.CAEA = serialize_caea() + serialized.CAEA = receipt.caea.caea_code return serialized @@ -111,7 +110,6 @@ def serialize_multiple_receipts(receipts): receipts = receipts.all().order_by("receipt_number") first = receipts.first() - is_caea = first.point_of_sales.issuance_type == 'CAEA' receipts = [serialize_receipt(receipt) for receipt in receipts] serialised = f.FECAERequest( @@ -208,11 +206,3 @@ def serialize_caea_order(order:int = None): return order else: return 1 - -def serialize_caea(): - caea = Caea.objects.all().filter(active=True) - - if caea.count() != 1: - raise CaeaCountError - else: - return caea.caea_code From 3284b40ad508d633f2e75032aa83159523f61c6f Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 13 Sep 2022 12:27:00 -0300 Subject: [PATCH 07/59] reorder logic in Receipt.save() --- django_afip/models.py | 16 +++++++++------- testapp/testapp/settings.py | 6 ++++++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index babdc8d4..d48445ca 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1413,13 +1413,7 @@ def is_validated(self) -> bool: return False def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.point_of_sales.issuance_type == 'CAEA': - if self.receipt_number == None or self.receipt_number == "": - counter = CaeaCounter.objects.get_or_create(pos=self.point_of_sales, receipt_type = self.receipt_type)[0] - self.receipt_number = counter.next_value - counter.next_value +=1 - counter.save() caea = Caea.objects.all().filter(active=True) if caea.count() != 1: @@ -1427,7 +1421,15 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): else: if self.caea == None or self.caea == "": self.caea = caea[0] - + + if self.receipt_number == None or self.receipt_number == "": + counter = CaeaCounter.objects.get_or_create(pos=self.point_of_sales, receipt_type = self.receipt_type)[0] + self.receipt_number = counter.next_value + counter.next_value +=1 + counter.save() + + super().save(force_insert, force_update, *args, **kwargs) + else: super().save(force_insert, force_update, *args, **kwargs) diff --git a/testapp/testapp/settings.py b/testapp/testapp/settings.py index 8aeddaa1..fc7de679 100644 --- a/testapp/testapp/settings.py +++ b/testapp/testapp/settings.py @@ -63,6 +63,12 @@ WSGI_APPLICATION = "testapp.wsgi.application" # Database +# DATABASES = { +# "default": { +# "ENGINE": "django.db.backends.sqlite3", +# "NAME": BASE_DIR / "db.sqlite3", +# } +# } DATABASES = {"default": dj_database_url.config()} # Internationalization From d0cf1c086f1098b34d245552e64c565c61623091 Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 14 Sep 2022 17:01:12 -0300 Subject: [PATCH 08/59] Modified Receipt and validation model to register CAEA validation, modified _validate to process creation of ReceiptValidation --- testapp/testapp/settings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/testapp/testapp/settings.py b/testapp/testapp/settings.py index fc7de679..637eb75b 100644 --- a/testapp/testapp/settings.py +++ b/testapp/testapp/settings.py @@ -63,13 +63,13 @@ WSGI_APPLICATION = "testapp.wsgi.application" # Database -# DATABASES = { -# "default": { -# "ENGINE": "django.db.backends.sqlite3", -# "NAME": BASE_DIR / "db.sqlite3", -# } -# } -DATABASES = {"default": dj_database_url.config()} +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} +#DATABASES = {"default": dj_database_url.config()} # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ From 82fe8001e420ac8d6b0460c4e0e4facddc4281da Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 14 Sep 2022 17:03:09 -0300 Subject: [PATCH 09/59] Modified Receipt and validation model to register CAEA validation, modified _validate to process creation of ReceiptValidation. --- .../migrations/0020_alter_receipt_caea.py | 19 +++++++++++++++++++ ..._alter_receiptvalidation_cae_expiration.py | 18 ++++++++++++++++++ django_afip/models.py | 14 +++++++++++--- django_afip/serializers.py | 7 +++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 django_afip/migrations/0020_alter_receipt_caea.py create mode 100644 django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py diff --git a/django_afip/migrations/0020_alter_receipt_caea.py b/django_afip/migrations/0020_alter_receipt_caea.py new file mode 100644 index 00000000..bc22204c --- /dev/null +++ b/django_afip/migrations/0020_alter_receipt_caea.py @@ -0,0 +1,19 @@ +# Generated by Django 4.1.1 on 2022-09-13 18:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0019_rename_value_caeacounter_next_value'), + ] + + operations = [ + migrations.AlterField( + model_name='receipt', + name='caea', + field=models.ForeignKey(blank=True, help_text='CAEA in case that the receipt must contain it', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='caea', to='afip.caea'), + ), + ] diff --git a/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py new file mode 100644 index 00000000..08ff8244 --- /dev/null +++ b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-13 19:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0020_alter_receipt_caea'), + ] + + operations = [ + migrations.AlterField( + model_name='receiptvalidation', + name='cae_expiration', + field=models.DateField(blank=True, help_text='The CAE expiration as returned by the AFIP.', null=True, verbose_name='cae expiration'), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index d48445ca..2241f3be 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1103,7 +1103,7 @@ def _validate(self, ticket=None) -> list[str]: validation = ReceiptValidation.objects.create( result=cae_data.Resultado, cae=cae_data.CAEA, - #cae_expiration=parsers.parse_date(cae_data.CAEFchVto), + #cae_expiration=parsers.parse_date(self.caea.expires), receipt=self.get( receipt_number=cae_data.CbteDesde, ), @@ -1356,6 +1356,7 @@ class Receipt(models.Model): caea = models.ForeignKey( Caea, + related_name='caea', on_delete=models.PROTECT, help_text= _('CAEA in case that the receipt must contain it'), blank=True, @@ -1489,11 +1490,16 @@ def revalidate(self) -> ReceiptValidation | None: if not receipt_data: return None + if receipt_data.Resultado == ReceiptValidation.RESULT_APPROVED: + if receipt_data.EmisionTipo == 'CAEA': + cae_expiration = None + else: + cae_expiration = receipt_data.EmisionTipo validation = ReceiptValidation.objects.create( result=receipt_data.Resultado, cae=receipt_data.CodAutorizacion, - cae_expiration=parsers.parse_date(receipt_data.FchVto), + cae_expiration=parsers.parse_date(cae_expiration), receipt=self, processed_date=parsers.parse_datetime( receipt_data.FchProceso, @@ -1524,7 +1530,7 @@ def __str__(self) -> str: return _("Unnumbered %s") % self.receipt_type class Meta: - ordering = ("issued_date",) + ordering = ("issued_date",) #this ordering return the same values for first(),last() when filter on 1 day verbose_name = _("receipt") verbose_name_plural = _("receipts") unique_together = [["point_of_sales", "receipt_type", "receipt_number"]] @@ -1866,6 +1872,8 @@ class ReceiptValidation(models.Model): cae_expiration = models.DateField( _("cae expiration"), help_text=_("The CAE expiration as returned by the AFIP."), + blank=True, #Must be blank or null when was approved from CAEA operations + null=True, ) observations = models.ManyToManyField( Observation, diff --git a/django_afip/serializers.py b/django_afip/serializers.py index f2af9b96..7fc9213d 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -26,6 +26,12 @@ def serialize_datetime(datetime): """ return datetime.strftime("%Y-%m-%dT%H:%M:%S-00:00") +def serialize_datetime_caea(datetime): + """ + A similar serealizer to the above one but, use a diferent format. + """ + return datetime.strftime("%Y%m%d%H%M%S") + def serialize_date(date): return date.strftime("%Y%m%d") @@ -102,6 +108,7 @@ def serialize_receipt_caea(receipt): ) serialized.CAEA = receipt.caea.caea_code + serialized.CbteFchHsGen = serialize_datetime_caea(receipt.generated) return serialized From ca587ea09e12fab7d93c9f74a98b89a374022ded Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 14 Sep 2022 17:08:16 -0300 Subject: [PATCH 10/59] Black formatting --- django_afip/exceptions.py | 1 + django_afip/migrations/0011_caea.py | 72 +++++- .../migrations/0012_alter_caea_caea_code.py | 16 +- .../migrations/0013_alter_caea_caea_code.py | 15 +- .../migrations/0014_remove_caea_service.py | 6 +- ...s_alter_caea_final_date_inform_and_more.py | 20 +- .../migrations/0016_receipt_generated.py | 12 +- ...017_receipt_caea_receiptvalidation_caea.py | 24 +- .../migrations/0018_caeacounter_and_more.py | 41 ++- ...019_rename_value_caeacounter_next_value.py | 8 +- .../migrations/0020_alter_receipt_caea.py | 15 +- ..._alter_receiptvalidation_cae_expiration.py | 13 +- django_afip/models.py | 235 ++++++++++-------- django_afip/serializers.py | 17 +- testapp/testapp/settings.py | 2 +- 15 files changed, 321 insertions(+), 176 deletions(-) diff --git a/django_afip/exceptions.py b/django_afip/exceptions.py index bb895764..3aaf08cc 100644 --- a/django_afip/exceptions.py +++ b/django_afip/exceptions.py @@ -64,5 +64,6 @@ class CannotValidateTogether(DjangoAfipException): class ValidationError(DjangoAfipException): """Raised when a single Receipt failed to validate with AFIP's WS.""" + class CaeaCountError(DjangoAfipException): """Raised when query the caea to obtain the number but 0 or 2 or more.""" diff --git a/django_afip/migrations/0011_caea.py b/django_afip/migrations/0011_caea.py index 681767f3..1cb83367 100644 --- a/django_afip/migrations/0011_caea.py +++ b/django_afip/migrations/0011_caea.py @@ -8,24 +8,70 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0010_alter_authticket_service'), + ("afip", "0010_alter_authticket_service"), ] operations = [ migrations.CreateModel( - name='Caea', + name="Caea", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('caea_code', models.BigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}')])), - ('period', models.IntegerField(help_text='Period to send in the CAEA request (yyyymm)')), - ('order', models.IntegerField(choices=[(1, '1'), (2, '2')], help_text='Month is divided in 1st quarter or 2nd quarter')), - ('valid_since', models.DateTimeField(verbose_name='valid_to')), - ('expires', models.DateTimeField(verbose_name='expires')), - ('generated', models.DateTimeField(verbose_name='generated')), - ('final_date_inform', models.DateTimeField(verbose_name='final_date_inform')), - ('service', models.CharField(help_text='Service for which this ticket has been authorized.', max_length=34, verbose_name='service')), - ('active', models.BooleanField(default=False)), - ('taxpayer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='caea_tickets', to='afip.taxpayer', verbose_name='taxpayer')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "caea_code", + models.BigIntegerField( + help_text="CAEA code to operate offline AFIP", + unique=True, + validators=[ + django.core.validators.RegexValidator(regex="[0-9]{14}") + ], + ), + ), + ( + "period", + models.IntegerField( + help_text="Period to send in the CAEA request (yyyymm)" + ), + ), + ( + "order", + models.IntegerField( + choices=[(1, "1"), (2, "2")], + help_text="Month is divided in 1st quarter or 2nd quarter", + ), + ), + ("valid_since", models.DateTimeField(verbose_name="valid_to")), + ("expires", models.DateTimeField(verbose_name="expires")), + ("generated", models.DateTimeField(verbose_name="generated")), + ( + "final_date_inform", + models.DateTimeField(verbose_name="final_date_inform"), + ), + ( + "service", + models.CharField( + help_text="Service for which this ticket has been authorized.", + max_length=34, + verbose_name="service", + ), + ), + ("active", models.BooleanField(default=False)), + ( + "taxpayer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="caea_tickets", + to="afip.taxpayer", + verbose_name="taxpayer", + ), + ), ], ), ] diff --git a/django_afip/migrations/0012_alter_caea_caea_code.py b/django_afip/migrations/0012_alter_caea_caea_code.py index afaac32b..dfa238de 100644 --- a/django_afip/migrations/0012_alter_caea_caea_code.py +++ b/django_afip/migrations/0012_alter_caea_caea_code.py @@ -7,13 +7,21 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0011_caea'), + ("afip", "0011_caea"), ] operations = [ migrations.AlterField( - model_name='caea', - name='caea_code', - field=models.BigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}'), django.core.validators.MinLengthValidator(14), django.core.validators.MaxLengthValidator(14)]), + model_name="caea", + name="caea_code", + field=models.BigIntegerField( + help_text="CAEA code to operate offline AFIP", + unique=True, + validators=[ + django.core.validators.RegexValidator(regex="[0-9]{14}"), + django.core.validators.MinLengthValidator(14), + django.core.validators.MaxLengthValidator(14), + ], + ), ), ] diff --git a/django_afip/migrations/0013_alter_caea_caea_code.py b/django_afip/migrations/0013_alter_caea_caea_code.py index 328c5bbe..f9128d42 100644 --- a/django_afip/migrations/0013_alter_caea_caea_code.py +++ b/django_afip/migrations/0013_alter_caea_caea_code.py @@ -7,13 +7,20 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0012_alter_caea_caea_code'), + ("afip", "0012_alter_caea_caea_code"), ] operations = [ migrations.AlterField( - model_name='caea', - name='caea_code', - field=models.PositiveBigIntegerField(help_text='CAEA code to operate offline AFIP', unique=True, validators=[django.core.validators.RegexValidator(regex='[0-9]{14}'), django.core.validators.MaxValueValidator(99999999999999)]), + model_name="caea", + name="caea_code", + field=models.PositiveBigIntegerField( + help_text="CAEA code to operate offline AFIP", + unique=True, + validators=[ + django.core.validators.RegexValidator(regex="[0-9]{14}"), + django.core.validators.MaxValueValidator(99999999999999), + ], + ), ), ] diff --git a/django_afip/migrations/0014_remove_caea_service.py b/django_afip/migrations/0014_remove_caea_service.py index 38e1b9ed..868b2f29 100644 --- a/django_afip/migrations/0014_remove_caea_service.py +++ b/django_afip/migrations/0014_remove_caea_service.py @@ -6,12 +6,12 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0013_alter_caea_caea_code'), + ("afip", "0013_alter_caea_caea_code"), ] operations = [ migrations.RemoveField( - model_name='caea', - name='service', + model_name="caea", + name="service", ), ] diff --git a/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py index aa5bcf94..2e5beb4a 100644 --- a/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py +++ b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py @@ -6,23 +6,23 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0014_remove_caea_service'), + ("afip", "0014_remove_caea_service"), ] operations = [ migrations.AlterField( - model_name='caea', - name='expires', - field=models.DateField(verbose_name='expires'), + model_name="caea", + name="expires", + field=models.DateField(verbose_name="expires"), ), migrations.AlterField( - model_name='caea', - name='final_date_inform', - field=models.DateField(verbose_name='final_date_inform'), + model_name="caea", + name="final_date_inform", + field=models.DateField(verbose_name="final_date_inform"), ), migrations.AlterField( - model_name='caea', - name='valid_since', - field=models.DateField(verbose_name='valid_to'), + model_name="caea", + name="valid_since", + field=models.DateField(verbose_name="valid_to"), ), ] diff --git a/django_afip/migrations/0016_receipt_generated.py b/django_afip/migrations/0016_receipt_generated.py index 02e19ad9..4dcaf1ed 100644 --- a/django_afip/migrations/0016_receipt_generated.py +++ b/django_afip/migrations/0016_receipt_generated.py @@ -7,14 +7,18 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0015_alter_caea_expires_alter_caea_final_date_inform_and_more'), + ("afip", "0015_alter_caea_expires_alter_caea_final_date_inform_and_more"), ] operations = [ migrations.AddField( - model_name='receipt', - name='generated', - field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now, verbose_name='Time when the receipt was created'), + model_name="receipt", + name="generated", + field=models.DateTimeField( + auto_now_add=True, + default=django.utils.timezone.now, + verbose_name="Time when the receipt was created", + ), preserve_default=False, ), ] diff --git a/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py index 2066679c..97a48358 100644 --- a/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py +++ b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py @@ -7,18 +7,28 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0016_receipt_generated'), + ("afip", "0016_receipt_generated"), ] operations = [ migrations.AddField( - model_name='receipt', - name='caea', - field=models.ForeignKey(blank=True, help_text='CAEA in case that the receipt must contain it', null=True, on_delete=django.db.models.deletion.PROTECT, to='afip.caea'), + model_name="receipt", + name="caea", + field=models.ForeignKey( + blank=True, + help_text="CAEA in case that the receipt must contain it", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="afip.caea", + ), ), migrations.AddField( - model_name='receiptvalidation', - name='caea', - field=models.BooleanField(default=False, help_text='Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number', verbose_name='is_caea'), + model_name="receiptvalidation", + name="caea", + field=models.BooleanField( + default=False, + help_text="Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number", + verbose_name="is_caea", + ), ), ] diff --git a/django_afip/migrations/0018_caeacounter_and_more.py b/django_afip/migrations/0018_caeacounter_and_more.py index 505f5c57..94b535c1 100644 --- a/django_afip/migrations/0018_caeacounter_and_more.py +++ b/django_afip/migrations/0018_caeacounter_and_more.py @@ -7,21 +7,46 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0017_receipt_caea_receiptvalidation_caea'), + ("afip", "0017_receipt_caea_receiptvalidation_caea"), ] operations = [ migrations.CreateModel( - name='CaeaCounter', + name="CaeaCounter", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('value', models.BigIntegerField(default=1)), - ('pos', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='counter', to='afip.pointofsales')), - ('receipt_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='counter', to='afip.receipttype')), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("value", models.BigIntegerField(default=1)), + ( + "pos", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="counter", + to="afip.pointofsales", + ), + ), + ( + "receipt_type", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="counter", + to="afip.receipttype", + ), + ), ], ), migrations.AddConstraint( - model_name='caeacounter', - constraint=models.UniqueConstraint(fields=('pos', 'receipt_type'), name='unique_migration_pos_receipt_combination'), + model_name="caeacounter", + constraint=models.UniqueConstraint( + fields=("pos", "receipt_type"), + name="unique_migration_pos_receipt_combination", + ), ), ] diff --git a/django_afip/migrations/0019_rename_value_caeacounter_next_value.py b/django_afip/migrations/0019_rename_value_caeacounter_next_value.py index b4b6c545..7d2245db 100644 --- a/django_afip/migrations/0019_rename_value_caeacounter_next_value.py +++ b/django_afip/migrations/0019_rename_value_caeacounter_next_value.py @@ -6,13 +6,13 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0018_caeacounter_and_more'), + ("afip", "0018_caeacounter_and_more"), ] operations = [ migrations.RenameField( - model_name='caeacounter', - old_name='value', - new_name='next_value', + model_name="caeacounter", + old_name="value", + new_name="next_value", ), ] diff --git a/django_afip/migrations/0020_alter_receipt_caea.py b/django_afip/migrations/0020_alter_receipt_caea.py index bc22204c..f6c206f3 100644 --- a/django_afip/migrations/0020_alter_receipt_caea.py +++ b/django_afip/migrations/0020_alter_receipt_caea.py @@ -7,13 +7,20 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0019_rename_value_caeacounter_next_value'), + ("afip", "0019_rename_value_caeacounter_next_value"), ] operations = [ migrations.AlterField( - model_name='receipt', - name='caea', - field=models.ForeignKey(blank=True, help_text='CAEA in case that the receipt must contain it', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='caea', to='afip.caea'), + model_name="receipt", + name="caea", + field=models.ForeignKey( + blank=True, + help_text="CAEA in case that the receipt must contain it", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="caea", + to="afip.caea", + ), ), ] diff --git a/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py index 08ff8244..ceed398c 100644 --- a/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py +++ b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py @@ -6,13 +6,18 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0020_alter_receipt_caea'), + ("afip", "0020_alter_receipt_caea"), ] operations = [ migrations.AlterField( - model_name='receiptvalidation', - name='cae_expiration', - field=models.DateField(blank=True, help_text='The CAE expiration as returned by the AFIP.', null=True, verbose_name='cae expiration'), + model_name="receiptvalidation", + name="cae_expiration", + field=models.DateField( + blank=True, + help_text="The CAE expiration as returned by the AFIP.", + null=True, + verbose_name="cae expiration", + ), ), ] diff --git a/django_afip/models.py b/django_afip/models.py index 2241f3be..44f4001e 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -115,6 +115,7 @@ def _get_storage_from_settings(setting_name: str) -> Storage: return import_string(settings.DEFAULT_FILE_STORAGE)() return import_string(path) + def caea_is_active(valid_since, valid_to) -> bool: valid_since = parsers.parse_date(valid_since) valid_to = parsers.parse_date(valid_to) @@ -546,8 +547,13 @@ def fetch_points_of_sales(self, ticket: AuthTicket = None) -> list[PointOfSales] ) return results - - def get_caea(self,period:int=None , order:int = None, ticket: AuthTicket = None,) -> Caea: + + def get_caea( + self, + period: int = None, + order: int = None, + ticket: AuthTicket = None, + ) -> Caea: """ Get a CAEA code for the TaxPayer, if a CAEA alredy exists the check_response will raise and exception @@ -559,25 +565,35 @@ def get_caea(self,period:int=None , order:int = None, ticket: AuthTicket = None, response = client.service.FECAEASolicitar( serializers.serialize_ticket(ticket), - Periodo = serializers.serialize_caea_period(period), - Orden = serializers.serialize_caea_order(order), + Periodo=serializers.serialize_caea_period(period), + Orden=serializers.serialize_caea_order(order), ) - check_response(response) #be aware that this func raise an error if it's present + check_response( + response + ) # be aware that this func raise an error if it's present caea = Caea.objects.create( - caea_code = int(response.ResultGet.CAEA), - period = int(response.ResultGet.Periodo), - order = int(response.ResultGet.Orden), - valid_since = parsers.parse_date(response.ResultGet.FchVigDesde), - expires = parsers.parse_date(response.ResultGet.FchVigHasta), - generated = parsers.parse_datetime(response.ResultGet.FchProceso), - final_date_inform = parsers.parse_date(response.ResultGet.FchTopeInf), - taxpayer = self, - active = caea_is_active(valid_since=response.ResultGet.FchVigDesde, valid_to=response.ResultGet.FchVigHasta), + caea_code=int(response.ResultGet.CAEA), + period=int(response.ResultGet.Periodo), + order=int(response.ResultGet.Orden), + valid_since=parsers.parse_date(response.ResultGet.FchVigDesde), + expires=parsers.parse_date(response.ResultGet.FchVigHasta), + generated=parsers.parse_datetime(response.ResultGet.FchProceso), + final_date_inform=parsers.parse_date(response.ResultGet.FchTopeInf), + taxpayer=self, + active=caea_is_active( + valid_since=response.ResultGet.FchVigDesde, + valid_to=response.ResultGet.FchVigHasta, + ), ) return caea - - def consult_caea(self,period: str = None, order:int = None, ticket: AuthTicket = None,) -> Caea: + + def consult_caea( + self, + period: str = None, + order: int = None, + ticket: AuthTicket = None, + ) -> Caea: """ Consult the CAEA code given by AFIP, if for some reason the code it's not saved in the db it will be created @@ -588,27 +604,31 @@ def consult_caea(self,period: str = None, order:int = None, ticket: AuthTicket = client = clients.get_client("wsfe", self.is_sandboxed) response = client.service.FECAEAConsultar( serializers.serialize_ticket(ticket), - Periodo = serializers.serialize_caea_period(period), - Orden = serializers.serialize_caea_order(order), + Periodo=serializers.serialize_caea_period(period), + Orden=serializers.serialize_caea_order(order), ) - check_response(response) #be aware that this func raise an error if it's present + check_response( + response + ) # be aware that this func raise an error if it's present update = { - 'caea_code': int(response.ResultGet.CAEA), - 'period' : int(response.ResultGet.Periodo), - 'order' : int(response.ResultGet.Orden), - 'valid_since' : parsers.parse_date(response.ResultGet.FchVigDesde), - 'expires' : parsers.parse_date(response.ResultGet.FchVigHasta), - 'generated' : parsers.parse_datetime(response.ResultGet.FchProceso), - 'final_date_inform' : parsers.parse_date(response.ResultGet.FchTopeInf), - 'taxpayer' : self, - 'active' : caea_is_active(valid_since=response.ResultGet.FchVigDesde, valid_to=response.ResultGet.FchVigHasta), - } + "caea_code": int(response.ResultGet.CAEA), + "period": int(response.ResultGet.Periodo), + "order": int(response.ResultGet.Orden), + "valid_since": parsers.parse_date(response.ResultGet.FchVigDesde), + "expires": parsers.parse_date(response.ResultGet.FchVigHasta), + "generated": parsers.parse_datetime(response.ResultGet.FchProceso), + "final_date_inform": parsers.parse_date(response.ResultGet.FchTopeInf), + "taxpayer": self, + "active": caea_is_active( + valid_since=response.ResultGet.FchVigDesde, + valid_to=response.ResultGet.FchVigHasta, + ), + } caea = Caea.objects.update_or_create( - caea_code = int(response.ResultGet.CAEA), - defaults=update) - + caea_code=int(response.ResultGet.CAEA), defaults=update + ) return caea @@ -626,8 +646,9 @@ class Meta: verbose_name = _("taxpayer") verbose_name_plural = _("taxpayers") + class Caea(models.Model): - """ Represents a CAEA code to continue operating when AFIP is offline. + """Represents a CAEA code to continue operating when AFIP is offline. The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached to the Queryset o the TaxPayer model as appropiate. @@ -642,17 +663,21 @@ def __init__(self, *args, **kwargs): self.__original_CAEA = self.caea_code caea_code = models.PositiveBigIntegerField( - validators=[RegexValidator(regex="[0-9]{14}"),MaxValueValidator(99999999999999)], + validators=[ + RegexValidator(regex="[0-9]{14}"), + MaxValueValidator(99999999999999), + ], help_text=_("CAEA code to operate offline AFIP"), unique=True, ) period = models.IntegerField( - help_text=_('Period to send in the CAEA request (yyyymm)') + help_text=_("Period to send in the CAEA request (yyyymm)") ) - order = models.IntegerField(choices=[(1,'1'),(2,'2')], - help_text=_('Month is divided in 1st quarter or 2nd quarter') + order = models.IntegerField( + choices=[(1, "1"), (2, "2")], + help_text=_("Month is divided in 1st quarter or 2nd quarter"), ) valid_since = models.DateField( @@ -681,17 +706,19 @@ def __init__(self, *args, **kwargs): def __str__(self) -> str: return str(self.caea_code) - def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.caea_code != '': - if self.__original_CAEA == None: #prevent to create a CAEA without code + if self.caea_code != "": + if self.__original_CAEA == None: # prevent to create a CAEA without code self.generated = default_generated() super().save(force_insert, force_update, *args, **kwargs) self.__original_CAEA = self.caea_code else: - if self.caea_code == self.__original_CAEA: #Allow modify the data only if the CAEA deosn't change. + if ( + self.caea_code == self.__original_CAEA + ): # Allow modify the data only if the CAEA deosn't change. super().save(force_insert, force_update, *args, **kwargs) + class PointOfSales(models.Model): """ Represents an existing AFIP point of sale. @@ -1036,8 +1063,8 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: qs = self.filter(validation__isnull=True).check_groupable() if qs.count() == 0: return [] - - pos = qs[0].point_of_sales.issuance_type == 'CAEA' + + pos = qs[0].point_of_sales.issuance_type == "CAEA" if pos: qs.order_by("issued_date", "id") @@ -1051,8 +1078,8 @@ def _validate(self, ticket=None) -> list[str]: ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - if first.point_of_sales.issuance_type == 'CAE': - + if first.point_of_sales.issuance_type == "CAE": + response = client.service.FECAESolicitar( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts(self), @@ -1093,44 +1120,43 @@ def _validate(self, ticket=None) -> list[str]: return errs else: response = client.service.FECAEARegInformativo( - serializers.serialize_ticket(ticket), - serializers.serialize_multiple_receipts_caea(self), - ) + serializers.serialize_ticket(ticket), + serializers.serialize_multiple_receipts_caea(self), + ) check_response(response) errs = [] for cae_data in response.FeDetResp.FECAEADetResponse: - if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: - validation = ReceiptValidation.objects.create( - result=cae_data.Resultado, - cae=cae_data.CAEA, - #cae_expiration=parsers.parse_date(self.caea.expires), - receipt=self.get( - receipt_number=cae_data.CbteDesde, - ), - processed_date=parsers.parse_datetime( - response.FeCabResp.FchProceso, - ), - caea = True, - ) - if cae_data.Observaciones: - for obs in cae_data.Observaciones.Obs: - observation = Observation.objects.create( - code=obs.Code, - message=obs.Msg, - ) - validation.observations.add(observation) - elif cae_data.Observaciones: + if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: + validation = ReceiptValidation.objects.create( + result=cae_data.Resultado, + cae=cae_data.CAEA, + # cae_expiration=parsers.parse_date(self.caea.expires), + receipt=self.get( + receipt_number=cae_data.CbteDesde, + ), + processed_date=parsers.parse_datetime( + response.FeCabResp.FchProceso, + ), + caea=True, + ) + if cae_data.Observaciones: for obs in cae_data.Observaciones.Obs: - errs.append( - "Error {}: {}".format( - obs.Code, - parsers.parse_string(obs.Msg), - ) + observation = Observation.objects.create( + code=obs.Code, + message=obs.Msg, ) + validation.observations.add(observation) + elif cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + errs.append( + "Error {}: {}".format( + obs.Code, + parsers.parse_string(obs.Msg), + ) + ) return errs - class ReceiptManager(models.Manager): """Default manager for the :class:`~.Receipt` class. @@ -1350,18 +1376,18 @@ class Receipt(models.Model): ) generated = models.DateTimeField( - _('Time when the receipt was created'), + _("Time when the receipt was created"), auto_now_add=True, - ) - + ) + caea = models.ForeignKey( Caea, - related_name='caea', + related_name="caea", on_delete=models.PROTECT, - help_text= _('CAEA in case that the receipt must contain it'), + help_text=_("CAEA in case that the receipt must contain it"), blank=True, null=True, - ) + ) objects = ReceiptManager() @@ -1412,11 +1438,11 @@ def is_validated(self) -> bool: return self.validation.result == ReceiptValidation.RESULT_APPROVED except ReceiptValidation.DoesNotExist: return False - + def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.point_of_sales.issuance_type == 'CAEA': + if self.point_of_sales.issuance_type == "CAEA": - caea = Caea.objects.all().filter(active=True) + caea = Caea.objects.all().filter(active=True) if caea.count() != 1: raise exceptions.CaeaCountError else: @@ -1424,17 +1450,17 @@ def save(self, force_insert=False, force_update=False, *args, **kwargs): self.caea = caea[0] if self.receipt_number == None or self.receipt_number == "": - counter = CaeaCounter.objects.get_or_create(pos=self.point_of_sales, receipt_type = self.receipt_type)[0] + counter = CaeaCounter.objects.get_or_create( + pos=self.point_of_sales, receipt_type=self.receipt_type + )[0] self.receipt_number = counter.next_value - counter.next_value +=1 + counter.next_value += 1 counter.save() super().save(force_insert, force_update, *args, **kwargs) else: super().save(force_insert, force_update, *args, **kwargs) - - def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: """Validates this receipt. @@ -1490,9 +1516,8 @@ def revalidate(self) -> ReceiptValidation | None: if not receipt_data: return None - if receipt_data.Resultado == ReceiptValidation.RESULT_APPROVED: - if receipt_data.EmisionTipo == 'CAEA': + if receipt_data.EmisionTipo == "CAEA": cae_expiration = None else: cae_expiration = receipt_data.EmisionTipo @@ -1530,7 +1555,9 @@ def __str__(self) -> str: return _("Unnumbered %s") % self.receipt_type class Meta: - ordering = ("issued_date",) #this ordering return the same values for first(),last() when filter on 1 day + ordering = ( + "issued_date", + ) # this ordering return the same values for first(),last() when filter on 1 day verbose_name = _("receipt") verbose_name_plural = _("receipts") unique_together = [["point_of_sales", "receipt_type", "receipt_number"]] @@ -1872,7 +1899,7 @@ class ReceiptValidation(models.Model): cae_expiration = models.DateField( _("cae expiration"), help_text=_("The CAE expiration as returned by the AFIP."), - blank=True, #Must be blank or null when was approved from CAEA operations + blank=True, # Must be blank or null when was approved from CAEA operations null=True, ) observations = models.ManyToManyField( @@ -1895,8 +1922,10 @@ class ReceiptValidation(models.Model): caea = models.BooleanField( default=False, - help_text=_('Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number'), - verbose_name=_("is_caea") + help_text=_( + "Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number" + ), + verbose_name=_("is_caea"), ) def __str__(self) -> str: @@ -1917,27 +1946,23 @@ class Meta: verbose_name = _("receipt validation") verbose_name_plural = _("receipt validations") + class CaeaCounter(models.Model): pos = models.ForeignKey( - PointOfSales, - related_name='counter', - on_delete=models.PROTECT) + PointOfSales, related_name="counter", on_delete=models.PROTECT + ) receipt_type = models.ForeignKey( - ReceiptType, - related_name='counter', - on_delete=models.PROTECT) + ReceiptType, related_name="counter", on_delete=models.PROTECT + ) next_value = models.BigIntegerField(default=1) class Meta: constraints = [ models.UniqueConstraint( - fields=['pos', 'receipt_type'], name='unique_migration_pos_receipt_combination' + fields=["pos", "receipt_type"], + name="unique_migration_pos_receipt_combination", ) ] - - - - diff --git a/django_afip/serializers.py b/django_afip/serializers.py index 7fc9213d..ea241fbb 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -26,6 +26,7 @@ def serialize_datetime(datetime): """ return datetime.strftime("%Y-%m-%dT%H:%M:%S-00:00") + def serialize_datetime_caea(datetime): """ A similar serealizer to the above one but, use a diferent format. @@ -44,6 +45,7 @@ def serialize_ticket(ticket): Cuit=ticket.owner.cuit, ) + def serialize_multiple_receipts_caea(receipts): receipts = receipts.all().order_by("receipt_number") @@ -62,6 +64,7 @@ def serialize_multiple_receipts_caea(receipts): return serialised + def serialize_receipt_caea(receipt): taxes = receipt.taxes.all() vats = receipt.vat.all() @@ -106,14 +109,15 @@ def serialize_receipt_caea(receipt): for r in related_receipts ] ) - + serialized.CAEA = receipt.caea.caea_code serialized.CbteFchHsGen = serialize_datetime_caea(receipt.generated) return serialized + def serialize_multiple_receipts(receipts): - + receipts = receipts.all().order_by("receipt_number") first = receipts.first() @@ -130,6 +134,7 @@ def serialize_multiple_receipts(receipts): return serialised + def serialize_receipt(receipt): taxes = receipt.taxes.all() vats = receipt.vat.all() @@ -201,14 +206,16 @@ def serialize_receipt_data(receipt_type, receipt_number, point_of_sales): CbteTipo=receipt_type, CbteNro=receipt_number, PtoVta=point_of_sales ) -def serialize_caea_period(period:str = None): + +def serialize_caea_period(period: str = None): if period: return period else: date = datetime.now() return date.strftime("%Y%m") - -def serialize_caea_order(order:int = None): + + +def serialize_caea_order(order: int = None): if order: return order else: diff --git a/testapp/testapp/settings.py b/testapp/testapp/settings.py index 637eb75b..08eef965 100644 --- a/testapp/testapp/settings.py +++ b/testapp/testapp/settings.py @@ -69,7 +69,7 @@ "NAME": BASE_DIR / "db.sqlite3", } } -#DATABASES = {"default": dj_database_url.config()} +# DATABASES = {"default": dj_database_url.config()} # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ From fc02db2e3f79275964f3631e58c0fe48e20a21c8 Mon Sep 17 00:00:00 2001 From: erebodino Date: Fri, 16 Sep 2022 08:34:22 -0300 Subject: [PATCH 11/59] added 2 new tests to ensure the type of insuance --- django_afip/models.py | 4 +++- tests/conftest.py | 3 +++ tests/test_models.py | 31 +++++++++++++++++++++++++++++++ tox.ini | 3 ++- 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 44f4001e..a1820b9b 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1118,6 +1118,7 @@ def _validate(self, ticket=None) -> list[str]: # Remove the number from ones that failed to validate: self.filter(validation__isnull=True).update(receipt_number=None) return errs + else: response = client.service.FECAEARegInformativo( serializers.serialize_ticket(ticket), @@ -1520,7 +1521,8 @@ def revalidate(self) -> ReceiptValidation | None: if receipt_data.EmisionTipo == "CAEA": cae_expiration = None else: - cae_expiration = receipt_data.EmisionTipo + cae_expiration = receipt_data.FchVto + validation = ReceiptValidation.objects.create( result=receipt_data.Resultado, cae=receipt_data.CodAutorizacion, diff --git a/tests/conftest.py b/tests/conftest.py index 7b34735b..4c1f7d4b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,3 +102,6 @@ def populated_db(live_ticket, live_taxpayer): models.load_metadata() live_taxpayer.fetch_points_of_sales() + pos = models.PointOfSales.objects.get(pk=1) + pos.issuance_type = 'CAE' + pos.save() diff --git a/tests/test_models.py b/tests/test_models.py index a9cbde88..a1cc7ff4 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -77,6 +77,37 @@ def test_validate_invoice(populated_db): assert receipt.validation.result == models.ReceiptValidation.RESULT_APPROVED assert models.ReceiptValidation.objects.count() == 1 +@pytest.mark.django_db +@pytest.mark.live +@pytest.mark.xfail +def test_fetch_pos_is_CAE(populated_db): + """Test validating valid receipts.""" + + payer = models.TaxPayer.objects.get(pk=1) + + assert models.PointOfSales.objects.get(pk=1).issuance_type == 'CAE' #POS fixed in populated_db + + pos = payer.fetch_points_of_sales() #get_or_update the existing pos + + assert len(pos) == 1 + assert pos[0][0].issuance_type == 'CAE' #fail because the field was updated + + + +@pytest.mark.django_db +@pytest.mark.live +def test_validate_issuance_type(populated_db): + """Test validating valid receipts.""" + + + receipt = ReceiptFactory() + another_receipt = ReceiptWithVatAndTaxFactory() + + assert (receipt.point_of_sales.issuance_type == 'CAE') + assert ( another_receipt.point_of_sales.issuance_type == 'CAE') + + + @pytest.mark.django_db @pytest.mark.live diff --git a/tox.ini b/tox.ini index 8e42b126..127c6e49 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ deps = mysql: -e .[mysql] django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 -commands = pytest -vv -m "not live" {posargs} +#commands = pytest -vv -m "not live" {posargs} +commands = pytest -vv setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From ce401399b1d137af4e0dcc21f3312cedfd89b885 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 19 Sep 2022 18:09:49 -0300 Subject: [PATCH 12/59] Modified logic in _validate to check if CAE or CAEA --- django_afip/factories.py | 30 +++ .../0022_alter_receiptvalidation_caea.py | 18 ++ .../migrations/0023_alter_receipt_caea.py | 26 +++ django_afip/models.py | 33 +-- tests/conftest.py | 6 +- tests/test_models.py | 201 +++++++++++++++--- tox.ini | 2 +- 7 files changed, 255 insertions(+), 61 deletions(-) create mode 100644 django_afip/migrations/0022_alter_receiptvalidation_caea.py create mode 100644 django_afip/migrations/0023_alter_receipt_caea.py diff --git a/django_afip/factories.py b/django_afip/factories.py index 02e0b4c9..9b947eb0 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -120,6 +120,13 @@ class Meta: sales_terms = "Credit Card" +class PointOfSalesFactoryCaea(PointOfSalesFactory): + + number = Sequence(lambda n: n + 1) + issuance_type = "CAEA" + + + class ReceiptFactory(DjangoModelFactory): class Meta: model = models.Receipt @@ -148,6 +155,14 @@ def post(obj: models.Receipt, create, extracted, **kwargs): VatFactory(vat_type__code=5, receipt=obj) TaxFactory(tax_type__code=3, receipt=obj) +class ReceiptWithVatAndTaxFactoryCaea(ReceiptFactory): + """Receipt with a valid Vat and Tax, ready to validate.""" + + @post_generation + def post(obj: models.Receipt, create, extracted, **kwargs): + VatFactory(vat_type__code=5, receipt=obj) + TaxFactory(tax_type__code=3, receipt=obj) + class ReceiptWithInconsistentVatAndTaxFactory(ReceiptWithVatAndTaxFactory): """Receipt with a valid Vat and Tax, ready to validate.""" @@ -241,3 +256,18 @@ class Meta: base_amount = 100 receipt = SubFactory(ReceiptFactory) tax_type = SubFactory(TaxTypeFactory) + +class CaeaFactory(DjangoModelFactory): + class Meta: + model = models.Caea + + caea_code = '12345678912345' + period = datetime.today().strftime('%Y%m') + order = '1' + valid_since = make_aware(datetime(2022, 6, 1)) + expires = make_aware(datetime(2022, 6, 15)) + generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) + final_date_inform = make_aware(datetime(2022, 6, 20)) + taxpayer = SubFactory(TaxPayerFactory) + active = True + diff --git a/django_afip/migrations/0022_alter_receiptvalidation_caea.py b/django_afip/migrations/0022_alter_receiptvalidation_caea.py new file mode 100644 index 00000000..0f537bf5 --- /dev/null +++ b/django_afip/migrations/0022_alter_receiptvalidation_caea.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2022-09-16 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0021_alter_receiptvalidation_cae_expiration'), + ] + + operations = [ + migrations.AlterField( + model_name='receiptvalidation', + name='caea', + field=models.BooleanField(default=False, help_text='Indicate if the validation was from a CAEA receipt, in that case the field CAE contains the CAEA number', verbose_name='is_caea'), + ), + ] diff --git a/django_afip/migrations/0023_alter_receipt_caea.py b/django_afip/migrations/0023_alter_receipt_caea.py new file mode 100644 index 00000000..19202d2b --- /dev/null +++ b/django_afip/migrations/0023_alter_receipt_caea.py @@ -0,0 +1,26 @@ +# Generated by Django 4.1.1 on 2022-09-16 19:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("afip", "0022_alter_receiptvalidation_caea"), + ] + + operations = [ + migrations.AlterField( + model_name="receipt", + name="caea", + field=models.ForeignKey( + blank=True, + help_text="CAEA in case that the receipt must contain it", + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="receipts", + to="afip.caea", + ), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index a1820b9b..e3424ef9 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -650,18 +650,10 @@ class Meta: class Caea(models.Model): """Represents a CAEA code to continue operating when AFIP is offline. - The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached to the Queryset o the TaxPayer model as appropiate. - - Save() was overraided to ensure that once a CAEA was created the CAEA_code cannot be changed. + The methods provideed by AFIP like: consulting CAEA or informing a Receipt will be attached the TaxPayer model. """ - __original_CAEA = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.__original_CAEA = self.caea_code - caea_code = models.PositiveBigIntegerField( validators=[ RegexValidator(regex="[0-9]{14}"), @@ -669,6 +661,8 @@ def __init__(self, *args, **kwargs): ], help_text=_("CAEA code to operate offline AFIP"), unique=True, + null=False, + blank=False, ) period = models.IntegerField( @@ -706,18 +700,6 @@ def __init__(self, *args, **kwargs): def __str__(self) -> str: return str(self.caea_code) - def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.caea_code != "": - if self.__original_CAEA == None: # prevent to create a CAEA without code - self.generated = default_generated() - super().save(force_insert, force_update, *args, **kwargs) - self.__original_CAEA = self.caea_code - else: - if ( - self.caea_code == self.__original_CAEA - ): # Allow modify the data only if the CAEA deosn't change. - super().save(force_insert, force_update, *args, **kwargs) - class PointOfSales(models.Model): """ @@ -1078,8 +1060,7 @@ def _validate(self, ticket=None) -> list[str]: ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - if first.point_of_sales.issuance_type == "CAE": - + if 'CAE' in first.point_of_sales.issuance_type: response = client.service.FECAESolicitar( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts(self), @@ -1383,7 +1364,7 @@ class Receipt(models.Model): caea = models.ForeignKey( Caea, - related_name="caea", + related_name="receipts", on_delete=models.PROTECT, help_text=_("CAEA in case that the receipt must contain it"), blank=True, @@ -1441,7 +1422,7 @@ def is_validated(self) -> bool: return False def save(self, force_insert=False, force_update=False, *args, **kwargs): - if self.point_of_sales.issuance_type == "CAEA": + if "CAEA" in self.point_of_sales.issuance_type: caea = Caea.objects.all().filter(active=True) if caea.count() != 1: @@ -1925,7 +1906,7 @@ class ReceiptValidation(models.Model): caea = models.BooleanField( default=False, help_text=_( - "Indicate if the validation was from a CAEA receipt, in that case the field cae contains the CAEA number" + "Indicate if the validation was from a CAEA receipt, in that case the field CAE contains the CAEA number" ), verbose_name=_("is_caea"), ) diff --git a/tests/conftest.py b/tests/conftest.py index 4c1f7d4b..f07e46ea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -102,6 +102,6 @@ def populated_db(live_ticket, live_taxpayer): models.load_metadata() live_taxpayer.fetch_points_of_sales() - pos = models.PointOfSales.objects.get(pk=1) - pos.issuance_type = 'CAE' - pos.save() + # pos = models.PointOfSales.objects.get(pk=1) + # pos.issuance_type = 'CAE' + # pos.save() diff --git a/tests/test_models.py b/tests/test_models.py index a1cc7ff4..e2ecd76a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -14,6 +14,8 @@ from django_afip.factories import ReceiptWithInconsistentVatAndTaxFactory from django_afip.factories import ReceiptWithVatAndTaxFactory +from datetime import datetime + def test_default_receipt_queryset(): assert isinstance(models.Receipt.objects.all(), models.ReceiptQuerySet) @@ -77,37 +79,6 @@ def test_validate_invoice(populated_db): assert receipt.validation.result == models.ReceiptValidation.RESULT_APPROVED assert models.ReceiptValidation.objects.count() == 1 -@pytest.mark.django_db -@pytest.mark.live -@pytest.mark.xfail -def test_fetch_pos_is_CAE(populated_db): - """Test validating valid receipts.""" - - payer = models.TaxPayer.objects.get(pk=1) - - assert models.PointOfSales.objects.get(pk=1).issuance_type == 'CAE' #POS fixed in populated_db - - pos = payer.fetch_points_of_sales() #get_or_update the existing pos - - assert len(pos) == 1 - assert pos[0][0].issuance_type == 'CAE' #fail because the field was updated - - - -@pytest.mark.django_db -@pytest.mark.live -def test_validate_issuance_type(populated_db): - """Test validating valid receipts.""" - - - receipt = ReceiptFactory() - another_receipt = ReceiptWithVatAndTaxFactory() - - assert (receipt.point_of_sales.issuance_type == 'CAE') - assert ( another_receipt.point_of_sales.issuance_type == 'CAE') - - - @pytest.mark.django_db @pytest.mark.live @@ -358,3 +329,171 @@ def test_populate_method(live_ticket): assert models.CurrencyType.objects.count() == 0 models.CurrencyType.objects.populate() assert models.CurrencyType.objects.count() == 50 + +@pytest.mark.django_db +def test_caea_creation(): + caea = factories.CaeaFactory() + + assert caea.caea_code == '12345678912345' + assert len(caea.caea_code) == 14 + assert caea.period == datetime.today().strftime('%Y%m') + +@pytest.mark.django_db +@pytest.mark.live +def test_create_caea_counter(populated_db): + + receipt_type = models.ReceiptType.objects.get(code=6) + pos = factories.PointOfSalesFactoryCaea() + try: + number = models.CaeaCounter.objects.get(pos=pos, receipt_type=receipt_type).next_value + except models.CaeaCounter.DoesNotExist: + number = None + + assert number == None + + caea = factories.CaeaFactory() + receipt = factories.ReceiptFactory(point_of_sales=pos) + number = models.CaeaCounter.objects.get(pos=pos,receipt_type=receipt_type).next_value + assert number == 2 + + +@pytest.mark.django_db +def test_create_receipt_caea(): + + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + pos_caea = models.PointOfSales.objects.all().filter(issuance_type = 'CAEA') + receipt = factories.ReceiptFactory(point_of_sales=pos) + + assert len(pos_caea) == 1 + assert receipt.point_of_sales.issuance_type == 'CAEA' + assert str(receipt.caea.caea_code) == caea.caea_code + assert receipt.receipt_number == 1 + + +@pytest.mark.django_db +@pytest.mark.xfail +def test_receipt_with_two_caea_should_fail(): + + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + caea2 = factories.CaeaFactory(caea_code = '12345678912346') + receipt = factories.ReceiptFactory(point_of_sales=pos) + +@pytest.mark.django_db +def test_caea_reverse_relation_receipts(): + + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + receipt_1 = factories.ReceiptFactory(point_of_sales=pos) + receipt_2 = factories.ReceiptFactory(point_of_sales=pos) + assert receipt_1 != receipt_2 + + receipts_with_caea = caea.receipts.all() + assert receipt_1 and receipt_2 in receipts_with_caea + assert len(receipts_with_caea) == 2 + assert receipt_1.receipt_number == 1 + assert receipt_2.receipt_number == 2 + +@pytest.mark.django_db +@pytest.mark.live +def test_validate_caea_receipt(populated_db): + + manager = models.ReceiptManager() + receipt_type = models.ReceiptType.objects.get(code=6) + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type) + caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + caea_counter.next_value = last_number + 1 + caea_counter.save() + + receipt_1 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + + errs = receipt_1.validate() + + assert len(errs) == 0 + assert receipt_1.validation.result == models.ReceiptValidation.RESULT_APPROVED + assert models.ReceiptValidation.objects.count() == 1 + + +@pytest.mark.django_db +@pytest.mark.live +def test_validate_caea_receipt_another_pos(populated_db): + + manager = models.ReceiptManager() + receipt_type = models.ReceiptType.objects.get(code=6) + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type) + caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + caea_counter.next_value = last_number + 1 + caea_counter.save() + + receipt_1 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + receipt_2 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + + caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + assert receipt_1 != receipt_2 + assert (caea_counter.next_value - 2) == receipt_1.receipt_number + assert (caea_counter.next_value - 1) == receipt_2.receipt_number + + qs = models.Receipt.objects.filter(point_of_sales = pos).filter(validation__isnull=True) + errs = qs.validate() + + assert len(errs) == 0 + assert receipt_1.validation.result == models.ReceiptValidation.RESULT_APPROVED + assert receipt_2.validation.result == models.ReceiptValidation.RESULT_APPROVED + assert models.ReceiptValidation.objects.count() == 2 + +@pytest.mark.django_db +@pytest.mark.live +def test_validate_credit_note_caea(populated_db): + """Test validating valid receipts.""" + #fetch data from afip to set the receipt number + manager = models.ReceiptManager() + receipt_type_fact = models.ReceiptType.objects.get(code=6) + receipt_type_cn = models.ReceiptType.objects.get(code=8) + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type_fact) + caea_counter_fact = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_fact)[0] + caea_counter_fact.next_value = last_number + 1 + caea_counter_fact.save() + + last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type_cn) + caea_counter_cn = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_cn)[0] + caea_counter_cn.next_value = last_number + 1 + caea_counter_cn.save() + + # Create a receipt (this credit note relates to it): + receipt = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + errs = receipt.validate() + assert len(errs) == 0 + + # Create a credit note for the above receipt: + credit_note = ReceiptWithVatAndTaxFactory(receipt_type__code=8, point_of_sales=pos) # Nota de Crédito B + credit_note.related_receipts.add(receipt) + credit_note.save() + + caea_counter_cn = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_cn)[0] + credit_note.validate(raise_=True) + assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) + assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED + + + + + + + + + + + + + + + + + diff --git a/tox.ini b/tox.ini index 127c6e49..8f4d31f0 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 #commands = pytest -vv -m "not live" {posargs} -commands = pytest -vv +commands = pytest -vv -s setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From 7f5e4d682710ac32be990107f20aa36c85fcb881 Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 21 Sep 2022 16:33:16 -0300 Subject: [PATCH 13/59] add pre_save to receipt, add __str__ to CaeaCounter, modify some test_models.py --- django_afip/factories.py | 32 +++-- .../0022_alter_receiptvalidation_caea.py | 12 +- django_afip/models.py | 38 ++---- django_afip/signals.py | 22 ++- tests/conftest.py | 16 ++- tests/test_models.py | 128 +++++++++++------- 6 files changed, 146 insertions(+), 102 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index 9b947eb0..7bc50401 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -14,6 +14,7 @@ from factory.django import ImageField from django_afip import models +import pytest def get_test_file(filename: str, mode="r") -> Path: @@ -121,12 +122,11 @@ class Meta: class PointOfSalesFactoryCaea(PointOfSalesFactory): - + number = Sequence(lambda n: n + 1) issuance_type = "CAEA" - class ReceiptFactory(DjangoModelFactory): class Meta: model = models.Receipt @@ -155,6 +155,7 @@ def post(obj: models.Receipt, create, extracted, **kwargs): VatFactory(vat_type__code=5, receipt=obj) TaxFactory(tax_type__code=3, receipt=obj) + class ReceiptWithVatAndTaxFactoryCaea(ReceiptFactory): """Receipt with a valid Vat and Tax, ready to validate.""" @@ -257,17 +258,18 @@ class Meta: receipt = SubFactory(ReceiptFactory) tax_type = SubFactory(TaxTypeFactory) -class CaeaFactory(DjangoModelFactory): - class Meta: - model = models.Caea - - caea_code = '12345678912345' - period = datetime.today().strftime('%Y%m') - order = '1' - valid_since = make_aware(datetime(2022, 6, 1)) - expires = make_aware(datetime(2022, 6, 15)) - generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) - final_date_inform = make_aware(datetime(2022, 6, 20)) - taxpayer = SubFactory(TaxPayerFactory) - active = True +# class CaeaFactory(DjangoModelFactory): +# class Meta: +# model = models.Caea + +# #payer = TaxPayerFactory +# caea_code = models.Caea.objects.get(pk=1).caea_code +# period = datetime.today().strftime('%Y%m') +# order = '1' +# valid_since = make_aware(datetime(2022, 6, 1)) +# expires = make_aware(datetime(2022, 6, 15)) +# generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) +# final_date_inform = make_aware(datetime(2022, 6, 20)) +# taxpayer = SubFactory(TaxPayerFactory) +# active = True diff --git a/django_afip/migrations/0022_alter_receiptvalidation_caea.py b/django_afip/migrations/0022_alter_receiptvalidation_caea.py index 0f537bf5..f5fef5ac 100644 --- a/django_afip/migrations/0022_alter_receiptvalidation_caea.py +++ b/django_afip/migrations/0022_alter_receiptvalidation_caea.py @@ -6,13 +6,17 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0021_alter_receiptvalidation_cae_expiration'), + ("afip", "0021_alter_receiptvalidation_cae_expiration"), ] operations = [ migrations.AlterField( - model_name='receiptvalidation', - name='caea', - field=models.BooleanField(default=False, help_text='Indicate if the validation was from a CAEA receipt, in that case the field CAE contains the CAEA number', verbose_name='is_caea'), + model_name="receiptvalidation", + name="caea", + field=models.BooleanField( + default=False, + help_text="Indicate if the validation was from a CAEA receipt, in that case the field CAE contains the CAEA number", + verbose_name="is_caea", + ), ), ] diff --git a/django_afip/models.py b/django_afip/models.py index e3424ef9..8d4392ac 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1060,7 +1060,7 @@ def _validate(self, ticket=None) -> list[str]: ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - if 'CAE' in first.point_of_sales.issuance_type: + if not "CAEA" in first.point_of_sales.issuance_type: response = client.service.FECAESolicitar( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts(self), @@ -1099,7 +1099,7 @@ def _validate(self, ticket=None) -> list[str]: # Remove the number from ones that failed to validate: self.filter(validation__isnull=True).update(receipt_number=None) return errs - + else: response = client.service.FECAEARegInformativo( serializers.serialize_ticket(ticket), @@ -1421,28 +1421,6 @@ def is_validated(self) -> bool: except ReceiptValidation.DoesNotExist: return False - def save(self, force_insert=False, force_update=False, *args, **kwargs): - if "CAEA" in self.point_of_sales.issuance_type: - - caea = Caea.objects.all().filter(active=True) - if caea.count() != 1: - raise exceptions.CaeaCountError - else: - if self.caea == None or self.caea == "": - self.caea = caea[0] - - if self.receipt_number == None or self.receipt_number == "": - counter = CaeaCounter.objects.get_or_create( - pos=self.point_of_sales, receipt_type=self.receipt_type - )[0] - self.receipt_number = counter.next_value - counter.next_value += 1 - counter.save() - - super().save(force_insert, force_update, *args, **kwargs) - else: - super().save(force_insert, force_update, *args, **kwargs) - def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: """Validates this receipt. @@ -1684,9 +1662,10 @@ def save_pdf(self, save_model: bool = True) -> None: from django_afip.views import ReceiptPDFView if not self.receipt.is_validated: - raise exceptions.DjangoAfipException( - _("Cannot generate pdf for non-authorized receipt") - ) + if not "CAEA" in self.receipt.point_of_sales.issuance_type: + raise exceptions.DjangoAfipException( + _("Cannot generate pdf for non-authorized receipt") + ) self.pdf_file = File(BytesIO(), name=f"{uuid4().hex}.pdf") render_pdf( @@ -1949,3 +1928,8 @@ class Meta: name="unique_migration_pos_receipt_combination", ) ] + + def __str__(self): + return "Counter for POS:{}, receipt_type:{}. Next_value is {}".format( + self.pos, self.receipt_type, self.next_value + ) diff --git a/django_afip/signals.py b/django_afip/signals.py index e6c11eb9..606db519 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -2,7 +2,7 @@ from django.db.models.signals import pre_save from django.dispatch import receiver -from django_afip import models +from django_afip import models, exceptions @receiver(pre_save, sender=models.TaxPayer) @@ -15,3 +15,23 @@ def update_certificate_expiration(sender, instance: models.TaxPayer, **kwargs): def generate_receipt_pdf(sender, instance: models.ReceiptPDF, **kwargs): if not instance.pdf_file and instance.receipt.is_validated: instance.save_pdf(save_model=True) + + +@receiver(pre_save, sender=models.Receipt) +def save_caea_data(sender, instance: models.TaxPayer, **kwargs): + if "CAEA" in instance.point_of_sales.issuance_type: + if instance.caea == "" or instance.caea == None: + caea = models.Caea.objects.all().filter(active=True) + if caea.count() != 1: + raise exceptions.CaeaCountError + else: + if instance.caea == None or instance.caea == "": + instance.caea = caea[0] + + if instance.receipt_number == None or instance.receipt_number == "": + counter = models.CaeaCounter.objects.get_or_create( + pos=instance.point_of_sales, receipt_type=instance.receipt_type + )[0] + instance.receipt_number = counter.next_value + counter.next_value += 1 + counter.save() diff --git a/tests/conftest.py b/tests/conftest.py index f07e46ea..5d890fef 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,8 @@ from django_afip.factories import get_test_file from django_afip.models import AuthTicket +from datetime import datetime + CACHED_TICKET_PATH = settings.BASE_DIR / "test_ticket.yaml" _live_mode = False @@ -102,6 +104,14 @@ def populated_db(live_ticket, live_taxpayer): models.load_metadata() live_taxpayer.fetch_points_of_sales() - # pos = models.PointOfSales.objects.get(pk=1) - # pos.issuance_type = 'CAE' - # pos.save() + + period = datetime.now().strftime("%Y%m") + if datetime.today().day > 15: + order = 2 + else: + order = 1 + + try: + live_taxpayer.get_caea(period=period, order=order) + except: + live_taxpayer.consult_caea(period=period, order=order) diff --git a/tests/test_models.py b/tests/test_models.py index e2ecd76a..abc36b55 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -330,13 +330,15 @@ def test_populate_method(live_ticket): models.CurrencyType.objects.populate() assert models.CurrencyType.objects.count() == 50 + @pytest.mark.django_db -def test_caea_creation(): - caea = factories.CaeaFactory() +def test_caea_creation(populated_db): + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + + assert len(str(caea.caea_code)) == 14 + assert str(caea.period) == datetime.today().strftime("%Y%m") - assert caea.caea_code == '12345678912345' - assert len(caea.caea_code) == 14 - assert caea.period == datetime.today().strftime('%Y%m') @pytest.mark.django_db @pytest.mark.live @@ -345,46 +347,55 @@ def test_create_caea_counter(populated_db): receipt_type = models.ReceiptType.objects.get(code=6) pos = factories.PointOfSalesFactoryCaea() try: - number = models.CaeaCounter.objects.get(pos=pos, receipt_type=receipt_type).next_value + number = models.CaeaCounter.objects.get( + pos=pos, receipt_type=receipt_type + ).next_value except models.CaeaCounter.DoesNotExist: number = None assert number == None - caea = factories.CaeaFactory() + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) receipt = factories.ReceiptFactory(point_of_sales=pos) - number = models.CaeaCounter.objects.get(pos=pos,receipt_type=receipt_type).next_value + number = models.CaeaCounter.objects.get( + pos=pos, receipt_type=receipt_type + ).next_value assert number == 2 @pytest.mark.django_db -def test_create_receipt_caea(): - +def test_create_receipt_caea(populated_db): + pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() - pos_caea = models.PointOfSales.objects.all().filter(issuance_type = 'CAEA') + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + pos_caea = models.PointOfSales.objects.all().filter(issuance_type="CAEA") receipt = factories.ReceiptFactory(point_of_sales=pos) assert len(pos_caea) == 1 - assert receipt.point_of_sales.issuance_type == 'CAEA' - assert str(receipt.caea.caea_code) == caea.caea_code + assert receipt.point_of_sales.issuance_type == "CAEA" + assert receipt.caea.caea_code == caea.caea_code assert receipt.receipt_number == 1 @pytest.mark.django_db @pytest.mark.xfail -def test_receipt_with_two_caea_should_fail(): +def test_receipt_with_two_caea_should_fail(populated_db): pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() - caea2 = factories.CaeaFactory(caea_code = '12345678912346') + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + caea2 = factories.CaeaFactory(caea_code="12345678912346") receipt = factories.ReceiptFactory(point_of_sales=pos) + @pytest.mark.django_db -def test_caea_reverse_relation_receipts(): +def test_caea_reverse_relation_receipts(populated_db): pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) receipt_1 = factories.ReceiptFactory(point_of_sales=pos) receipt_2 = factories.ReceiptFactory(point_of_sales=pos) assert receipt_1 != receipt_2 @@ -395,6 +406,7 @@ def test_caea_reverse_relation_receipts(): assert receipt_1.receipt_number == 1 assert receipt_2.receipt_number == 2 + @pytest.mark.django_db @pytest.mark.live def test_validate_caea_receipt(populated_db): @@ -402,9 +414,14 @@ def test_validate_caea_receipt(populated_db): manager = models.ReceiptManager() receipt_type = models.ReceiptType.objects.get(code=6) pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() - last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type) - caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + last_number = manager.fetch_last_receipt_number( + point_of_sales=pos, receipt_type=receipt_type + ) + caea_counter = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type + )[0] caea_counter.next_value = last_number + 1 caea_counter.save() @@ -424,21 +441,30 @@ def test_validate_caea_receipt_another_pos(populated_db): manager = models.ReceiptManager() receipt_type = models.ReceiptType.objects.get(code=6) pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() - last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type) - caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + last_number = manager.fetch_last_receipt_number( + point_of_sales=pos, receipt_type=receipt_type + ) + caea_counter = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type + )[0] caea_counter.next_value = last_number + 1 caea_counter.save() receipt_1 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) receipt_2 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) - caea_counter = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type)[0] + caea_counter = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type + )[0] assert receipt_1 != receipt_2 assert (caea_counter.next_value - 2) == receipt_1.receipt_number assert (caea_counter.next_value - 1) == receipt_2.receipt_number - qs = models.Receipt.objects.filter(point_of_sales = pos).filter(validation__isnull=True) + qs = models.Receipt.objects.filter(point_of_sales=pos).filter( + validation__isnull=True + ) errs = qs.validate() assert len(errs) == 0 @@ -446,23 +472,34 @@ def test_validate_caea_receipt_another_pos(populated_db): assert receipt_2.validation.result == models.ReceiptValidation.RESULT_APPROVED assert models.ReceiptValidation.objects.count() == 2 + @pytest.mark.django_db @pytest.mark.live def test_validate_credit_note_caea(populated_db): + """Test validating valid receipts.""" - #fetch data from afip to set the receipt number + # fetch data from afip to set the receipt number manager = models.ReceiptManager() receipt_type_fact = models.ReceiptType.objects.get(code=6) receipt_type_cn = models.ReceiptType.objects.get(code=8) pos = factories.PointOfSalesFactoryCaea() - caea = factories.CaeaFactory() - last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type_fact) - caea_counter_fact = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_fact)[0] + # caea = factories.CaeaFactory() + caea = models.Caea.objects.get(pk=1) + last_number = manager.fetch_last_receipt_number( + point_of_sales=pos, receipt_type=receipt_type_fact + ) + caea_counter_fact = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type_fact + )[0] caea_counter_fact.next_value = last_number + 1 caea_counter_fact.save() - last_number = manager.fetch_last_receipt_number(point_of_sales=pos, receipt_type=receipt_type_cn) - caea_counter_cn = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_cn)[0] + last_number = manager.fetch_last_receipt_number( + point_of_sales=pos, receipt_type=receipt_type_cn + ) + caea_counter_cn = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type_cn + )[0] caea_counter_cn.next_value = last_number + 1 caea_counter_cn.save() @@ -472,28 +509,15 @@ def test_validate_credit_note_caea(populated_db): assert len(errs) == 0 # Create a credit note for the above receipt: - credit_note = ReceiptWithVatAndTaxFactory(receipt_type__code=8, point_of_sales=pos) # Nota de Crédito B + credit_note = ReceiptWithVatAndTaxFactory( + receipt_type__code=8, point_of_sales=pos + ) # Nota de Crédito B credit_note.related_receipts.add(receipt) credit_note.save() - caea_counter_cn = models.CaeaCounter.objects.get_or_create(pos=pos, receipt_type=receipt_type_cn)[0] + caea_counter_cn = models.CaeaCounter.objects.get_or_create( + pos=pos, receipt_type=receipt_type_cn + )[0] credit_note.validate(raise_=True) assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED - - - - - - - - - - - - - - - - - From f25df0a0f8671db85a403406265ac4d0894dd1de Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 21 Sep 2022 18:19:47 -0300 Subject: [PATCH 14/59] add method to consult and inform CAEA without movement --- django_afip/factories.py | 28 +++--- .../migrations/0024_informedcaeas_and_more.py | 51 +++++++++++ ...0025_alter_informedcaeas_processed_date.py | 18 ++++ django_afip/models.py | 91 ++++++++++++++++++- 4 files changed, 173 insertions(+), 15 deletions(-) create mode 100644 django_afip/migrations/0024_informedcaeas_and_more.py create mode 100644 django_afip/migrations/0025_alter_informedcaeas_processed_date.py diff --git a/django_afip/factories.py b/django_afip/factories.py index 7bc50401..3591db8c 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -259,17 +259,17 @@ class Meta: tax_type = SubFactory(TaxTypeFactory) -# class CaeaFactory(DjangoModelFactory): -# class Meta: -# model = models.Caea - -# #payer = TaxPayerFactory -# caea_code = models.Caea.objects.get(pk=1).caea_code -# period = datetime.today().strftime('%Y%m') -# order = '1' -# valid_since = make_aware(datetime(2022, 6, 1)) -# expires = make_aware(datetime(2022, 6, 15)) -# generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) -# final_date_inform = make_aware(datetime(2022, 6, 20)) -# taxpayer = SubFactory(TaxPayerFactory) -# active = True +class CaeaFactory(DjangoModelFactory): + class Meta: + model = models.Caea + + # payer = TaxPayerFactory + caea_code = "12345678974125" + period = datetime.today().strftime("%Y%m") + order = "1" + valid_since = make_aware(datetime(2022, 6, 1)) + expires = make_aware(datetime(2022, 6, 15)) + generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) + final_date_inform = make_aware(datetime(2022, 6, 20)) + taxpayer = SubFactory(TaxPayerFactory) + active = True diff --git a/django_afip/migrations/0024_informedcaeas_and_more.py b/django_afip/migrations/0024_informedcaeas_and_more.py new file mode 100644 index 00000000..b09521e6 --- /dev/null +++ b/django_afip/migrations/0024_informedcaeas_and_more.py @@ -0,0 +1,51 @@ +# Generated by Django 4.1.1 on 2022-09-21 20:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("afip", "0023_alter_receipt_caea"), + ] + + operations = [ + migrations.CreateModel( + name="InformedCaeas", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("processed_date", models.DateTimeField(verbose_name="processed date")), + ( + "caea", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="informed", + to="afip.caea", + ), + ), + ( + "pos", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="informed", + to="afip.pointofsales", + ), + ), + ], + ), + migrations.AddConstraint( + model_name="informedcaeas", + constraint=models.UniqueConstraint( + fields=("pos", "caea"), name="unique_migration_pos_caea_combination" + ), + ), + ] diff --git a/django_afip/migrations/0025_alter_informedcaeas_processed_date.py b/django_afip/migrations/0025_alter_informedcaeas_processed_date.py new file mode 100644 index 00000000..d23fa2ff --- /dev/null +++ b/django_afip/migrations/0025_alter_informedcaeas_processed_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-21 21:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("afip", "0024_informedcaeas_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="informedcaeas", + name="processed_date", + field=models.DateField(verbose_name="processed date"), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 8d4392ac..aa99d2db 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -33,6 +33,7 @@ from OpenSSL.crypto import X509 from OpenSSL.crypto import load_certificate from zeep.exceptions import Fault +from django.utils.dateparse import parse_date from django_afip.clients import TZ_AR @@ -632,6 +633,68 @@ def consult_caea( return caea + def _inform_caea_without_operations( + self, + pos: PointOfSales = None, + caea: Caea = None, + ticket: AuthTicket = None, + ) -> InformedCaeas: + """ + Inform to AFIP that the PointOfSales and CAEA passed have not any movement between the duration of the CAEA + """ + ticket = ticket or self.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", self.is_sandboxed) + response = client.service.FECAEASinMovimientoInformar( + serializers.serialize_ticket(ticket), + PtoVta=pos.number, + CAEA=caea.caea_code, + ) + + check_response( + response + ) # be aware that this func raise an error if it's present + registry = InformedCaeas.objects.create( + pos=pos, + caea=caea, + processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), + ) + return registry + + def consult_caea_without_operations( + self, + pos: PointOfSales = None, + caea: Caea = None, + ticket: AuthTicket = None, + ) -> InformedCaeas: + """ + Inform to AFIP that the PointOfSales and CAEA passed have not any movement between the duration of the CAEA + """ + + try: + registry = InformedCaeas.objects.get(pos=pos, caea=caea) + return registry + except InformedCaeas.DoesNotExist: + registry = None + + ticket = ticket or self.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", self.is_sandboxed) + response = client.service.FECAEASinMovimientoConsultar( + serializers.serialize_ticket(ticket), + PtoVta=pos.number, + CAEA=caea.caea_code, + ) + try: + check_response( + response + ) # be aware that this func raise an error if it's present + except exceptions.AfipException: + registry = self._inform_caea_without_operations( + pos=pos, caea=caea, ticket=ticket + ) + return registry + def __repr__(self) -> str: return "".format( self.pk, @@ -1493,7 +1556,7 @@ def revalidate(self) -> ReceiptValidation | None: ) if receipt_data.Observaciones: for obs in receipt_data.Observaciones.Obs: - observation = Observation.objects.get_or_create( + observation, _ = Observation.objects.get_or_create( code=obs.Code, message=obs.Msg, ) @@ -1933,3 +1996,29 @@ def __str__(self): return "Counter for POS:{}, receipt_type:{}. Next_value is {}".format( self.pos, self.receipt_type, self.next_value ) + + +class InformedCaeas(models.Model): + + pos = models.ForeignKey( + PointOfSales, related_name="informed", on_delete=models.PROTECT + ) + + caea = models.ForeignKey(Caea, related_name="informed", on_delete=models.PROTECT) + + processed_date = models.DateField( + _("processed date"), + ) + + def __str__(self): + return "POS:{}, with CAEA:{}, informed as without movement in {}".format( + self.pos, self.caea, self.processed_date + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["pos", "caea"], + name="unique_migration_pos_caea_combination", + ) + ] From 4c9d26a8bd6028207eb8f17ce26f42e0136951d5 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 10:25:40 -0300 Subject: [PATCH 15/59] added test to check InformedCaea and rewrite some test with CaeaFactory --- django_afip/factories.py | 1 - tests/conftest.py | 1 - tests/test_models.py | 65 +++++++++++++++++++++++++++++++++++----- tox.ini | 2 +- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index 3591db8c..9aab470b 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -263,7 +263,6 @@ class CaeaFactory(DjangoModelFactory): class Meta: model = models.Caea - # payer = TaxPayerFactory caea_code = "12345678974125" period = datetime.today().strftime("%Y%m") order = "1" diff --git a/tests/conftest.py b/tests/conftest.py index 5d890fef..803d8c94 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -110,7 +110,6 @@ def populated_db(live_ticket, live_taxpayer): order = 2 else: order = 1 - try: live_taxpayer.get_caea(period=period, order=order) except: diff --git a/tests/test_models.py b/tests/test_models.py index abc36b55..2ae48fd8 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -333,7 +333,26 @@ def test_populate_method(live_ticket): @pytest.mark.django_db def test_caea_creation(populated_db): - # caea = factories.CaeaFactory() + caea = factories.CaeaFactory() + + assert len(str(caea.caea_code)) == 14 + assert str(caea.caea_code) == "12345678974125" + + +@pytest.mark.django_db +def test_caea_creation_should_fail(populated_db): + + with pytest.raises( + ValueError, + match="Field 'caea_code' expected a number but got 'A234567897412B'.", + ): + caea = factories.CaeaFactory(caea_code="A234567897412B") + + +@pytest.mark.django_db +@pytest.mark.live +def test_caea_creation_live(populated_db): + caea = models.Caea.objects.get(pk=1) assert len(str(caea.caea_code)) == 14 @@ -346,12 +365,13 @@ def test_create_caea_counter(populated_db): receipt_type = models.ReceiptType.objects.get(code=6) pos = factories.PointOfSalesFactoryCaea() - try: + number = None + with pytest.raises( + models.CaeaCounter.DoesNotExist, + ): number = models.CaeaCounter.objects.get( pos=pos, receipt_type=receipt_type ).next_value - except models.CaeaCounter.DoesNotExist: - number = None assert number == None @@ -380,14 +400,15 @@ def test_create_receipt_caea(populated_db): @pytest.mark.django_db -@pytest.mark.xfail def test_receipt_with_two_caea_should_fail(populated_db): pos = factories.PointOfSalesFactoryCaea() - # caea = factories.CaeaFactory() caea = models.Caea.objects.get(pk=1) caea2 = factories.CaeaFactory(caea_code="12345678912346") - receipt = factories.ReceiptFactory(point_of_sales=pos) + with pytest.raises( + exceptions.CaeaCountError, + ): + receipt = factories.ReceiptFactory(point_of_sales=pos) @pytest.mark.django_db @@ -521,3 +542,33 @@ def test_validate_credit_note_caea(populated_db): credit_note.validate(raise_=True) assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED + + +@pytest.mark.django_db +@pytest.mark.live +def test_inform_caea_without_movement(populated_db): + pos = factories.PointOfSalesFactoryCaea() + caea = models.Caea.objects.get(pk=1) + payer = factories.TaxPayerFactory() + + resp = payer.consult_caea_without_operations(pos=pos, caea=caea) + assert isinstance(resp, models.InformedCaeas) + + +@pytest.mark.django_db +@pytest.mark.live +def test_creation_informedcaea(populated_db): + pos = factories.PointOfSalesFactoryCaea() + caea = models.Caea.objects.get(pk=1) + payer = factories.TaxPayerFactory() + + with pytest.raises( + models.InformedCaeas.DoesNotExist, + ): + informed_caea = models.InformedCaeas.objects.get(pos=pos, caea=caea) + + payer.consult_caea_without_operations(pos=pos, caea=caea) + informed_caea = models.InformedCaeas.objects.get(pos=pos, caea=caea) + assert informed_caea.pk == 1 + assert informed_caea.caea == caea + assert informed_caea.pos == pos diff --git a/tox.ini b/tox.ini index 8f4d31f0..ba8d7ac2 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 #commands = pytest -vv -m "not live" {posargs} -commands = pytest -vv -s +commands = pytest -vv -s tests/test_models.py setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From 521d3c782c4704ec93623a1013412fe6e4d340c3 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 10:28:29 -0300 Subject: [PATCH 16/59] Formatting of variables in docs --- django_afip/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/models.py b/django_afip/models.py index aa99d2db..d5576461 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -523,7 +523,7 @@ def fetch_points_of_sales(self, ticket: AuthTicket = None) -> list[PointOfSales] Fetch all point of sales from the WS and store (or update) them locally. - Returns a list of tuples with the format (pos, created,). + Returns a list of tuples with the format ``(pos, created,)``. """ ticket = ticket or self.get_or_create_ticket("wsfe") From fd055bf7e87ab1fe9c09dba1a33e93d603d363f9 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 13:30:50 -0300 Subject: [PATCH 17/59] Added support for PDF generation with CAEA --- django_afip/pdf.py | 8 +++++++- django_afip/signals.py | 10 ++++++++-- django_afip/templates/receipts/code_6.html | 15 +++++++++++---- django_afip/views.py | 4 ++-- 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/django_afip/pdf.py b/django_afip/pdf.py index e1b0d6f9..613ce4fa 100644 --- a/django_afip/pdf.py +++ b/django_afip/pdf.py @@ -26,6 +26,12 @@ def __init__(self, receipt: Receipt): # # Using floats seems to be the only viable solution, and SHOULD be fine for # values in the range supported. + + if "CAEA" in receipt.point_of_sales.issuance_type: + authorization_code = receipt.caea.caea_code + else: + authorization_code = receipt.validation.cae + self._data = { "ver": 1, "fecha": receipt.issued_date.strftime("%Y-%m-%d"), @@ -39,7 +45,7 @@ def __init__(self, receipt: Receipt): "tipoDocRec": receipt.document_type.code, "nroDocRec": receipt.document_number, "tipoCodAut": "E", # We don't support anything except CAE. - "codAut": receipt.validation.cae, + "codAut": authorization_code, } def as_png(self) -> Image: diff --git a/django_afip/signals.py b/django_afip/signals.py index 606db519..1d40525a 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -13,8 +13,14 @@ def update_certificate_expiration(sender, instance: models.TaxPayer, **kwargs): @receiver(post_save, sender=models.ReceiptPDF) def generate_receipt_pdf(sender, instance: models.ReceiptPDF, **kwargs): - if not instance.pdf_file and instance.receipt.is_validated: - instance.save_pdf(save_model=True) + if not instance.pdf_file: + if "CAEA" in instance.receipt.point_of_sales.issuance_type: + instance.save_pdf(save_model=True) + if ( + "CAE" in instance.receipt.point_of_sales.issuance_type + and instance.receipt.is_validated + ): + instance.save_pdf(save_model=True) @receiver(pre_save, sender=models.Receipt) diff --git a/django_afip/templates/receipts/code_6.html b/django_afip/templates/receipts/code_6.html index 176e61cb..123ff386 100644 --- a/django_afip/templates/receipts/code_6.html +++ b/django_afip/templates/receipts/code_6.html @@ -116,10 +116,17 @@

- CAE - {{ pdf.receipt.validation.cae }} - Vto CAE - {{ pdf.receipt.validation.cae_expiration }} + {% if pdf.receipt.caea %} + CAEA + {{ pdf.receipt.caea.caea_code }} + Vto CAEA + {{ pdf.receipt.caea.expires }} + {% else %} + CAE + {{ pdf.receipt.validation.cae }} + Vto CAE + {{ pdf.receipt.validation.cae_expiration }} + {%endif%}

Consultas de validez: diff --git a/django_afip/views.py b/django_afip/views.py index b5b8cebf..2c59c6dc 100644 --- a/django_afip/views.py +++ b/django_afip/views.py @@ -69,7 +69,7 @@ def get_context_for_pk(pk, *args, **kwargs): "receipt", "receipt__receipt_type", "receipt__document_type", - "receipt__validation", + # "receipt__validation", "receipt__point_of_sales", "receipt__point_of_sales__owner", ) @@ -86,7 +86,7 @@ def get_context_for_pk(pk, *args, **kwargs): models.Receipt.objects.select_related( "receipt_type", "document_type", - "validation", + # "validation", "point_of_sales", "point_of_sales__owner", ) From 08a96c7535206fa0e25d09354f4285b535d93d69 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 18:48:09 -0300 Subject: [PATCH 18/59] added tests to PDF creations --- tests/test_models.py | 23 ++++++------- tests/test_pdf.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 4 +-- 3 files changed, 92 insertions(+), 13 deletions(-) diff --git a/tests/test_models.py b/tests/test_models.py index 2ae48fd8..5574b558 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -332,7 +332,7 @@ def test_populate_method(live_ticket): @pytest.mark.django_db -def test_caea_creation(populated_db): +def test_caea_creation(): caea = factories.CaeaFactory() assert len(str(caea.caea_code)) == 14 @@ -340,7 +340,7 @@ def test_caea_creation(populated_db): @pytest.mark.django_db -def test_caea_creation_should_fail(populated_db): +def test_caea_creation_should_fail(): with pytest.raises( ValueError, @@ -385,25 +385,26 @@ def test_create_caea_counter(populated_db): @pytest.mark.django_db -def test_create_receipt_caea(populated_db): +def test_create_receipt_caea(): pos = factories.PointOfSalesFactoryCaea() - # caea = factories.CaeaFactory() - caea = models.Caea.objects.get(pk=1) + caea = factories.CaeaFactory() + #caea = models.Caea.objects.get(pk=1) pos_caea = models.PointOfSales.objects.all().filter(issuance_type="CAEA") receipt = factories.ReceiptFactory(point_of_sales=pos) assert len(pos_caea) == 1 assert receipt.point_of_sales.issuance_type == "CAEA" - assert receipt.caea.caea_code == caea.caea_code + assert str(receipt.caea.caea_code) == caea.caea_code assert receipt.receipt_number == 1 @pytest.mark.django_db -def test_receipt_with_two_caea_should_fail(populated_db): +def test_receipt_with_two_caea_should_fail(): pos = factories.PointOfSalesFactoryCaea() - caea = models.Caea.objects.get(pk=1) + #caea = models.Caea.objects.get(pk=1) + caea = factories.CaeaFactory() caea2 = factories.CaeaFactory(caea_code="12345678912346") with pytest.raises( exceptions.CaeaCountError, @@ -412,11 +413,11 @@ def test_receipt_with_two_caea_should_fail(populated_db): @pytest.mark.django_db -def test_caea_reverse_relation_receipts(populated_db): +def test_caea_reverse_relation_receipts(): pos = factories.PointOfSalesFactoryCaea() - # caea = factories.CaeaFactory() - caea = models.Caea.objects.get(pk=1) + caea = factories.CaeaFactory() + #caea = models.Caea.objects.get(pk=1) receipt_1 = factories.ReceiptFactory(point_of_sales=pos) receipt_2 = factories.ReceiptFactory(point_of_sales=pos) assert receipt_1 != receipt_2 diff --git a/tests/test_pdf.py b/tests/test_pdf.py index 3cb2bf74..f7bdc381 100644 --- a/tests/test_pdf.py +++ b/tests/test_pdf.py @@ -91,3 +91,81 @@ def test_qrcode_data(): "tipoDocRec": 96, "ver": 1, } + + +# -------------------------------------------- +@pytest.mark.django_db +def test_pdf_generation_caea(): + """Test PDF file generation. + + For the moment, this test case mostly verifies that pdf generation + *works*, but does not actually validate the pdf file itself. + + Running this locally *will* yield the file itself, which is useful for + manual inspection. + """ + caea = factories.CaeaFactory() + pdf = factories.ReceiptPDFFactory( + receipt=factories.ReceiptFactory( + point_of_sales=factories.SubFactory(factories.PointOfSalesFactoryCaea) + ), + receipt__receipt_number=3, + ) + factories.ReceiptValidationFactory(receipt=pdf.receipt) + pdf.save_pdf() + regex = r"afip/receipts/[a-f0-9]{2}/[a-f0-9]{2}/[a-f0-9]{32}.pdf" + + assert re.match(regex, pdf.pdf_file.name) + assert pdf.pdf_file.name.endswith(".pdf") + + +@pytest.mark.django_db +def test_qrcode_data_caea(): + caea = factories.CaeaFactory() + pos_caea = factories.PointOfSalesFactoryCaea() + receipt = factories.ReceiptFactory(point_of_sales=pos_caea) + pdf = factories.ReceiptPDFFactory( + receipt=receipt, + ) + factories.ReceiptValidationFactory(receipt=pdf.receipt, cae=caea.caea_code) + + qrcode = ReceiptQrCode(pdf.receipt) + assert qrcode._data == { + "codAut": int(caea.caea_code), + "ctz": 1.0, + "cuit": "20329642330", + "fecha": str(date.today()), + "importe": 130.0, + "moneda": "PES", + "nroCmp": receipt.receipt_number, + "nroDocRec": receipt.document_number, + "ptoVta": receipt.point_of_sales.number, + "tipoCmp": receipt.receipt_type.code, + "tipoCodAut": "E", + "tipoDocRec": receipt.document_type.code, + "ver": 1, + } + + +@pytest.mark.django_db +def test_signal_generation_for_not_validated_receipt(): + caea = factories.CaeaFactory() + pos = factories.PointOfSalesFactoryCaea() + receipt = factories.ReceiptFactory(point_of_sales=pos) + printable = factories.ReceiptPDFFactory(receipt=receipt) + assert ( + printable.pdf_file + ) # even if is not validated with CAEA must print make a PDF + assert not receipt.is_validated + + +@pytest.mark.django_db +def test_signal_generation_for_not_validated_receipt(): + caea = factories.CaeaFactory() + pos = factories.PointOfSalesFactoryCaea() + receipt = factories.ReceiptFactory(point_of_sales=pos) + validation = factories.ReceiptValidationFactory(receipt=receipt) + + printable = factories.ReceiptPDFFactory(receipt=receipt) + assert printable.pdf_file + assert receipt.is_validated diff --git a/tox.ini b/tox.ini index ba8d7ac2..d4325d31 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,8 @@ deps = mysql: -e .[mysql] django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 -#commands = pytest -vv -m "not live" {posargs} -commands = pytest -vv -s tests/test_models.py +commands = pytest -vv -m "not live" {posargs} +#commands = pytest -vv -s setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From 4ac986acff39e698fe0505ca21ad8b026f8b779d Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 19:43:30 -0300 Subject: [PATCH 19/59] change .gitignore and __init__ --- .gitignore | 5 +---- django_afip/__init__.py | 7 ++----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index ed38f834..ae6b1eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -20,9 +20,6 @@ test.csr test2.csr #Branch files -/afi -/afip Pipfile Pipfile.lock -manage.py -django_afip/__init__.py + diff --git a/django_afip/__init__.py b/django_afip/__init__.py index 6ce7e18e..54657ca5 100644 --- a/django_afip/__init__.py +++ b/django_afip/__init__.py @@ -1,8 +1,5 @@ -try: - from . import version # type: ignore +from . import version # type: ignore - __version__ = version.version -except: - pass +__version__ = version.version default_app_config = "django_afip.apps.AfipConfig" From 0572768e75e9ffedf3dab1cce6b365dbe60bb068 Mon Sep 17 00:00:00 2001 From: erebodino Date: Thu, 22 Sep 2022 19:50:37 -0300 Subject: [PATCH 20/59] merge all the migrations from remote-origin and local --- django_afip/migrations/0026_merge_20220922_2245.py | 13 +++++++++++++ tox.ini | 1 - 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 django_afip/migrations/0026_merge_20220922_2245.py diff --git a/django_afip/migrations/0026_merge_20220922_2245.py b/django_afip/migrations/0026_merge_20220922_2245.py new file mode 100644 index 00000000..43d1fd52 --- /dev/null +++ b/django_afip/migrations/0026_merge_20220922_2245.py @@ -0,0 +1,13 @@ +# Generated by Django 4.1.1 on 2022-09-22 22:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("afip", "0011_receiptentry_discount_and_more"), + ("afip", "0025_alter_informedcaeas_processed_date"), + ] + + operations = [] diff --git a/tox.ini b/tox.ini index d4325d31..8e42b126 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,6 @@ deps = django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 commands = pytest -vv -m "not live" {posargs} -#commands = pytest -vv -s setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From d11f58fa009da8a3c79f4a548b1a58d5c5944212 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 22 Sep 2022 23:13:01 +0000 Subject: [PATCH 21/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .gitignore | 1 - django_afip/factories.py | 2 +- django_afip/migrations/0011_caea.py | 3 ++- django_afip/migrations/0012_alter_caea_caea_code.py | 3 ++- django_afip/migrations/0013_alter_caea_caea_code.py | 3 ++- ...caea_expires_alter_caea_final_date_inform_and_more.py | 3 ++- django_afip/migrations/0016_receipt_generated.py | 3 ++- .../0017_receipt_caea_receiptvalidation_caea.py | 3 ++- django_afip/migrations/0018_caeacounter_and_more.py | 3 ++- django_afip/migrations/0020_alter_receipt_caea.py | 3 ++- .../0021_alter_receiptvalidation_cae_expiration.py | 3 ++- .../migrations/0022_alter_receiptvalidation_caea.py | 3 ++- django_afip/migrations/0023_alter_receipt_caea.py | 3 ++- django_afip/migrations/0024_informedcaeas_and_more.py | 3 ++- .../0025_alter_informedcaeas_processed_date.py | 3 ++- django_afip/models.py | 5 +++-- django_afip/serializers.py | 2 ++ django_afip/signals.py | 3 ++- tests/conftest.py | 3 +-- tests/test_models.py | 9 ++++----- 20 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index ae6b1eb9..a737f2cc 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,3 @@ test2.csr #Branch files Pipfile Pipfile.lock - diff --git a/django_afip/factories.py b/django_afip/factories.py index f962235b..42e373c5 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -2,6 +2,7 @@ from datetime import datetime from pathlib import Path +import pytest from django.contrib.auth.models import User from django.utils.timezone import make_aware from factory import LazyFunction @@ -14,7 +15,6 @@ from factory.django import ImageField from django_afip import models -import pytest def get_test_file(filename: str, mode="r") -> Path: diff --git a/django_afip/migrations/0011_caea.py b/django_afip/migrations/0011_caea.py index 1cb83367..5a25dc19 100644 --- a/django_afip/migrations/0011_caea.py +++ b/django_afip/migrations/0011_caea.py @@ -1,8 +1,9 @@ # Generated by Django 4.1.1 on 2022-09-06 15:02 import django.core.validators -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0012_alter_caea_caea_code.py b/django_afip/migrations/0012_alter_caea_caea_code.py index dfa238de..792ad6db 100644 --- a/django_afip/migrations/0012_alter_caea_caea_code.py +++ b/django_afip/migrations/0012_alter_caea_caea_code.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-06 15:15 import django.core.validators -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0013_alter_caea_caea_code.py b/django_afip/migrations/0013_alter_caea_caea_code.py index f9128d42..625dd05f 100644 --- a/django_afip/migrations/0013_alter_caea_caea_code.py +++ b/django_afip/migrations/0013_alter_caea_caea_code.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-06 15:24 import django.core.validators -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py index 2e5beb4a..f5d1e207 100644 --- a/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py +++ b/django_afip/migrations/0015_alter_caea_expires_alter_caea_final_date_inform_and_more.py @@ -1,6 +1,7 @@ # Generated by Django 4.1.1 on 2022-09-06 21:43 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0016_receipt_generated.py b/django_afip/migrations/0016_receipt_generated.py index 4dcaf1ed..c2edd5d6 100644 --- a/django_afip/migrations/0016_receipt_generated.py +++ b/django_afip/migrations/0016_receipt_generated.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-07 13:01 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py index 97a48358..0f9e699e 100644 --- a/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py +++ b/django_afip/migrations/0017_receipt_caea_receiptvalidation_caea.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-08 19:18 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0018_caeacounter_and_more.py b/django_afip/migrations/0018_caeacounter_and_more.py index 94b535c1..db890079 100644 --- a/django_afip/migrations/0018_caeacounter_and_more.py +++ b/django_afip/migrations/0018_caeacounter_and_more.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-08 20:18 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0020_alter_receipt_caea.py b/django_afip/migrations/0020_alter_receipt_caea.py index f6c206f3..13606e9b 100644 --- a/django_afip/migrations/0020_alter_receipt_caea.py +++ b/django_afip/migrations/0020_alter_receipt_caea.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-13 18:35 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py index ceed398c..dc3120b0 100644 --- a/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py +++ b/django_afip/migrations/0021_alter_receiptvalidation_cae_expiration.py @@ -1,6 +1,7 @@ # Generated by Django 4.1.1 on 2022-09-13 19:54 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0022_alter_receiptvalidation_caea.py b/django_afip/migrations/0022_alter_receiptvalidation_caea.py index f5fef5ac..da8cea35 100644 --- a/django_afip/migrations/0022_alter_receiptvalidation_caea.py +++ b/django_afip/migrations/0022_alter_receiptvalidation_caea.py @@ -1,6 +1,7 @@ # Generated by Django 4.0.7 on 2022-09-16 13:54 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0023_alter_receipt_caea.py b/django_afip/migrations/0023_alter_receipt_caea.py index 19202d2b..afd4aace 100644 --- a/django_afip/migrations/0023_alter_receipt_caea.py +++ b/django_afip/migrations/0023_alter_receipt_caea.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-16 19:05 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0024_informedcaeas_and_more.py b/django_afip/migrations/0024_informedcaeas_and_more.py index b09521e6..d8cc0940 100644 --- a/django_afip/migrations/0024_informedcaeas_and_more.py +++ b/django_afip/migrations/0024_informedcaeas_and_more.py @@ -1,7 +1,8 @@ # Generated by Django 4.1.1 on 2022-09-21 20:45 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/migrations/0025_alter_informedcaeas_processed_date.py b/django_afip/migrations/0025_alter_informedcaeas_processed_date.py index d23fa2ff..7405b1f9 100644 --- a/django_afip/migrations/0025_alter_informedcaeas_processed_date.py +++ b/django_afip/migrations/0025_alter_informedcaeas_processed_date.py @@ -1,6 +1,7 @@ # Generated by Django 4.1.1 on 2022-09-21 21:04 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): diff --git a/django_afip/models.py b/django_afip/models.py index fb9c1ad2..b2830124 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -19,7 +19,9 @@ from django.core import management from django.core.files import File from django.core.files.storage import Storage +from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator +from django.core.validators import RegexValidator from django.db import connection from django.db import models from django.db.models import CheckConstraint @@ -27,17 +29,16 @@ from django.db.models import F from django.db.models import Q from django.db.models import Sum +from django.utils.dateparse import parse_date from django.utils.module_loading import import_string from django.utils.translation import gettext_lazy as _ from django_renderpdf.helpers import render_pdf -from django.core.validators import MaxValueValidator, RegexValidator from lxml import etree from lxml.builder import E from OpenSSL.crypto import FILETYPE_PEM from OpenSSL.crypto import X509 from OpenSSL.crypto import load_certificate from zeep.exceptions import Fault -from django.utils.dateparse import parse_date from django_afip.clients import TZ_AR diff --git a/django_afip/serializers.py b/django_afip/serializers.py index ea241fbb..ab1d0648 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -1,7 +1,9 @@ from datetime import datetime + from django.utils.functional import LazyObject from django_afip.clients import get_client + from .exceptions import CaeaCountError diff --git a/django_afip/signals.py b/django_afip/signals.py index 1d40525a..0d428a51 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -2,7 +2,8 @@ from django.db.models.signals import pre_save from django.dispatch import receiver -from django_afip import models, exceptions +from django_afip import exceptions +from django_afip import models @receiver(pre_save, sender=models.TaxPayer) diff --git a/tests/conftest.py b/tests/conftest.py index 803d8c94..0a386fa3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest.mock import patch import pytest @@ -10,8 +11,6 @@ from django_afip.factories import get_test_file from django_afip.models import AuthTicket -from datetime import datetime - CACHED_TICKET_PATH = settings.BASE_DIR / "test_ticket.yaml" _live_mode = False diff --git a/tests/test_models.py b/tests/test_models.py index 6f851e32..38464a97 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch @@ -14,8 +15,6 @@ from django_afip.factories import ReceiptWithInconsistentVatAndTaxFactory from django_afip.factories import ReceiptWithVatAndTaxFactory -from datetime import datetime - def test_default_receipt_queryset(): assert isinstance(models.Receipt.objects.all(), models.ReceiptQuerySet) @@ -389,7 +388,7 @@ def test_create_receipt_caea(): pos = factories.PointOfSalesFactoryCaea() caea = factories.CaeaFactory() - #caea = models.Caea.objects.get(pk=1) + # caea = models.Caea.objects.get(pk=1) pos_caea = models.PointOfSales.objects.all().filter(issuance_type="CAEA") receipt = factories.ReceiptFactory(point_of_sales=pos) @@ -403,7 +402,7 @@ def test_create_receipt_caea(): def test_receipt_with_two_caea_should_fail(): pos = factories.PointOfSalesFactoryCaea() - #caea = models.Caea.objects.get(pk=1) + # caea = models.Caea.objects.get(pk=1) caea = factories.CaeaFactory() caea2 = factories.CaeaFactory(caea_code="12345678912346") with pytest.raises( @@ -417,7 +416,7 @@ def test_caea_reverse_relation_receipts(): pos = factories.PointOfSalesFactoryCaea() caea = factories.CaeaFactory() - #caea = models.Caea.objects.get(pk=1) + # caea = models.Caea.objects.get(pk=1) receipt_1 = factories.ReceiptFactory(point_of_sales=pos) receipt_2 = factories.ReceiptFactory(point_of_sales=pos) assert receipt_1 != receipt_2 From d0e8e5fd6f80e90e4eb4f3954c249f8f8700bedb Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 4 Oct 2022 20:11:49 -0300 Subject: [PATCH 22/59] Change number of POS in CAEA factory --- django_afip/factories.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index 42e373c5..2c2e53a4 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -123,7 +123,7 @@ class Meta: class PointOfSalesFactoryCaea(PointOfSalesFactory): - number = Sequence(lambda n: n + 1) + number = 4 issuance_type = "CAEA" From 4e3e664ed5d5f71f3c6a33e21ff80034cbd066bc Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 11 Oct 2022 10:44:11 -0300 Subject: [PATCH 23/59] Solved errors marked by pre-commit.ci --- django_afip/models.py | 21 +++++++++++++++------ django_afip/serializers.py | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index b2830124..4d55683a 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -643,8 +643,8 @@ def consult_caea( def _inform_caea_without_operations( self, - pos: PointOfSales = None, - caea: Caea = None, + pos: PointOfSales, + caea: Caea, ticket: AuthTicket = None, ) -> InformedCaeas: """ @@ -671,12 +671,13 @@ def _inform_caea_without_operations( def consult_caea_without_operations( self, - pos: PointOfSales = None, - caea: Caea = None, + pos: PointOfSales, + caea: Caea, ticket: AuthTicket = None, - ) -> InformedCaeas: + ) -> InformedCaeas or None: """ - Inform to AFIP that the PointOfSales and CAEA passed have not any movement between the duration of the CAEA + Consult the state of the CAEA with AFIP, if the consult raise an error (probably CAEA without movement was never informed) + the method handle this an inform to AFIP the CAEA and POS """ try: @@ -697,6 +698,14 @@ def consult_caea_without_operations( check_response( response ) # be aware that this func raise an error if it's present + + #if for some reason a CAEA code was informed into AFIP DB but we have not a InformedCAEA this solve that + registry = InformedCaeas.objects.create( + pos=pos, + caea=caea, + processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), + ) + return registry except exceptions.AfipException: registry = self._inform_caea_without_operations( pos=pos, caea=caea, ticket=ticket diff --git a/django_afip/serializers.py b/django_afip/serializers.py index ab1d0648..f7408802 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -209,7 +209,7 @@ def serialize_receipt_data(receipt_type, receipt_number, point_of_sales): ) -def serialize_caea_period(period: str = None): +def serialize_caea_period(period: int = None): if period: return period else: From f9db7c3e1c52fb78b0d1bbde982372e00ba364cf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Oct 2022 13:44:34 +0000 Subject: [PATCH 24/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 4d55683a..2c74cfd6 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -699,12 +699,12 @@ def consult_caea_without_operations( response ) # be aware that this func raise an error if it's present - #if for some reason a CAEA code was informed into AFIP DB but we have not a InformedCAEA this solve that + # if for some reason a CAEA code was informed into AFIP DB but we have not a InformedCAEA this solve that registry = InformedCaeas.objects.create( - pos=pos, - caea=caea, - processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), - ) + pos=pos, + caea=caea, + processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), + ) return registry except exceptions.AfipException: registry = self._inform_caea_without_operations( From 27078aed2f99cc0ddf8485b45cc3c6505a8bf0cb Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 13:06:40 -0300 Subject: [PATCH 25/59] Added ready_to_print property on Receipt, refactor func generate_receipt_pdf on signal --- django_afip/models.py | 11 +++++++++++ django_afip/signals.py | 7 +------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 2c74cfd6..1fb26b65 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1502,6 +1502,17 @@ def is_validated(self) -> bool: return self.validation.result == ReceiptValidation.RESULT_APPROVED except ReceiptValidation.DoesNotExist: return False + + @property + def ready_to_print(self)->bool: + if "CAEA" in self.point_of_sales.issuance_type: + return True + + if ( + "CAE" in self.point_of_sales.issuance_type + and self.is_validated + ): + return True def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: """Validates this receipt. diff --git a/django_afip/signals.py b/django_afip/signals.py index 0d428a51..ad26df04 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -15,12 +15,7 @@ def update_certificate_expiration(sender, instance: models.TaxPayer, **kwargs): @receiver(post_save, sender=models.ReceiptPDF) def generate_receipt_pdf(sender, instance: models.ReceiptPDF, **kwargs): if not instance.pdf_file: - if "CAEA" in instance.receipt.point_of_sales.issuance_type: - instance.save_pdf(save_model=True) - if ( - "CAE" in instance.receipt.point_of_sales.issuance_type - and instance.receipt.is_validated - ): + if instance.receipt.ready_to_print: instance.save_pdf(save_model=True) From b2846057ec3c554f4d6003e876766a9f6ac1e3d1 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 18:39:12 -0300 Subject: [PATCH 26/59] Refactor caea assignation on save for receipts, now can handle multiple taxpayer and multiple caea actives --- django_afip/signals.py | 40 ++++++++++++++++++------------- tests/test_models.py | 54 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/django_afip/signals.py b/django_afip/signals.py index ad26df04..96dc46ae 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -2,9 +2,11 @@ from django.db.models.signals import pre_save from django.dispatch import receiver + from django_afip import exceptions from django_afip import models +import datetime @receiver(pre_save, sender=models.TaxPayer) def update_certificate_expiration(sender, instance: models.TaxPayer, **kwargs): @@ -21,19 +23,25 @@ def generate_receipt_pdf(sender, instance: models.ReceiptPDF, **kwargs): @receiver(pre_save, sender=models.Receipt) def save_caea_data(sender, instance: models.TaxPayer, **kwargs): - if "CAEA" in instance.point_of_sales.issuance_type: - if instance.caea == "" or instance.caea == None: - caea = models.Caea.objects.all().filter(active=True) - if caea.count() != 1: - raise exceptions.CaeaCountError - else: - if instance.caea == None or instance.caea == "": - instance.caea = caea[0] - - if instance.receipt_number == None or instance.receipt_number == "": - counter = models.CaeaCounter.objects.get_or_create( - pos=instance.point_of_sales, receipt_type=instance.receipt_type - )[0] - instance.receipt_number = counter.next_value - counter.next_value += 1 - counter.save() + if "CAEA" not in instance.point_of_sales.issuance_type: + return + + if instance.caea == "" or instance.caea == None: + date = datetime.datetime.now() + period = date.today().strftime("%Y%m") + order = 1 + if date.day > 15: + order = 2 + caea = models.Caea.objects.all().filter(active=True, taxpayer = instance.point_of_sales.owner, period=int(period), order=order) + if caea.count() != 1: + raise exceptions.CaeaCountError + else: + if instance.caea == None or instance.caea == "": + instance.caea = caea[0] + if instance.receipt_number == None or instance.receipt_number == "": + counter,_ = models.CaeaCounter.objects.get_or_create( + pos=instance.point_of_sales, receipt_type=instance.receipt_type + ) + instance.receipt_number = counter.next_value + counter.next_value += 1 + counter.save() diff --git a/tests/test_models.py b/tests/test_models.py index 38464a97..17278b0d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,4 +1,5 @@ from datetime import datetime +from django.utils.timezone import make_aware from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch @@ -9,7 +10,7 @@ from django_afip import exceptions from django_afip import factories from django_afip import models -from django_afip.factories import ReceiptFactory +from django_afip.factories import CaeaFactory, ReceiptFactory, TaxPayerFactory, SubFactory from django_afip.factories import ReceiptValidationFactory from django_afip.factories import ReceiptWithApprovedValidation from django_afip.factories import ReceiptWithInconsistentVatAndTaxFactory @@ -629,3 +630,54 @@ def test_receipt_entry_gt_total_discount(): with pytest.raises(Exception, match=r"\bdiscount_less_than_total\b"): factories.ReceiptEntryFactory(quantity=1, unit_price=1, discount=2) + +@pytest.mark.django_db +@pytest.mark.este +def test_bad_retrive_caea(): + """ + Test that in the way that the CAEA is assigned in the save signal even if + there are multiple taxpayer with 1 CAEA each the code assigned well. + + It not ensure that if the same taxpayer has multiples actives CAEAs the correct will be assigned + + """ + caea_good = CaeaFactory() + caea_bad = CaeaFactory( + caea_code = "12345678974188", + valid_since = make_aware(datetime(2022, 5, 15)), + expires = make_aware(datetime(2022, 5, 30)), + generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)), + taxpayer = TaxPayerFactory( + name = "Jane Doe", + cuit = 20366642330, + ) + ) + + pos = factories.PointOfSalesFactoryCaea() + receipt = factories.ReceiptFactory(point_of_sales=pos) + + assert caea_bad.caea_code != receipt.caea.caea_code + +@pytest.mark.django_db +@pytest.mark.este +def test_caea_assigned_receipt_correct(): + """ + Test that even if a taxpayer has multiples actives CAEAs the correct will be assigned + """ + caea_good = CaeaFactory() + caea_bad = CaeaFactory( + caea_code = "12345678974188", + period = 202209, + order = "1", + valid_since = make_aware(datetime(2022, 4, 15)), + expires = make_aware(datetime(2022, 4, 30)), + generated = make_aware(datetime(2022, 4, 14, 21, 6, 4)), + ) + + assert caea_bad != caea_good + + pos = factories.PointOfSalesFactoryCaea() + receipt = factories.ReceiptFactory(point_of_sales=pos) + + assert caea_bad.caea_code != receipt.caea.caea_code + assert models.Caea.objects.all().filter(active=True, taxpayer = receipt.point_of_sales.owner).count() == 2 \ No newline at end of file From 44c3fca5d2cca3cc24d46140ac4f6f69f91c4343 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 15 Oct 2022 21:39:33 +0000 Subject: [PATCH 27/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 9 +++------ django_afip/signals.py | 13 ++++++++---- tests/test_models.py | 46 +++++++++++++++++++++++++----------------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 1fb26b65..d1c1c3d6 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1502,16 +1502,13 @@ def is_validated(self) -> bool: return self.validation.result == ReceiptValidation.RESULT_APPROVED except ReceiptValidation.DoesNotExist: return False - + @property - def ready_to_print(self)->bool: + def ready_to_print(self) -> bool: if "CAEA" in self.point_of_sales.issuance_type: return True - if ( - "CAE" in self.point_of_sales.issuance_type - and self.is_validated - ): + if "CAE" in self.point_of_sales.issuance_type and self.is_validated: return True def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: diff --git a/django_afip/signals.py b/django_afip/signals.py index 96dc46ae..ed7c385c 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -1,12 +1,12 @@ +import datetime + from django.db.models.signals import post_save from django.db.models.signals import pre_save from django.dispatch import receiver - from django_afip import exceptions from django_afip import models -import datetime @receiver(pre_save, sender=models.TaxPayer) def update_certificate_expiration(sender, instance: models.TaxPayer, **kwargs): @@ -32,14 +32,19 @@ def save_caea_data(sender, instance: models.TaxPayer, **kwargs): order = 1 if date.day > 15: order = 2 - caea = models.Caea.objects.all().filter(active=True, taxpayer = instance.point_of_sales.owner, period=int(period), order=order) + caea = models.Caea.objects.all().filter( + active=True, + taxpayer=instance.point_of_sales.owner, + period=int(period), + order=order, + ) if caea.count() != 1: raise exceptions.CaeaCountError else: if instance.caea == None or instance.caea == "": instance.caea = caea[0] if instance.receipt_number == None or instance.receipt_number == "": - counter,_ = models.CaeaCounter.objects.get_or_create( + counter, _ = models.CaeaCounter.objects.get_or_create( pos=instance.point_of_sales, receipt_type=instance.receipt_type ) instance.receipt_number = counter.next_value diff --git a/tests/test_models.py b/tests/test_models.py index 17278b0d..57badea3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,20 +1,23 @@ from datetime import datetime -from django.utils.timezone import make_aware from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch import pytest +from django.utils.timezone import make_aware from pytest_django.asserts import assertQuerysetEqual from django_afip import exceptions from django_afip import factories from django_afip import models -from django_afip.factories import CaeaFactory, ReceiptFactory, TaxPayerFactory, SubFactory +from django_afip.factories import CaeaFactory +from django_afip.factories import ReceiptFactory from django_afip.factories import ReceiptValidationFactory from django_afip.factories import ReceiptWithApprovedValidation from django_afip.factories import ReceiptWithInconsistentVatAndTaxFactory from django_afip.factories import ReceiptWithVatAndTaxFactory +from django_afip.factories import SubFactory +from django_afip.factories import TaxPayerFactory def test_default_receipt_queryset(): @@ -630,7 +633,8 @@ def test_receipt_entry_gt_total_discount(): with pytest.raises(Exception, match=r"\bdiscount_less_than_total\b"): factories.ReceiptEntryFactory(quantity=1, unit_price=1, discount=2) - + + @pytest.mark.django_db @pytest.mark.este def test_bad_retrive_caea(): @@ -643,14 +647,14 @@ def test_bad_retrive_caea(): """ caea_good = CaeaFactory() caea_bad = CaeaFactory( - caea_code = "12345678974188", - valid_since = make_aware(datetime(2022, 5, 15)), - expires = make_aware(datetime(2022, 5, 30)), - generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)), - taxpayer = TaxPayerFactory( - name = "Jane Doe", - cuit = 20366642330, - ) + caea_code="12345678974188", + valid_since=make_aware(datetime(2022, 5, 15)), + expires=make_aware(datetime(2022, 5, 30)), + generated=make_aware(datetime(2022, 5, 30, 21, 6, 4)), + taxpayer=TaxPayerFactory( + name="Jane Doe", + cuit=20366642330, + ), ) pos = factories.PointOfSalesFactoryCaea() @@ -658,6 +662,7 @@ def test_bad_retrive_caea(): assert caea_bad.caea_code != receipt.caea.caea_code + @pytest.mark.django_db @pytest.mark.este def test_caea_assigned_receipt_correct(): @@ -666,12 +671,12 @@ def test_caea_assigned_receipt_correct(): """ caea_good = CaeaFactory() caea_bad = CaeaFactory( - caea_code = "12345678974188", - period = 202209, - order = "1", - valid_since = make_aware(datetime(2022, 4, 15)), - expires = make_aware(datetime(2022, 4, 30)), - generated = make_aware(datetime(2022, 4, 14, 21, 6, 4)), + caea_code="12345678974188", + period=202209, + order="1", + valid_since=make_aware(datetime(2022, 4, 15)), + expires=make_aware(datetime(2022, 4, 30)), + generated=make_aware(datetime(2022, 4, 14, 21, 6, 4)), ) assert caea_bad != caea_good @@ -680,4 +685,9 @@ def test_caea_assigned_receipt_correct(): receipt = factories.ReceiptFactory(point_of_sales=pos) assert caea_bad.caea_code != receipt.caea.caea_code - assert models.Caea.objects.all().filter(active=True, taxpayer = receipt.point_of_sales.owner).count() == 2 \ No newline at end of file + assert ( + models.Caea.objects.all() + .filter(active=True, taxpayer=receipt.point_of_sales.owner) + .count() + == 2 + ) From f5d2eaf12ec5dadbf0399972fc45a93913757863 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 19:04:55 -0300 Subject: [PATCH 28/59] Uncomment settings.DATABASES --- testapp/testapp/settings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/testapp/testapp/settings.py b/testapp/testapp/settings.py index 08eef965..fc7de679 100644 --- a/testapp/testapp/settings.py +++ b/testapp/testapp/settings.py @@ -63,13 +63,13 @@ WSGI_APPLICATION = "testapp.wsgi.application" # Database -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} -# DATABASES = {"default": dj_database_url.config()} +# DATABASES = { +# "default": { +# "ENGINE": "django.db.backends.sqlite3", +# "NAME": BASE_DIR / "db.sqlite3", +# } +# } +DATABASES = {"default": dj_database_url.config()} # Internationalization # https://docs.djangoproject.com/en/1.8/topics/i18n/ From b0653f96783c351a293268cb3f0e908d4570b56a Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 19:14:54 -0300 Subject: [PATCH 29/59] test_caea_creation_live change get(pk=1) for first() --- tests/test_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_models.py b/tests/test_models.py index 57badea3..2e8ae468 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -356,7 +356,7 @@ def test_caea_creation_should_fail(): @pytest.mark.live def test_caea_creation_live(populated_db): - caea = models.Caea.objects.get(pk=1) + caea = models.Caea.objects.first() assert len(str(caea.caea_code)) == 14 assert str(caea.period) == datetime.today().strftime("%Y%m") From 9c3a66c7c46a1b8536dfef8c7f71050162ec6543 Mon Sep 17 00:00:00 2001 From: erebodino <64107053+erebodino@users.noreply.github.com> Date: Sat, 15 Oct 2022 19:30:21 -0300 Subject: [PATCH 30/59] Update django_afip/models.py Added helptext to become more obvious Co-authored-by: Hugo --- django_afip/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/django_afip/models.py b/django_afip/models.py index d1c1c3d6..dcf573d9 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -763,6 +763,7 @@ class Caea(models.Model): generated = models.DateTimeField( _("generated"), + help_text=_("When this CAEA was generated"), ) final_date_inform = models.DateField( _("final_date_inform"), From 5947663da636fbdc54eec552308610e4483c7bf6 Mon Sep 17 00:00:00 2001 From: erebodino <64107053+erebodino@users.noreply.github.com> Date: Sat, 15 Oct 2022 19:30:52 -0300 Subject: [PATCH 31/59] Update django_afip/models.py Co-authored-by: Hugo --- django_afip/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/models.py b/django_afip/models.py index dcf573d9..cf9e091e 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -564,7 +564,7 @@ def get_caea( ticket: AuthTicket = None, ) -> Caea: """ - Get a CAEA code for the TaxPayer, if a CAEA alredy exists the check_response will raise and exception + Get a CAEA code for the TaxPayer, if a CAEA alredy exists the check_response will raise an exception Returns a caea object. """ From ff4ee100453d464e517728ce9f8c5ba8a55697e2 Mon Sep 17 00:00:00 2001 From: erebodino <64107053+erebodino@users.noreply.github.com> Date: Sat, 15 Oct 2022 19:35:12 -0300 Subject: [PATCH 32/59] Update django_afip/serializers.py Co-authored-by: Hugo --- django_afip/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/serializers.py b/django_afip/serializers.py index f7408802..dfbc96a1 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -217,7 +217,7 @@ def serialize_caea_period(period: int = None): return date.strftime("%Y%m") -def serialize_caea_order(order: int = None): +def serialize_caea_order(order: int = None) -> int: if order: return order else: From 198257ae39a224eb3690a4e91326a53fb2316bdf Mon Sep 17 00:00:00 2001 From: erebodino <64107053+erebodino@users.noreply.github.com> Date: Sat, 15 Oct 2022 20:37:27 -0300 Subject: [PATCH 33/59] Update django_afip/models.py Co-authored-by: Hugo --- django_afip/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/models.py b/django_afip/models.py index cf9e091e..a53d4814 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1755,7 +1755,7 @@ def save_pdf(self, save_model: bool = True) -> None: from django_afip.views import ReceiptPDFView if not self.receipt.is_validated: - if not "CAEA" in self.receipt.point_of_sales.issuance_type: + if "CAEA" not in self.receipt.point_of_sales.issuance_type: raise exceptions.DjangoAfipException( _("Cannot generate pdf for non-authorized receipt") ) From b1ecfaaf31572f76628625fa80b5f4b3e6217669 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 20:23:12 -0300 Subject: [PATCH 34/59] Added Unique Constraint to CAEA and refactor test --- ...ea_generated_alter_caea_unique_together.py | 22 +++++++++++++++++++ django_afip/models.py | 5 ++++- tests/test_models.py | 12 +++++----- 3 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py diff --git a/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py b/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py new file mode 100644 index 00000000..56864fcc --- /dev/null +++ b/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.8 on 2022-10-15 23:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0026_merge_20220922_2245'), + ] + + operations = [ + migrations.AlterField( + model_name='caea', + name='generated', + field=models.DateTimeField(help_text='When this CAEA was generated', verbose_name='generated'), + ), + migrations.AlterUniqueTogether( + name='caea', + unique_together={('order', 'period', 'taxpayer')}, + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index a53d4814..99d6524e 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1,5 +1,4 @@ from __future__ import annotations - import base64 import logging import os @@ -781,6 +780,10 @@ class Caea(models.Model): def __str__(self) -> str: return str(self.caea_code) + class Meta: + unique_together = ('order', 'period','taxpayer') + + class PointOfSales(models.Model): """ diff --git a/tests/test_models.py b/tests/test_models.py index 2e8ae468..3bf98b82 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch +import django import pytest from django.utils.timezone import make_aware @@ -403,16 +404,17 @@ def test_create_receipt_caea(): @pytest.mark.django_db -def test_receipt_with_two_caea_should_fail(): +def test_two_caea_unique_constraint_should_fail(): + """ + Test that 2 caea with same order,period and taxpayer cannot be created + """ pos = factories.PointOfSalesFactoryCaea() - # caea = models.Caea.objects.get(pk=1) caea = factories.CaeaFactory() - caea2 = factories.CaeaFactory(caea_code="12345678912346") with pytest.raises( - exceptions.CaeaCountError, + django.db.utils.IntegrityError, ): - receipt = factories.ReceiptFactory(point_of_sales=pos) + caea2 = factories.CaeaFactory(caea_code="12345678912346") @pytest.mark.django_db From 50f6b8915a94162990d069266b776f1a2033d522 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 20:25:28 -0300 Subject: [PATCH 35/59] Update aclarations on pdf.py, CAEA is now supported --- django_afip/pdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/pdf.py b/django_afip/pdf.py index 613ce4fa..1884d6b4 100644 --- a/django_afip/pdf.py +++ b/django_afip/pdf.py @@ -44,7 +44,7 @@ def __init__(self, receipt: Receipt): "ctz": float(receipt.currency_quote), "tipoDocRec": receipt.document_type.code, "nroDocRec": receipt.document_number, - "tipoCodAut": "E", # We don't support anything except CAE. + "tipoCodAut": "E", "codAut": authorization_code, } From ad53fa5a7279c86b85e21202bc9ac76079a5aa22 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sat, 15 Oct 2022 20:50:05 -0300 Subject: [PATCH 36/59] Ordering in Receipts fixed, test added to ensure that --- .../migrations/0028_alter_receipt_options.py | 17 +++++++++++++++++ django_afip/models.py | 3 +++ tests/test_models.py | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 django_afip/migrations/0028_alter_receipt_options.py diff --git a/django_afip/migrations/0028_alter_receipt_options.py b/django_afip/migrations/0028_alter_receipt_options.py new file mode 100644 index 00000000..8f55712a --- /dev/null +++ b/django_afip/migrations/0028_alter_receipt_options.py @@ -0,0 +1,17 @@ +# Generated by Django 4.0.8 on 2022-10-15 23:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0027_alter_caea_generated_alter_caea_unique_together'), + ] + + operations = [ + migrations.AlterModelOptions( + name='receipt', + options={'ordering': ('issued_date', 'point_of_sales_id', 'receipt_number', 'pk'), 'verbose_name': 'receipt', 'verbose_name_plural': 'receipts'}, + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 99d6524e..7dcad43a 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1614,6 +1614,9 @@ def __str__(self) -> str: class Meta: ordering = ( "issued_date", + "point_of_sales_id", + "receipt_number", + "pk" ) # this ordering return the same values for first(),last() when filter on 1 day verbose_name = _("receipt") verbose_name_plural = _("receipts") diff --git a/tests/test_models.py b/tests/test_models.py index 3bf98b82..765e1311 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -693,3 +693,13 @@ def test_caea_assigned_receipt_correct(): .count() == 2 ) + +@pytest.mark.django_db +@pytest.mark.este +def test_ordering_receipts_work(): + + receipt_1 = ReceiptFactory() + receipt_2 = ReceiptFactory() + receipt_3 = ReceiptFactory() + + assert models.Receipt.objects.last() == receipt_3 From 5c3d1a370acd99349433705c7e756e4ed2dc2e25 Mon Sep 17 00:00:00 2001 From: erebodino Date: Sun, 16 Oct 2022 11:17:42 -0300 Subject: [PATCH 37/59] Fixed CaeaFactory, not CAEA in _validate --- django_afip/factories.py | 11 ++++++++++- django_afip/models.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index 2c2e53a4..ee173ad8 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -22,6 +22,15 @@ def get_test_file(filename: str, mode="r") -> Path: path = Path(__file__).parent / "testing" / filename return path +def get_order_of_date()-> int: + today = datetime.now() + order = 1 + if today.day > 15: + order = 2 + return order + + + class UserFactory(DjangoModelFactory): class Meta: @@ -265,7 +274,7 @@ class Meta: caea_code = "12345678974125" period = datetime.today().strftime("%Y%m") - order = "1" + order = LazyFunction(get_order_of_date) valid_since = make_aware(datetime(2022, 6, 1)) expires = make_aware(datetime(2022, 6, 15)) generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) diff --git a/django_afip/models.py b/django_afip/models.py index 7dcad43a..64267bc8 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1146,7 +1146,7 @@ def _validate(self, ticket=None) -> list[str]: ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - if not "CAEA" in first.point_of_sales.issuance_type: + if "CAEA" not in first.point_of_sales.issuance_type: response = client.service.FECAESolicitar( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts(self), From cab7ba1a201041397e16b295c840b32721bf6cc0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 16 Oct 2022 14:34:12 +0000 Subject: [PATCH 38/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/factories.py | 5 ++--- ...caea_generated_alter_caea_unique_together.py | 17 ++++++++++------- .../migrations/0028_alter_receipt_options.py | 15 ++++++++++++--- django_afip/models.py | 6 +++--- tests/test_models.py | 3 ++- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index ee173ad8..8fc00334 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -22,15 +22,14 @@ def get_test_file(filename: str, mode="r") -> Path: path = Path(__file__).parent / "testing" / filename return path -def get_order_of_date()-> int: + +def get_order_of_date() -> int: today = datetime.now() order = 1 if today.day > 15: order = 2 return order - - class UserFactory(DjangoModelFactory): class Meta: diff --git a/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py b/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py index 56864fcc..d861f1f0 100644 --- a/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py +++ b/django_afip/migrations/0027_alter_caea_generated_alter_caea_unique_together.py @@ -1,22 +1,25 @@ # Generated by Django 4.0.8 on 2022-10-15 23:17 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('afip', '0026_merge_20220922_2245'), + ("afip", "0026_merge_20220922_2245"), ] operations = [ migrations.AlterField( - model_name='caea', - name='generated', - field=models.DateTimeField(help_text='When this CAEA was generated', verbose_name='generated'), + model_name="caea", + name="generated", + field=models.DateTimeField( + help_text="When this CAEA was generated", verbose_name="generated" + ), ), migrations.AlterUniqueTogether( - name='caea', - unique_together={('order', 'period', 'taxpayer')}, + name="caea", + unique_together={("order", "period", "taxpayer")}, ), ] diff --git a/django_afip/migrations/0028_alter_receipt_options.py b/django_afip/migrations/0028_alter_receipt_options.py index 8f55712a..566091da 100644 --- a/django_afip/migrations/0028_alter_receipt_options.py +++ b/django_afip/migrations/0028_alter_receipt_options.py @@ -6,12 +6,21 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0027_alter_caea_generated_alter_caea_unique_together'), + ("afip", "0027_alter_caea_generated_alter_caea_unique_together"), ] operations = [ migrations.AlterModelOptions( - name='receipt', - options={'ordering': ('issued_date', 'point_of_sales_id', 'receipt_number', 'pk'), 'verbose_name': 'receipt', 'verbose_name_plural': 'receipts'}, + name="receipt", + options={ + "ordering": ( + "issued_date", + "point_of_sales_id", + "receipt_number", + "pk", + ), + "verbose_name": "receipt", + "verbose_name_plural": "receipts", + }, ), ] diff --git a/django_afip/models.py b/django_afip/models.py index 64267bc8..e285bae2 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1,4 +1,5 @@ from __future__ import annotations + import base64 import logging import os @@ -781,8 +782,7 @@ def __str__(self) -> str: return str(self.caea_code) class Meta: - unique_together = ('order', 'period','taxpayer') - + unique_together = ("order", "period", "taxpayer") class PointOfSales(models.Model): @@ -1616,7 +1616,7 @@ class Meta: "issued_date", "point_of_sales_id", "receipt_number", - "pk" + "pk", ) # this ordering return the same values for first(),last() when filter on 1 day verbose_name = _("receipt") verbose_name_plural = _("receipts") diff --git a/tests/test_models.py b/tests/test_models.py index 765e1311..73415279 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock from unittest.mock import call from unittest.mock import patch -import django +import django import pytest from django.utils.timezone import make_aware from pytest_django.asserts import assertQuerysetEqual @@ -694,6 +694,7 @@ def test_caea_assigned_receipt_correct(): == 2 ) + @pytest.mark.django_db @pytest.mark.este def test_ordering_receipts_work(): From dc9fa8699ca2af45b5cd6252cd9930725341f30f Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 09:22:18 -0300 Subject: [PATCH 39/59] Uncomment validation and receipt_validation on views --- django_afip/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/django_afip/views.py b/django_afip/views.py index 2c59c6dc..b5b8cebf 100644 --- a/django_afip/views.py +++ b/django_afip/views.py @@ -69,7 +69,7 @@ def get_context_for_pk(pk, *args, **kwargs): "receipt", "receipt__receipt_type", "receipt__document_type", - # "receipt__validation", + "receipt__validation", "receipt__point_of_sales", "receipt__point_of_sales__owner", ) @@ -86,7 +86,7 @@ def get_context_for_pk(pk, *args, **kwargs): models.Receipt.objects.select_related( "receipt_type", "document_type", - # "validation", + "validation", "point_of_sales", "point_of_sales__owner", ) From 1735e0138151a38c3b6777e79e17e47c6599b271 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 09:56:39 -0300 Subject: [PATCH 40/59] Rename get_cae for request_new_cae, rename consult_caea for fetch_caea, include a new method fetch_or_create_caea. Refactor conftest to reflect the changes. --- django_afip/models.py | 12 ++++++++++-- tests/conftest.py | 6 ++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index e285bae2..a4ddb016 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -557,7 +557,7 @@ def fetch_points_of_sales( return results - def get_caea( + def request_new_caea( self, period: int = None, order: int = None, @@ -597,7 +597,7 @@ def get_caea( ) return caea - def consult_caea( + def fetch_caea( self, period: str = None, order: int = None, @@ -640,6 +640,13 @@ def consult_caea( ) return caea + + def fetch_or_create_caea(self, period:int, order:int): + try: + self.request_new_caea(period=period, order=order) + except: + self.fetch_caea(period=period, order=order) + def _inform_caea_without_operations( self, @@ -1446,6 +1453,7 @@ class Receipt(models.Model): generated = models.DateTimeField( _("Time when the receipt was created"), auto_now_add=True, + ) caea = models.ForeignKey( diff --git a/tests/conftest.py b/tests/conftest.py index 0a386fa3..ca36245c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,7 +109,5 @@ def populated_db(live_ticket, live_taxpayer): order = 2 else: order = 1 - try: - live_taxpayer.get_caea(period=period, order=order) - except: - live_taxpayer.consult_caea(period=period, order=order) + + live_taxpayer.fetch_or_create_caea(period=period, order=order) From 71f3243b89dda7e989452e223d24033fb4e7e8b8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:57:19 +0000 Subject: [PATCH 41/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 6 ++---- tests/conftest.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index a4ddb016..38282ab1 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -640,14 +640,13 @@ def fetch_caea( ) return caea - - def fetch_or_create_caea(self, period:int, order:int): + + def fetch_or_create_caea(self, period: int, order: int): try: self.request_new_caea(period=period, order=order) except: self.fetch_caea(period=period, order=order) - def _inform_caea_without_operations( self, pos: PointOfSales, @@ -1453,7 +1452,6 @@ class Receipt(models.Model): generated = models.DateTimeField( _("Time when the receipt was created"), auto_now_add=True, - ) caea = models.ForeignKey( diff --git a/tests/conftest.py b/tests/conftest.py index ca36245c..7446a1c3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,5 +109,5 @@ def populated_db(live_ticket, live_taxpayer): order = 2 else: order = 1 - + live_taxpayer.fetch_or_create_caea(period=period, order=order) From fed9a7c90183c34b93ce1858ee2bd64ecf5d3cd8 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 11:45:27 -0300 Subject: [PATCH 42/59] Refactor consult_caea and _inform_caea,methods moved from TaxPayer to Caea --- django_afip/models.py | 142 +++++++++++++++++++++--------------------- tests/test_models.py | 6 +- 2 files changed, 73 insertions(+), 75 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 38282ab1..c14de6a6 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -647,77 +647,6 @@ def fetch_or_create_caea(self, period: int, order: int): except: self.fetch_caea(period=period, order=order) - def _inform_caea_without_operations( - self, - pos: PointOfSales, - caea: Caea, - ticket: AuthTicket = None, - ) -> InformedCaeas: - """ - Inform to AFIP that the PointOfSales and CAEA passed have not any movement between the duration of the CAEA - """ - ticket = ticket or self.get_or_create_ticket("wsfe") - - client = clients.get_client("wsfe", self.is_sandboxed) - response = client.service.FECAEASinMovimientoInformar( - serializers.serialize_ticket(ticket), - PtoVta=pos.number, - CAEA=caea.caea_code, - ) - - check_response( - response - ) # be aware that this func raise an error if it's present - registry = InformedCaeas.objects.create( - pos=pos, - caea=caea, - processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), - ) - return registry - - def consult_caea_without_operations( - self, - pos: PointOfSales, - caea: Caea, - ticket: AuthTicket = None, - ) -> InformedCaeas or None: - """ - Consult the state of the CAEA with AFIP, if the consult raise an error (probably CAEA without movement was never informed) - the method handle this an inform to AFIP the CAEA and POS - """ - - try: - registry = InformedCaeas.objects.get(pos=pos, caea=caea) - return registry - except InformedCaeas.DoesNotExist: - registry = None - - ticket = ticket or self.get_or_create_ticket("wsfe") - - client = clients.get_client("wsfe", self.is_sandboxed) - response = client.service.FECAEASinMovimientoConsultar( - serializers.serialize_ticket(ticket), - PtoVta=pos.number, - CAEA=caea.caea_code, - ) - try: - check_response( - response - ) # be aware that this func raise an error if it's present - - # if for some reason a CAEA code was informed into AFIP DB but we have not a InformedCAEA this solve that - registry = InformedCaeas.objects.create( - pos=pos, - caea=caea, - processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), - ) - return registry - except exceptions.AfipException: - registry = self._inform_caea_without_operations( - pos=pos, caea=caea, ticket=ticket - ) - return registry - def __repr__(self) -> str: return "".format( self.pk, @@ -791,6 +720,77 @@ class Meta: unique_together = ("order", "period", "taxpayer") + + def _inform_caea_without_operations( + self, + pos: PointOfSales, + ticket: AuthTicket = None, + ) -> InformedCaeas: + """ + Inform to AFIP that the PointOfSales and CAEA passed have not any movement between the duration of the CAEA + """ + ticket = ticket or pos.owner.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", pos.owner.is_sandboxed) + response = client.service.FECAEASinMovimientoInformar( + serializers.serialize_ticket(ticket), + PtoVta=pos.number, + CAEA=self.caea_code, + ) + + check_response( + response + ) # be aware that this func raise an error if it's present + registry = InformedCaeas.objects.create( + pos=pos, + caea=self, + processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), + ) + return registry + + def consult_caea_without_operations( + self, + pos: PointOfSales, + ticket: AuthTicket = None, + ) -> InformedCaeas or None: + """ + Consult the state of the CAEA with AFIP, if the consult raise an error (probably CAEA without movement was never informed) + the method handle this an inform to AFIP the CAEA and POS + """ + + try: + registry = InformedCaeas.objects.get(pos=pos, caea=self.caea_code) + return registry + except InformedCaeas.DoesNotExist: + registry = None + + ticket = ticket or pos.owner.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", pos.owner.is_sandboxed) + response = client.service.FECAEASinMovimientoConsultar( + serializers.serialize_ticket(ticket), + PtoVta=pos.number, + CAEA=self.caea_code, + ) + try: + check_response( + response + ) # be aware that this func raise an error if it's present + + # if for some reason a CAEA code was informed into AFIP DB but we have not a InformedCAEA this solve that + registry = InformedCaeas.objects.create( + pos=pos, + caea=self, + processed_date=datetime.strptime(response.FchProceso, "%Y%m%d").date(), + ) + return registry + except exceptions.AfipException: + registry = self._inform_caea_without_operations( + pos=pos, ticket=ticket + ) + return registry + + class PointOfSales(models.Model): """ Represents an existing AFIP point of sale. diff --git a/tests/test_models.py b/tests/test_models.py index 73415279..3d679888 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -549,7 +549,6 @@ def test_validate_credit_note_caea(populated_db): assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED - @pytest.mark.django_db @pytest.mark.live def test_inform_caea_without_movement(populated_db): @@ -557,10 +556,9 @@ def test_inform_caea_without_movement(populated_db): caea = models.Caea.objects.get(pk=1) payer = factories.TaxPayerFactory() - resp = payer.consult_caea_without_operations(pos=pos, caea=caea) + resp = caea.consult_caea_without_operations(pos=pos) assert isinstance(resp, models.InformedCaeas) - @pytest.mark.django_db @pytest.mark.live def test_creation_informedcaea(populated_db): @@ -573,7 +571,7 @@ def test_creation_informedcaea(populated_db): ): informed_caea = models.InformedCaeas.objects.get(pos=pos, caea=caea) - payer.consult_caea_without_operations(pos=pos, caea=caea) + caea.consult_caea_without_operations(pos=pos) informed_caea = models.InformedCaeas.objects.get(pos=pos, caea=caea) assert informed_caea.pk == 1 assert informed_caea.caea == caea From bfcd5bab87df3c69e1cdbc2d0a7344a5558d1e2f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 14:54:57 +0000 Subject: [PATCH 43/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 6 +----- tests/test_models.py | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index c14de6a6..0df7b200 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -719,8 +719,6 @@ def __str__(self) -> str: class Meta: unique_together = ("order", "period", "taxpayer") - - def _inform_caea_without_operations( self, pos: PointOfSales, @@ -785,9 +783,7 @@ def consult_caea_without_operations( ) return registry except exceptions.AfipException: - registry = self._inform_caea_without_operations( - pos=pos, ticket=ticket - ) + registry = self._inform_caea_without_operations(pos=pos, ticket=ticket) return registry diff --git a/tests/test_models.py b/tests/test_models.py index 3d679888..70be82c0 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -549,6 +549,7 @@ def test_validate_credit_note_caea(populated_db): assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED + @pytest.mark.django_db @pytest.mark.live def test_inform_caea_without_movement(populated_db): @@ -559,6 +560,7 @@ def test_inform_caea_without_movement(populated_db): resp = caea.consult_caea_without_operations(pos=pos) assert isinstance(resp, models.InformedCaeas) + @pytest.mark.django_db @pytest.mark.live def test_creation_informedcaea(populated_db): From 41667ddf1e60911f6ff2b90d5de5f7370372e385 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 12:28:38 -0300 Subject: [PATCH 44/59] Refactor _validate from ReceiptQuerySet --- django_afip/models.py | 158 +++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 73 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 0df7b200..669368c7 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1142,89 +1142,101 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: return qs._validate(ticket) - def _validate(self, ticket=None) -> list[str]: - first = self.first() - assert first is not None # should never happen; mostly a hint for mypy - ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") - client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) - - if "CAEA" not in first.point_of_sales.issuance_type: - response = client.service.FECAESolicitar( + def _validate_with_cae(self,client,ticket): + """ + A helper method to validate the Receipt made with CAE + """ + response = client.service.FECAESolicitar( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts(self), ) - check_response(response) - errs = [] - for cae_data in response.FeDetResp.FECAEDetResponse: - if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: - validation = ReceiptValidation.objects.create( - result=cae_data.Resultado, - cae=cae_data.CAE, - cae_expiration=parsers.parse_date(cae_data.CAEFchVto), - receipt=self.get( - receipt_number=cae_data.CbteDesde, - ), - processed_date=parsers.parse_datetime( - response.FeCabResp.FchProceso, - ), - ) - if cae_data.Observaciones: - for obs in cae_data.Observaciones.Obs: - observation = Observation.objects.create( - code=obs.Code, - message=obs.Msg, - ) - validation.observations.add(observation) - elif cae_data.Observaciones: + check_response(response) + errs = [] + for cae_data in response.FeDetResp.FECAEDetResponse: + if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: + validation = ReceiptValidation.objects.create( + result=cae_data.Resultado, + cae=cae_data.CAE, + cae_expiration=parsers.parse_date(cae_data.CAEFchVto), + receipt=self.get( + receipt_number=cae_data.CbteDesde, + ), + processed_date=parsers.parse_datetime( + response.FeCabResp.FchProceso, + ), + ) + if cae_data.Observaciones: for obs in cae_data.Observaciones.Obs: - errs.append( - "Error {}: {}".format( - obs.Code, - parsers.parse_string(obs.Msg), - ) + observation = Observation.objects.create( + code=obs.Code, + message=obs.Msg, ) - - # Remove the number from ones that failed to validate: - self.filter(validation__isnull=True).update(receipt_number=None) - return errs - - else: - response = client.service.FECAEARegInformativo( + validation.observations.add(observation) + elif cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + errs.append( + "Error {}: {}".format( + obs.Code, + parsers.parse_string(obs.Msg), + ) + ) + # Remove the number from ones that failed to validate: + self.filter(validation__isnull=True).update(receipt_number=None) + return errs + + def _validate_with_caea(self,client,ticket): + """ + A helper method to validate the Receipt made with CAEA + """ + response = client.service.FECAEARegInformativo( serializers.serialize_ticket(ticket), serializers.serialize_multiple_receipts_caea(self), ) - check_response(response) - errs = [] - for cae_data in response.FeDetResp.FECAEADetResponse: - if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: - validation = ReceiptValidation.objects.create( - result=cae_data.Resultado, - cae=cae_data.CAEA, - # cae_expiration=parsers.parse_date(self.caea.expires), - receipt=self.get( - receipt_number=cae_data.CbteDesde, - ), - processed_date=parsers.parse_datetime( - response.FeCabResp.FchProceso, - ), - caea=True, - ) - if cae_data.Observaciones: - for obs in cae_data.Observaciones.Obs: - observation = Observation.objects.create( - code=obs.Code, - message=obs.Msg, - ) - validation.observations.add(observation) - elif cae_data.Observaciones: + check_response(response) + errs = [] + for cae_data in response.FeDetResp.FECAEADetResponse: + if cae_data.Resultado == ReceiptValidation.RESULT_APPROVED: + validation = ReceiptValidation.objects.create( + result=cae_data.Resultado, + cae=cae_data.CAEA, + # cae_expiration=parsers.parse_date(self.caea.expires), + receipt=self.get( + receipt_number=cae_data.CbteDesde, + ), + processed_date=parsers.parse_datetime( + response.FeCabResp.FchProceso, + ), + caea=True, + ) + if cae_data.Observaciones: for obs in cae_data.Observaciones.Obs: - errs.append( - "Error {}: {}".format( - obs.Code, - parsers.parse_string(obs.Msg), - ) + observation = Observation.objects.create( + code=obs.Code, + message=obs.Msg, + ) + validation.observations.add(observation) + elif cae_data.Observaciones: + for obs in cae_data.Observaciones.Obs: + errs.append( + "Error {}: {}".format( + obs.Code, + parsers.parse_string(obs.Msg), ) - return errs + ) + return errs + + def _validate(self, ticket=None) -> list[str]: + first = self.first() + assert first is not None # should never happen; mostly a hint for mypy + ticket = ticket or first.point_of_sales.owner.get_or_create_ticket("wsfe") + client = clients.get_client("wsfe", first.point_of_sales.owner.is_sandboxed) + + errs = [] + if "CAEA" not in first.point_of_sales.issuance_type: + errs = self._validate_with_cae(ticket=ticket,client=client) + else: + errs = self._validate_with_caea(ticket=ticket,client=client) + return errs class ReceiptManager(models.Manager): From 8c40a08e1de2153762804efa913db419555311ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:31:23 +0000 Subject: [PATCH 45/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 669368c7..dd7966b5 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1142,14 +1142,14 @@ def validate(self, ticket: AuthTicket = None) -> list[str]: return qs._validate(ticket) - def _validate_with_cae(self,client,ticket): + def _validate_with_cae(self, client, ticket): """ A helper method to validate the Receipt made with CAE """ response = client.service.FECAESolicitar( - serializers.serialize_ticket(ticket), - serializers.serialize_multiple_receipts(self), - ) + serializers.serialize_ticket(ticket), + serializers.serialize_multiple_receipts(self), + ) check_response(response) errs = [] for cae_data in response.FeDetResp.FECAEDetResponse: @@ -1183,15 +1183,15 @@ def _validate_with_cae(self,client,ticket): # Remove the number from ones that failed to validate: self.filter(validation__isnull=True).update(receipt_number=None) return errs - - def _validate_with_caea(self,client,ticket): + + def _validate_with_caea(self, client, ticket): """ A helper method to validate the Receipt made with CAEA """ response = client.service.FECAEARegInformativo( - serializers.serialize_ticket(ticket), - serializers.serialize_multiple_receipts_caea(self), - ) + serializers.serialize_ticket(ticket), + serializers.serialize_multiple_receipts_caea(self), + ) check_response(response) errs = [] for cae_data in response.FeDetResp.FECAEADetResponse: @@ -1233,9 +1233,9 @@ def _validate(self, ticket=None) -> list[str]: errs = [] if "CAEA" not in first.point_of_sales.issuance_type: - errs = self._validate_with_cae(ticket=ticket,client=client) + errs = self._validate_with_cae(ticket=ticket, client=client) else: - errs = self._validate_with_caea(ticket=ticket,client=client) + errs = self._validate_with_caea(ticket=ticket, client=client) return errs From 1524dce6e04d84ab2a3ce689b7fa99b5904dda5b Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 12:33:05 -0300 Subject: [PATCH 46/59] Added null=True to -generated- attribute from Receipt model --- .../migrations/0029_alter_receipt_generated.py | 18 ++++++++++++++++++ django_afip/models.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 django_afip/migrations/0029_alter_receipt_generated.py diff --git a/django_afip/migrations/0029_alter_receipt_generated.py b/django_afip/migrations/0029_alter_receipt_generated.py new file mode 100644 index 00000000..7619ca54 --- /dev/null +++ b/django_afip/migrations/0029_alter_receipt_generated.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-10-17 15:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0028_alter_receipt_options'), + ] + + operations = [ + migrations.AlterField( + model_name='receipt', + name='generated', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Time when the receipt was created'), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index dd7966b5..e12a23ad 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1460,6 +1460,7 @@ class Receipt(models.Model): generated = models.DateTimeField( _("Time when the receipt was created"), auto_now_add=True, + null=True, ) caea = models.ForeignKey( From e5428981f589e08ddfb09fa73e1a882382c4f53d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:34:16 +0000 Subject: [PATCH 47/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../migrations/0029_alter_receipt_generated.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/django_afip/migrations/0029_alter_receipt_generated.py b/django_afip/migrations/0029_alter_receipt_generated.py index 7619ca54..8243cadd 100644 --- a/django_afip/migrations/0029_alter_receipt_generated.py +++ b/django_afip/migrations/0029_alter_receipt_generated.py @@ -1,18 +1,23 @@ # Generated by Django 4.1.1 on 2022-10-17 15:30 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('afip', '0028_alter_receipt_options'), + ("afip", "0028_alter_receipt_options"), ] operations = [ migrations.AlterField( - model_name='receipt', - name='generated', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Time when the receipt was created'), + model_name="receipt", + name="generated", + field=models.DateTimeField( + auto_now_add=True, + null=True, + verbose_name="Time when the receipt was created", + ), ), ] From 91cc5af8be1ed9b74798c93fcf39248cc1b23e73 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 12:43:09 -0300 Subject: [PATCH 48/59] Rename final_date_inform attibute to report_deadline from Caea model --- django_afip/factories.py | 2 +- ..._final_date_inform_caea_report_deadline.py | 24 +++++++++++++++++++ django_afip/models.py | 9 +++---- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py diff --git a/django_afip/factories.py b/django_afip/factories.py index 8fc00334..58d37960 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -277,7 +277,7 @@ class Meta: valid_since = make_aware(datetime(2022, 6, 1)) expires = make_aware(datetime(2022, 6, 15)) generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) - final_date_inform = make_aware(datetime(2022, 6, 20)) + report_deadline = make_aware(datetime(2022, 6, 20)) taxpayer = SubFactory(TaxPayerFactory) active = True diff --git a/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py b/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py new file mode 100644 index 00000000..6b35a7c8 --- /dev/null +++ b/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.1 on 2022-10-17 15:40 + +import datetime +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0029_alter_receipt_generated'), + ] + + operations = [ + migrations.RemoveField( + model_name='caea', + name='final_date_inform', + ), + migrations.AddField( + model_name='caea', + name='report_deadline', + field=models.DateField(default=datetime.datetime(2022, 10, 17, 15, 40, 36, 849667), help_text='Activities for this CAEA must be informed before this date', verbose_name='report deadline'), + preserve_default=False, + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index e12a23ad..6088bb17 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -588,7 +588,7 @@ def request_new_caea( valid_since=parsers.parse_date(response.ResultGet.FchVigDesde), expires=parsers.parse_date(response.ResultGet.FchVigHasta), generated=parsers.parse_datetime(response.ResultGet.FchProceso), - final_date_inform=parsers.parse_date(response.ResultGet.FchTopeInf), + report_deadline=parsers.parse_date(response.ResultGet.FchTopeInf), taxpayer=self, active=caea_is_active( valid_since=response.ResultGet.FchVigDesde, @@ -627,7 +627,7 @@ def fetch_caea( "valid_since": parsers.parse_date(response.ResultGet.FchVigDesde), "expires": parsers.parse_date(response.ResultGet.FchVigHasta), "generated": parsers.parse_datetime(response.ResultGet.FchProceso), - "final_date_inform": parsers.parse_date(response.ResultGet.FchTopeInf), + "report_deadline": parsers.parse_date(response.ResultGet.FchTopeInf), "taxpayer": self, "active": caea_is_active( valid_since=response.ResultGet.FchVigDesde, @@ -700,8 +700,9 @@ class Caea(models.Model): _("generated"), help_text=_("When this CAEA was generated"), ) - final_date_inform = models.DateField( - _("final_date_inform"), + report_deadline = models.DateField( + _("report deadline"), + help_text=_("Activities for this CAEA must be informed before this date"), ) taxpayer = models.ForeignKey( From 2bcd233b8e94536791660bf4c53865f6accf8ee9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 15:43:55 +0000 Subject: [PATCH 49/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ..._final_date_inform_caea_report_deadline.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py b/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py index 6b35a7c8..e5d527e1 100644 --- a/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py +++ b/django_afip/migrations/0030_remove_caea_final_date_inform_caea_report_deadline.py @@ -1,24 +1,30 @@ # Generated by Django 4.1.1 on 2022-10-17 15:40 import datetime -from django.db import migrations, models + +from django.db import migrations +from django.db import models class Migration(migrations.Migration): dependencies = [ - ('afip', '0029_alter_receipt_generated'), + ("afip", "0029_alter_receipt_generated"), ] operations = [ migrations.RemoveField( - model_name='caea', - name='final_date_inform', + model_name="caea", + name="final_date_inform", ), migrations.AddField( - model_name='caea', - name='report_deadline', - field=models.DateField(default=datetime.datetime(2022, 10, 17, 15, 40, 36, 849667), help_text='Activities for this CAEA must be informed before this date', verbose_name='report deadline'), + model_name="caea", + name="report_deadline", + field=models.DateField( + default=datetime.datetime(2022, 10, 17, 15, 40, 36, 849667), + help_text="Activities for this CAEA must be informed before this date", + verbose_name="report deadline", + ), preserve_default=False, ), ] From 4f2e04e7dcebd55d7864b9364c1c43787cc98f27 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 20:32:46 -0300 Subject: [PATCH 50/59] Moved active field of Caea to a property, create custom queryset tofilter actives caeas. Refactor tests,signal and factories --- django_afip/factories.py | 34 ++++++++++- .../migrations/0031_alter_caea_managers.py | 20 +++++++ ..._alter_caea_managers_remove_caea_active.py | 22 +++++++ django_afip/models.py | 28 +++++---- django_afip/signals.py | 3 +- tests/test_models.py | 58 +++++++++++++++++-- 6 files changed, 144 insertions(+), 21 deletions(-) create mode 100644 django_afip/migrations/0031_alter_caea_managers.py create mode 100644 django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py diff --git a/django_afip/factories.py b/django_afip/factories.py index 58d37960..7010d85f 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -15,6 +15,7 @@ from factory.django import ImageField from django_afip import models +import calendar def get_test_file(filename: str, mode="r") -> Path: @@ -24,12 +25,40 @@ def get_test_file(filename: str, mode="r") -> Path: def get_order_of_date() -> int: + """ + Helper method to detect if the day of the month + corresponds to the first quarter (1) or the second (2) + """ today = datetime.now() order = 1 if today.day > 15: order = 2 return order +def valid_since_caea(): + """ + Helper method to assign the valid_since field from Caea model + to the correspondent year,month,day in the quarter + """ + order = get_order_of_date() + valid_since = datetime(datetime.now().year, datetime.now().month, 1) + if order == 2: + valid_since = datetime(datetime.now().year, datetime.now().month, 16) + return valid_since + +def expires_caea(): + """ + Helper method to assign the expires field from Caea model + to the correspondent year,month,day in the quarter + """ + order = get_order_of_date() + expires = datetime(datetime.now().year, datetime.now().month, 15) + if order == 2: + final = calendar.monthrange(datetime.now().year, datetime.now().month)[1] + expires = datetime(datetime.now().year, datetime.now().month,final) + return expires + + class UserFactory(DjangoModelFactory): class Meta: @@ -274,12 +303,11 @@ class Meta: caea_code = "12345678974125" period = datetime.today().strftime("%Y%m") order = LazyFunction(get_order_of_date) - valid_since = make_aware(datetime(2022, 6, 1)) - expires = make_aware(datetime(2022, 6, 15)) + valid_since = LazyFunction(valid_since_caea) + expires = LazyFunction(expires_caea) generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) report_deadline = make_aware(datetime(2022, 6, 20)) taxpayer = SubFactory(TaxPayerFactory) - active = True class ReceiptEntryFactory(DjangoModelFactory): diff --git a/django_afip/migrations/0031_alter_caea_managers.py b/django_afip/migrations/0031_alter_caea_managers.py new file mode 100644 index 00000000..445478a2 --- /dev/null +++ b/django_afip/migrations/0031_alter_caea_managers.py @@ -0,0 +1,20 @@ +# Generated by Django 4.0.8 on 2022-10-17 22:35 + +from django.db import migrations +import django.db.models.manager + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0030_remove_caea_final_date_inform_caea_report_deadline'), + ] + + operations = [ + migrations.AlterModelManagers( + name='caea', + managers=[ + ('actives_caea', django.db.models.manager.Manager()), + ], + ), + ] diff --git a/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py b/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py new file mode 100644 index 00000000..d71f9a7a --- /dev/null +++ b/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.1 on 2022-10-17 23:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0031_alter_caea_managers'), + ] + + operations = [ + migrations.AlterModelManagers( + name='caea', + managers=[ + ], + ), + migrations.RemoveField( + model_name='caea', + name='active', + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 6088bb17..47a6e577 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -589,12 +589,8 @@ def request_new_caea( expires=parsers.parse_date(response.ResultGet.FchVigHasta), generated=parsers.parse_datetime(response.ResultGet.FchProceso), report_deadline=parsers.parse_date(response.ResultGet.FchTopeInf), - taxpayer=self, - active=caea_is_active( - valid_since=response.ResultGet.FchVigDesde, - valid_to=response.ResultGet.FchVigHasta, - ), - ) + taxpayer=self + ) return caea def fetch_caea( @@ -628,11 +624,7 @@ def fetch_caea( "expires": parsers.parse_date(response.ResultGet.FchVigHasta), "generated": parsers.parse_datetime(response.ResultGet.FchProceso), "report_deadline": parsers.parse_date(response.ResultGet.FchTopeInf), - "taxpayer": self, - "active": caea_is_active( - valid_since=response.ResultGet.FchVigDesde, - valid_to=response.ResultGet.FchVigHasta, - ), + "taxpayer": self } caea = Caea.objects.update_or_create( @@ -661,6 +653,10 @@ class Meta: verbose_name = _("taxpayer") verbose_name_plural = _("taxpayers") +class CaeaQuerySet(models.QuerySet): + def active(self): + today = datetime.today() + return self.filter(Q(valid_since__lte=today),Q(expires__gte=today)) class Caea(models.Model): """Represents a CAEA code to continue operating when AFIP is offline. @@ -712,7 +708,15 @@ class Caea(models.Model): on_delete=models.CASCADE, ) - active = models.BooleanField(default=False) + objects = CaeaQuerySet.as_manager() + + @property + def caea_is_active(self)-> bool: + today = datetime.today() + if self.valid_since <= today <= self.expires: + return True + else: + return False def __str__(self) -> str: return str(self.caea_code) diff --git a/django_afip/signals.py b/django_afip/signals.py index ed7c385c..a3269ea7 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -32,8 +32,7 @@ def save_caea_data(sender, instance: models.TaxPayer, **kwargs): order = 1 if date.day > 15: order = 2 - caea = models.Caea.objects.all().filter( - active=True, + caea = models.Caea.objects.active().filter( taxpayer=instance.point_of_sales.owner, period=int(period), order=order, diff --git a/tests/test_models.py b/tests/test_models.py index 70be82c0..9818ccd6 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -638,7 +638,6 @@ def test_receipt_entry_gt_total_discount(): @pytest.mark.django_db -@pytest.mark.este def test_bad_retrive_caea(): """ Test that in the way that the CAEA is assigned in the save signal even if @@ -666,7 +665,6 @@ def test_bad_retrive_caea(): @pytest.mark.django_db -@pytest.mark.este def test_caea_assigned_receipt_correct(): """ Test that even if a taxpayer has multiples actives CAEAs the correct will be assigned @@ -689,14 +687,13 @@ def test_caea_assigned_receipt_correct(): assert caea_bad.caea_code != receipt.caea.caea_code assert ( models.Caea.objects.all() - .filter(active=True, taxpayer=receipt.point_of_sales.owner) + .filter(taxpayer=receipt.point_of_sales.owner) .count() == 2 ) @pytest.mark.django_db -@pytest.mark.este def test_ordering_receipts_work(): receipt_1 = ReceiptFactory() @@ -704,3 +701,56 @@ def test_ordering_receipts_work(): receipt_3 = ReceiptFactory() assert models.Receipt.objects.last() == receipt_3 + +@pytest.mark.django_db +def test_isactive_caea(): + + caea = CaeaFactory( + period = datetime.today().strftime("%Y%m"), + order = 1, + valid_since = datetime(2022, 10, 1), + expires = datetime(2022, 10, 15), + ) + + caea_2 = CaeaFactory( + caea_code = "12345678974128", + period = datetime.today().strftime("%Y%m"), + order = 2, + valid_since = datetime(2022, 10, 16), + expires = datetime(2022, 10, 31), + ) + + assert caea.valid_since <= datetime(2022,10,1) #Should be true + assert caea.expires <= datetime(2022,10,15) #Should be true + + assert caea.valid_since <= datetime(2022,10,15) <= caea.expires #Should be true + assert caea.valid_since <= datetime(2022,10,1) <= caea.expires #Should be true + + assert caea.valid_since > datetime(2022,9,30) #Should be true + assert caea.expires < datetime(2022,10,16) #Should be true + + assert not caea.caea_is_active + assert caea_2.caea_is_active + +@pytest.mark.django_db +def test_caea_queryset(): + + caea = CaeaFactory( + valid_since = datetime(2022, 10, 16), + expires = datetime(2022, 10, 31), + ) + + caea_2 = CaeaFactory( + caea_code = "12345678974128", + period = datetime.today().strftime("%Y%m"), + order = 1, + valid_since = datetime(2022, 8, 16), + expires = datetime(2022, 8, 31), + ) + caea_active = models.Caea.objects.first() + caea_active_count = models.Caea.objects.active().count() + + assert not caea_2.caea_is_active + assert caea.caea_is_active + assert caea == caea_active + assert caea_active_count == 1 From 1e0ccc47cabe81dd973b5e54a9cd94297d4a128e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:33:30 +0000 Subject: [PATCH 51/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/factories.py | 9 ++-- .../migrations/0031_alter_caea_managers.py | 8 +-- ..._alter_caea_managers_remove_caea_active.py | 11 ++-- django_afip/models.py | 12 +++-- tests/test_models.py | 54 +++++++++---------- 5 files changed, 48 insertions(+), 46 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index 7010d85f..f58bf2a9 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -1,3 +1,4 @@ +import calendar from datetime import date from datetime import datetime from pathlib import Path @@ -15,7 +16,6 @@ from factory.django import ImageField from django_afip import models -import calendar def get_test_file(filename: str, mode="r") -> Path: @@ -26,7 +26,7 @@ def get_test_file(filename: str, mode="r") -> Path: def get_order_of_date() -> int: """ - Helper method to detect if the day of the month + Helper method to detect if the day of the month corresponds to the first quarter (1) or the second (2) """ today = datetime.now() @@ -35,6 +35,7 @@ def get_order_of_date() -> int: order = 2 return order + def valid_since_caea(): """ Helper method to assign the valid_since field from Caea model @@ -46,6 +47,7 @@ def valid_since_caea(): valid_since = datetime(datetime.now().year, datetime.now().month, 16) return valid_since + def expires_caea(): """ Helper method to assign the expires field from Caea model @@ -55,11 +57,10 @@ def expires_caea(): expires = datetime(datetime.now().year, datetime.now().month, 15) if order == 2: final = calendar.monthrange(datetime.now().year, datetime.now().month)[1] - expires = datetime(datetime.now().year, datetime.now().month,final) + expires = datetime(datetime.now().year, datetime.now().month, final) return expires - class UserFactory(DjangoModelFactory): class Meta: model = User diff --git a/django_afip/migrations/0031_alter_caea_managers.py b/django_afip/migrations/0031_alter_caea_managers.py index 445478a2..1af67139 100644 --- a/django_afip/migrations/0031_alter_caea_managers.py +++ b/django_afip/migrations/0031_alter_caea_managers.py @@ -1,20 +1,20 @@ # Generated by Django 4.0.8 on 2022-10-17 22:35 -from django.db import migrations import django.db.models.manager +from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('afip', '0030_remove_caea_final_date_inform_caea_report_deadline'), + ("afip", "0030_remove_caea_final_date_inform_caea_report_deadline"), ] operations = [ migrations.AlterModelManagers( - name='caea', + name="caea", managers=[ - ('actives_caea', django.db.models.manager.Manager()), + ("actives_caea", django.db.models.manager.Manager()), ], ), ] diff --git a/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py b/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py index d71f9a7a..f2902050 100644 --- a/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py +++ b/django_afip/migrations/0032_alter_caea_managers_remove_caea_active.py @@ -6,17 +6,16 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0031_alter_caea_managers'), + ("afip", "0031_alter_caea_managers"), ] operations = [ migrations.AlterModelManagers( - name='caea', - managers=[ - ], + name="caea", + managers=[], ), migrations.RemoveField( - model_name='caea', - name='active', + model_name="caea", + name="active", ), ] diff --git a/django_afip/models.py b/django_afip/models.py index 47a6e577..6ba09bfe 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -589,8 +589,8 @@ def request_new_caea( expires=parsers.parse_date(response.ResultGet.FchVigHasta), generated=parsers.parse_datetime(response.ResultGet.FchProceso), report_deadline=parsers.parse_date(response.ResultGet.FchTopeInf), - taxpayer=self - ) + taxpayer=self, + ) return caea def fetch_caea( @@ -624,7 +624,7 @@ def fetch_caea( "expires": parsers.parse_date(response.ResultGet.FchVigHasta), "generated": parsers.parse_datetime(response.ResultGet.FchProceso), "report_deadline": parsers.parse_date(response.ResultGet.FchTopeInf), - "taxpayer": self + "taxpayer": self, } caea = Caea.objects.update_or_create( @@ -653,10 +653,12 @@ class Meta: verbose_name = _("taxpayer") verbose_name_plural = _("taxpayers") + class CaeaQuerySet(models.QuerySet): def active(self): today = datetime.today() - return self.filter(Q(valid_since__lte=today),Q(expires__gte=today)) + return self.filter(Q(valid_since__lte=today), Q(expires__gte=today)) + class Caea(models.Model): """Represents a CAEA code to continue operating when AFIP is offline. @@ -711,7 +713,7 @@ class Caea(models.Model): objects = CaeaQuerySet.as_manager() @property - def caea_is_active(self)-> bool: + def caea_is_active(self) -> bool: today = datetime.today() if self.valid_since <= today <= self.expires: return True diff --git a/tests/test_models.py b/tests/test_models.py index 9818ccd6..d56f8cf1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -686,9 +686,7 @@ def test_caea_assigned_receipt_correct(): assert caea_bad.caea_code != receipt.caea.caea_code assert ( - models.Caea.objects.all() - .filter(taxpayer=receipt.point_of_sales.owner) - .count() + models.Caea.objects.all().filter(taxpayer=receipt.point_of_sales.owner).count() == 2 ) @@ -702,50 +700,52 @@ def test_ordering_receipts_work(): assert models.Receipt.objects.last() == receipt_3 + @pytest.mark.django_db def test_isactive_caea(): caea = CaeaFactory( - period = datetime.today().strftime("%Y%m"), - order = 1, - valid_since = datetime(2022, 10, 1), - expires = datetime(2022, 10, 15), + period=datetime.today().strftime("%Y%m"), + order=1, + valid_since=datetime(2022, 10, 1), + expires=datetime(2022, 10, 15), ) caea_2 = CaeaFactory( - caea_code = "12345678974128", - period = datetime.today().strftime("%Y%m"), - order = 2, - valid_since = datetime(2022, 10, 16), - expires = datetime(2022, 10, 31), + caea_code="12345678974128", + period=datetime.today().strftime("%Y%m"), + order=2, + valid_since=datetime(2022, 10, 16), + expires=datetime(2022, 10, 31), ) - assert caea.valid_since <= datetime(2022,10,1) #Should be true - assert caea.expires <= datetime(2022,10,15) #Should be true - - assert caea.valid_since <= datetime(2022,10,15) <= caea.expires #Should be true - assert caea.valid_since <= datetime(2022,10,1) <= caea.expires #Should be true + assert caea.valid_since <= datetime(2022, 10, 1) # Should be true + assert caea.expires <= datetime(2022, 10, 15) # Should be true + + assert caea.valid_since <= datetime(2022, 10, 15) <= caea.expires # Should be true + assert caea.valid_since <= datetime(2022, 10, 1) <= caea.expires # Should be true - assert caea.valid_since > datetime(2022,9,30) #Should be true - assert caea.expires < datetime(2022,10,16) #Should be true + assert caea.valid_since > datetime(2022, 9, 30) # Should be true + assert caea.expires < datetime(2022, 10, 16) # Should be true assert not caea.caea_is_active assert caea_2.caea_is_active + @pytest.mark.django_db def test_caea_queryset(): caea = CaeaFactory( - valid_since = datetime(2022, 10, 16), - expires = datetime(2022, 10, 31), - ) + valid_since=datetime(2022, 10, 16), + expires=datetime(2022, 10, 31), + ) caea_2 = CaeaFactory( - caea_code = "12345678974128", - period = datetime.today().strftime("%Y%m"), - order = 1, - valid_since = datetime(2022, 8, 16), - expires = datetime(2022, 8, 31), + caea_code="12345678974128", + period=datetime.today().strftime("%Y%m"), + order=1, + valid_since=datetime(2022, 8, 16), + expires=datetime(2022, 8, 31), ) caea_active = models.Caea.objects.first() caea_active_count = models.Caea.objects.active().count() From c1138e6d3a7171aa54e35237178b7f55d02ddd7b Mon Sep 17 00:00:00 2001 From: erebodino <64107053+erebodino@users.noreply.github.com> Date: Mon, 17 Oct 2022 20:38:41 -0300 Subject: [PATCH 52/59] Update django_afip/factories.py Co-authored-by: Hugo --- django_afip/factories.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index f58bf2a9..fcc2580a 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -30,10 +30,9 @@ def get_order_of_date() -> int: corresponds to the first quarter (1) or the second (2) """ today = datetime.now() - order = 1 if today.day > 15: - order = 2 - return order + return 2 + return 1 def valid_since_caea(): From 58ba38bc121f216617acb528108b1fd53047b9f6 Mon Sep 17 00:00:00 2001 From: erebodino Date: Mon, 17 Oct 2022 20:47:53 -0300 Subject: [PATCH 53/59] Change over ready_to_print if clause, and add some explanation --- django_afip/models.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index 6ba09bfe..b258d767 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1531,11 +1531,17 @@ def is_validated(self) -> bool: @property def ready_to_print(self) -> bool: + """Whether this receipt is ready to print (or a PDF can be generated). + + Will return ``False`` is some validation is required before this instance can be printed. + """ + if "CAEA" in self.point_of_sales.issuance_type: return True - - if "CAE" in self.point_of_sales.issuance_type and self.is_validated: - return True + + if "CAE" in self.point_of_sales.issuance_type: + return self.is_validated + raise AssertionError("unreachable") def validate(self, ticket: AuthTicket = None, raise_=False) -> list[str]: """Validates this receipt. From 13e4eb193cc6fcacf8b70b5e4ed0e10ff07057ea Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 23:49:37 +0000 Subject: [PATCH 54/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/django_afip/models.py b/django_afip/models.py index b258d767..6bb2c385 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -1532,13 +1532,13 @@ def is_validated(self) -> bool: @property def ready_to_print(self) -> bool: """Whether this receipt is ready to print (or a PDF can be generated). - - Will return ``False`` is some validation is required before this instance can be printed. + + Will return ``False`` is some validation is required before this instance can be printed. """ - + if "CAEA" in self.point_of_sales.issuance_type: return True - + if "CAE" in self.point_of_sales.issuance_type: return self.is_validated raise AssertionError("unreachable") From 32bc9793f1d61afab13d82d066440a42a95182d1 Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 18 Oct 2022 17:47:51 -0300 Subject: [PATCH 55/59] Deleted CaeaCounter, now the receipt_number is assigned filtering receipts with same pos and receipt_type --- .../migrations/0033_delete_caeacounter.py | 16 +++ django_afip/models.py | 26 ----- django_afip/signals.py | 23 +++- tests/test_models.py | 107 ++++++++---------- tox.ini | 3 +- 5 files changed, 84 insertions(+), 91 deletions(-) create mode 100644 django_afip/migrations/0033_delete_caeacounter.py diff --git a/django_afip/migrations/0033_delete_caeacounter.py b/django_afip/migrations/0033_delete_caeacounter.py new file mode 100644 index 00000000..73c062be --- /dev/null +++ b/django_afip/migrations/0033_delete_caeacounter.py @@ -0,0 +1,16 @@ +# Generated by Django 4.0.8 on 2022-10-18 20:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0032_alter_caea_managers_remove_caea_active'), + ] + + operations = [ + migrations.DeleteModel( + name='CaeaCounter', + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 6bb2c385..403790f1 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -2053,32 +2053,6 @@ class Meta: verbose_name_plural = _("receipt validations") -class CaeaCounter(models.Model): - - pos = models.ForeignKey( - PointOfSales, related_name="counter", on_delete=models.PROTECT - ) - - receipt_type = models.ForeignKey( - ReceiptType, related_name="counter", on_delete=models.PROTECT - ) - - next_value = models.BigIntegerField(default=1) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["pos", "receipt_type"], - name="unique_migration_pos_receipt_combination", - ) - ] - - def __str__(self): - return "Counter for POS:{}, receipt_type:{}. Next_value is {}".format( - self.pos, self.receipt_type, self.next_value - ) - - class InformedCaeas(models.Model): pos = models.ForeignKey( diff --git a/django_afip/signals.py b/django_afip/signals.py index a3269ea7..3444d415 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -42,10 +42,21 @@ def save_caea_data(sender, instance: models.TaxPayer, **kwargs): else: if instance.caea == None or instance.caea == "": instance.caea = caea[0] + if instance.receipt_number == None or instance.receipt_number == "": - counter, _ = models.CaeaCounter.objects.get_or_create( - pos=instance.point_of_sales, receipt_type=instance.receipt_type - ) - instance.receipt_number = counter.next_value - counter.next_value += 1 - counter.save() + + last_number = models.Receipt.objects.filter( + point_of_sales=instance.point_of_sales, + receipt_type=instance.receipt_type + ).order_by( + "-receipt_number" + ).values_list( + "receipt_number", + flat=True + ).first() + + if last_number == None: #First record on the db + last_number = 0 + + correct_number = last_number + 1 + instance.receipt_number = correct_number diff --git a/tests/test_models.py b/tests/test_models.py index d56f8cf1..5a353595 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -148,7 +148,6 @@ def test_fetch_existing_data(populated_db): receipt_number=last_receipt_number, point_of_sales=pos, ) - assert receipt.CbteDesde == last_receipt_number assert receipt.PtoVta == pos.number @@ -363,31 +362,6 @@ def test_caea_creation_live(populated_db): assert str(caea.period) == datetime.today().strftime("%Y%m") -@pytest.mark.django_db -@pytest.mark.live -def test_create_caea_counter(populated_db): - - receipt_type = models.ReceiptType.objects.get(code=6) - pos = factories.PointOfSalesFactoryCaea() - number = None - with pytest.raises( - models.CaeaCounter.DoesNotExist, - ): - number = models.CaeaCounter.objects.get( - pos=pos, receipt_type=receipt_type - ).next_value - - assert number == None - - # caea = factories.CaeaFactory() - caea = models.Caea.objects.get(pk=1) - receipt = factories.ReceiptFactory(point_of_sales=pos) - number = models.CaeaCounter.objects.get( - pos=pos, receipt_type=receipt_type - ).next_value - assert number == 2 - - @pytest.mark.django_db def test_create_receipt_caea(): @@ -446,13 +420,10 @@ def test_validate_caea_receipt(populated_db): last_number = manager.fetch_last_receipt_number( point_of_sales=pos, receipt_type=receipt_type ) - caea_counter = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type - )[0] - caea_counter.next_value = last_number + 1 - caea_counter.save() receipt_1 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + receipt_1.receipt_number = last_number + 1 + receipt_1.save() errs = receipt_1.validate() @@ -473,21 +444,17 @@ def test_validate_caea_receipt_another_pos(populated_db): last_number = manager.fetch_last_receipt_number( point_of_sales=pos, receipt_type=receipt_type ) - caea_counter = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type - )[0] - caea_counter.next_value = last_number + 1 - caea_counter.save() receipt_1 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + receipt_1.receipt_number = last_number + 1 + receipt_1.save() receipt_2 = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + receipt_2.receipt_number = last_number + 2 + receipt_2.save() - caea_counter = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type - )[0] assert receipt_1 != receipt_2 - assert (caea_counter.next_value - 2) == receipt_1.receipt_number - assert (caea_counter.next_value - 1) == receipt_2.receipt_number + assert last_number + 1 == receipt_1.receipt_number + assert last_number + 2 == receipt_2.receipt_number qs = models.Receipt.objects.filter(point_of_sales=pos).filter( validation__isnull=True @@ -504,7 +471,7 @@ def test_validate_caea_receipt_another_pos(populated_db): @pytest.mark.live def test_validate_credit_note_caea(populated_db): - """Test validating valid receipts.""" + """Test validating a receipt with credit note attached.""" # fetch data from afip to set the receipt number manager = models.ReceiptManager() receipt_type_fact = models.ReceiptType.objects.get(code=6) @@ -512,26 +479,19 @@ def test_validate_credit_note_caea(populated_db): pos = factories.PointOfSalesFactoryCaea() # caea = factories.CaeaFactory() caea = models.Caea.objects.get(pk=1) - last_number = manager.fetch_last_receipt_number( + last_number_fact = manager.fetch_last_receipt_number( point_of_sales=pos, receipt_type=receipt_type_fact ) - caea_counter_fact = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type_fact - )[0] - caea_counter_fact.next_value = last_number + 1 - caea_counter_fact.save() - last_number = manager.fetch_last_receipt_number( + last_number_cn = manager.fetch_last_receipt_number( point_of_sales=pos, receipt_type=receipt_type_cn ) - caea_counter_cn = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type_cn - )[0] - caea_counter_cn.next_value = last_number + 1 - caea_counter_cn.save() # Create a receipt (this credit note relates to it): receipt = factories.ReceiptWithVatAndTaxFactoryCaea(point_of_sales=pos) + receipt.receipt_number = last_number_fact + 1 + receipt.save() + errs = receipt.validate() assert len(errs) == 0 @@ -540,13 +500,11 @@ def test_validate_credit_note_caea(populated_db): receipt_type__code=8, point_of_sales=pos ) # Nota de Crédito B credit_note.related_receipts.add(receipt) + credit_note.receipt_number = last_number_cn + 1 credit_note.save() - caea_counter_cn = models.CaeaCounter.objects.get_or_create( - pos=pos, receipt_type=receipt_type_cn - )[0] credit_note.validate(raise_=True) - assert credit_note.receipt_number == (caea_counter_cn.next_value - 1) + assert credit_note.receipt_number == (last_number_cn + 1) assert credit_note.validation.result == models.ReceiptValidation.RESULT_APPROVED @@ -754,3 +712,36 @@ def test_caea_queryset(): assert caea.caea_is_active assert caea == caea_active assert caea_active_count == 1 + +@pytest.mark.django_db +def test_receipt_caea_get_correct_numeration(): + + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + # caea = models.Caea.objects.get(pk=1) + receipt_1 = factories.ReceiptFactory(point_of_sales=pos) + receipt_1.receipt_number = 28930 + receipt_1.save() + receipt_2 = factories.ReceiptFactory(point_of_sales=pos) + + assert receipt_1.receipt_number == 28930 + assert receipt_2.receipt_number == 28931 + +@pytest.mark.django_db +def test_mixed_receipts_caea_get_correct_numeration(): + + pos = factories.PointOfSalesFactoryCaea() + caea = factories.CaeaFactory() + # caea = models.Caea.objects.get(pk=1) + receipt_1 = factories.ReceiptFactory(point_of_sales=pos) + receipt_2 = factories.ReceiptFactory(point_of_sales=pos) + + #factura A + receipt_type = factories.ReceiptTypeFactory(code=1) + receipt_a_1 = factories.ReceiptFactory(receipt_type=receipt_type, point_of_sales=pos) + receipt_a_2 = factories.ReceiptFactory(receipt_type=receipt_type, point_of_sales=pos) + + assert receipt_1.receipt_number == 1 + assert receipt_2.receipt_number == 2 + assert receipt_a_1.receipt_number == 1 + assert receipt_a_2.receipt_number == 2 \ No newline at end of file diff --git a/tox.ini b/tox.ini index 8e42b126..8f4d31f0 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,8 @@ deps = mysql: -e .[mysql] django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 -commands = pytest -vv -m "not live" {posargs} +#commands = pytest -vv -m "not live" {posargs} +commands = pytest -vv -s setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: From 3851a1abb32ef5fb36e41b6fa30bde01ac77e7ff Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 20:49:18 +0000 Subject: [PATCH 56/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../migrations/0033_delete_caeacounter.py | 4 ++-- django_afip/signals.py | 24 +++++++++---------- tests/test_models.py | 14 +++++++---- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/django_afip/migrations/0033_delete_caeacounter.py b/django_afip/migrations/0033_delete_caeacounter.py index 73c062be..db7b2b33 100644 --- a/django_afip/migrations/0033_delete_caeacounter.py +++ b/django_afip/migrations/0033_delete_caeacounter.py @@ -6,11 +6,11 @@ class Migration(migrations.Migration): dependencies = [ - ('afip', '0032_alter_caea_managers_remove_caea_active'), + ("afip", "0032_alter_caea_managers_remove_caea_active"), ] operations = [ migrations.DeleteModel( - name='CaeaCounter', + name="CaeaCounter", ), ] diff --git a/django_afip/signals.py b/django_afip/signals.py index 3444d415..fb05b23b 100644 --- a/django_afip/signals.py +++ b/django_afip/signals.py @@ -42,20 +42,20 @@ def save_caea_data(sender, instance: models.TaxPayer, **kwargs): else: if instance.caea == None or instance.caea == "": instance.caea = caea[0] - + if instance.receipt_number == None or instance.receipt_number == "": - last_number = models.Receipt.objects.filter( - point_of_sales=instance.point_of_sales, - receipt_type=instance.receipt_type - ).order_by( - "-receipt_number" - ).values_list( - "receipt_number", - flat=True - ).first() - - if last_number == None: #First record on the db + last_number = ( + models.Receipt.objects.filter( + point_of_sales=instance.point_of_sales, + receipt_type=instance.receipt_type, + ) + .order_by("-receipt_number") + .values_list("receipt_number", flat=True) + .first() + ) + + if last_number == None: # First record on the db last_number = 0 correct_number = last_number + 1 diff --git a/tests/test_models.py b/tests/test_models.py index 5a353595..563331d3 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -713,6 +713,7 @@ def test_caea_queryset(): assert caea == caea_active assert caea_active_count == 1 + @pytest.mark.django_db def test_receipt_caea_get_correct_numeration(): @@ -727,6 +728,7 @@ def test_receipt_caea_get_correct_numeration(): assert receipt_1.receipt_number == 28930 assert receipt_2.receipt_number == 28931 + @pytest.mark.django_db def test_mixed_receipts_caea_get_correct_numeration(): @@ -736,12 +738,16 @@ def test_mixed_receipts_caea_get_correct_numeration(): receipt_1 = factories.ReceiptFactory(point_of_sales=pos) receipt_2 = factories.ReceiptFactory(point_of_sales=pos) - #factura A + # factura A receipt_type = factories.ReceiptTypeFactory(code=1) - receipt_a_1 = factories.ReceiptFactory(receipt_type=receipt_type, point_of_sales=pos) - receipt_a_2 = factories.ReceiptFactory(receipt_type=receipt_type, point_of_sales=pos) + receipt_a_1 = factories.ReceiptFactory( + receipt_type=receipt_type, point_of_sales=pos + ) + receipt_a_2 = factories.ReceiptFactory( + receipt_type=receipt_type, point_of_sales=pos + ) assert receipt_1.receipt_number == 1 assert receipt_2.receipt_number == 2 assert receipt_a_1.receipt_number == 1 - assert receipt_a_2.receipt_number == 2 \ No newline at end of file + assert receipt_a_2.receipt_number == 2 From 087248e5cf3776d403c56b81bca5016c8bc10d7c Mon Sep 17 00:00:00 2001 From: erebodino Date: Tue, 18 Oct 2022 18:06:21 -0300 Subject: [PATCH 57/59] Modify caea is_active property, CaeaQueryset and tests --- django_afip/factories.py | 18 ++++++++---------- django_afip/models.py | 9 +++------ tests/test_models.py | 8 ++++---- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/django_afip/factories.py b/django_afip/factories.py index fcc2580a..57179034 100644 --- a/django_afip/factories.py +++ b/django_afip/factories.py @@ -24,7 +24,7 @@ def get_test_file(filename: str, mode="r") -> Path: return path -def get_order_of_date() -> int: +def get_current_order() -> int: """ Helper method to detect if the day of the month corresponds to the first quarter (1) or the second (2) @@ -40,11 +40,10 @@ def valid_since_caea(): Helper method to assign the valid_since field from Caea model to the correspondent year,month,day in the quarter """ - order = get_order_of_date() - valid_since = datetime(datetime.now().year, datetime.now().month, 1) + order = get_current_order() if order == 2: - valid_since = datetime(datetime.now().year, datetime.now().month, 16) - return valid_since + return datetime(datetime.now().year, datetime.now().month, 16) + return datetime(datetime.now().year, datetime.now().month, 1) def expires_caea(): @@ -52,12 +51,11 @@ def expires_caea(): Helper method to assign the expires field from Caea model to the correspondent year,month,day in the quarter """ - order = get_order_of_date() - expires = datetime(datetime.now().year, datetime.now().month, 15) + order = get_current_order() if order == 2: final = calendar.monthrange(datetime.now().year, datetime.now().month)[1] - expires = datetime(datetime.now().year, datetime.now().month, final) - return expires + return datetime(datetime.now().year, datetime.now().month, final) + return datetime(datetime.now().year, datetime.now().month, 15) class UserFactory(DjangoModelFactory): @@ -302,7 +300,7 @@ class Meta: caea_code = "12345678974125" period = datetime.today().strftime("%Y%m") - order = LazyFunction(get_order_of_date) + order = LazyFunction(get_current_order) valid_since = LazyFunction(valid_since_caea) expires = LazyFunction(expires_caea) generated = make_aware(datetime(2022, 5, 30, 21, 6, 4)) diff --git a/django_afip/models.py b/django_afip/models.py index 403790f1..5e252741 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -657,7 +657,7 @@ class Meta: class CaeaQuerySet(models.QuerySet): def active(self): today = datetime.today() - return self.filter(Q(valid_since__lte=today), Q(expires__gte=today)) + return self.filter(valid_since__lte=today,expires__gte=today) class Caea(models.Model): @@ -713,12 +713,9 @@ class Caea(models.Model): objects = CaeaQuerySet.as_manager() @property - def caea_is_active(self) -> bool: + def is_active(self) -> bool: today = datetime.today() - if self.valid_since <= today <= self.expires: - return True - else: - return False + return self.valid_since <= today <= self.expires def __str__(self) -> str: return str(self.caea_code) diff --git a/tests/test_models.py b/tests/test_models.py index 563331d3..239365bd 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -686,8 +686,8 @@ def test_isactive_caea(): assert caea.valid_since > datetime(2022, 9, 30) # Should be true assert caea.expires < datetime(2022, 10, 16) # Should be true - assert not caea.caea_is_active - assert caea_2.caea_is_active + assert not caea.is_active + assert caea_2.is_active @pytest.mark.django_db @@ -708,8 +708,8 @@ def test_caea_queryset(): caea_active = models.Caea.objects.first() caea_active_count = models.Caea.objects.active().count() - assert not caea_2.caea_is_active - assert caea.caea_is_active + assert not caea_2.is_active + assert caea.is_active assert caea == caea_active assert caea_active_count == 1 From 7f1eff163c6d69b8d0a2162b9f7e960d02ad752c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:06:51 +0000 Subject: [PATCH 58/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- django_afip/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/django_afip/models.py b/django_afip/models.py index 5e252741..4c20867f 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -657,7 +657,7 @@ class Meta: class CaeaQuerySet(models.QuerySet): def active(self): today = datetime.today() - return self.filter(valid_since__lte=today,expires__gte=today) + return self.filter(valid_since__lte=today, expires__gte=today) class Caea(models.Model): From 578929f8bc5ea6afeba460da6cb34c4ef6db9fe8 Mon Sep 17 00:00:00 2001 From: erebodino Date: Wed, 19 Oct 2022 12:15:58 -0300 Subject: [PATCH 59/59] Correct command for pytest on tox.ini --- tox.ini | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 8f4d31f0..8e42b126 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,7 @@ deps = mysql: -e .[mysql] django32: Django>=3.2,<3.3 django40: Django>=4.0,<4.1 -#commands = pytest -vv -m "not live" {posargs} -commands = pytest -vv -s +commands = pytest -vv -m "not live" {posargs} setenv = PYTHONPATH={toxinidir}/testapp:{toxinidir} sqlite: DATABASE_URL=sqlite:///:memory: