diff --git a/.gitignore b/.gitignore index 7f883106..07d16e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ test2.csr # Created by tests: /media/ + +# venv +.venv/ \ No newline at end of file diff --git a/django_afip/admin.py b/django_afip/admin.py index 06268ae1..3307a728 100644 --- a/django_afip/admin.py +++ b/django_afip/admin.py @@ -405,6 +405,11 @@ class PointOfSalesAdmin(admin.ModelAdmin): ) +@admin.register(models.ClientVatCondition) +class ClientVatConditionAdmin(admin.ModelAdmin): + search_fields = ("code", "description", "cmp_clase") + list_display = ("code", "description", "cmp_clase") + @admin.register(models.CurrencyType) class CurrencyTypeAdmin(admin.ModelAdmin): search_fields = ( diff --git a/django_afip/fixtures/clientvatcondition.yaml b/django_afip/fixtures/clientvatcondition.yaml new file mode 100644 index 00000000..be7e3ba9 --- /dev/null +++ b/django_afip/fixtures/clientvatcondition.yaml @@ -0,0 +1,65 @@ +- model: afip.clientvatcondition + fields: + code: 16 + description: "Monotributo Trabajador Independiente Promovido" + cmp_clase: "A/M/C" + +- model: afip.clientvatcondition + fields: + code: 15 + description: "IVA No Alcanzado" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 13 + description: "Monotributista Social" + cmp_clase: "A/M/C" + +- model: afip.clientvatcondition + fields: + code: 10 + description: "IVA Liberado – Ley N° 19.640" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 9 + description: "Cliente del Exterior" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 8 + description: "Proveedor del Exterior" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 7 + description: "Sujeto No Categorizado" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 6 + description: "Responsable Monotributo" + cmp_clase: "A/M/C" + +- model: afip.clientvatcondition + fields: + code: 5 + description: "Consumidor Final" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 4 + description: "IVA Sujeto Exento" + cmp_clase: "B/C" + +- model: afip.clientvatcondition + fields: + code: 1 + description: "IVA Responsable Inscripto" + cmp_clase: "A/M/C" diff --git a/django_afip/management/commands/load_client_vat_conditions.py b/django_afip/management/commands/load_client_vat_conditions.py new file mode 100644 index 00000000..4009c672 --- /dev/null +++ b/django_afip/management/commands/load_client_vat_conditions.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from django.core.management.base import BaseCommand +from django.utils.translation import gettext as _ + +from django_afip import clients, models, serializers + + +class Command(BaseCommand): + help = _( + "Retrieves all the ClientVatConditions from the AFIP server and updates it in the DB." + ) + requires_migrations_checks = True + + def add_arguments(self, parser): + parser.add_argument( + "cuit", + type=int, + help=_("CUIT of the tax payer to be used to authenticate."), + ) + + def handle(self, *args, **options) -> None: + from django_afip.models import TaxPayer + + tax_payer = TaxPayer.objects.get(cuit=options["cuit"]) + ticket = tax_payer.get_or_create_ticket("wsfe") + + client = clients.get_client("wsfe", sandbox=tax_payer.is_sandboxed) + response = client.service.FEParamGetCondicionIvaReceptor( + serializers.serialize_ticket(ticket), + ) + + for condition in response.ResultGet.CondicionIvaReceptor: + models.ClientVatCondition.objects.get_or_create( + code=condition.Id, + defaults={ + "description": condition.Desc, + "cmp_clase": condition.Cmp_Clase, + }, + ) + self.stdout.write(self.style.SUCCESS(f"Loaded {condition.Desc}")) + self.stdout.write(self.style.SUCCESS("All done!")) diff --git a/django_afip/migrations/0016_clientvatcondition_receipt_client_vat_condition.py b/django_afip/migrations/0016_clientvatcondition_receipt_client_vat_condition.py new file mode 100644 index 00000000..aab595e4 --- /dev/null +++ b/django_afip/migrations/0016_clientvatcondition_receipt_client_vat_condition.py @@ -0,0 +1,32 @@ +# Generated by Django 5.1.6 on 2025-02-26 14:09 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('afip', '0015_alter_taxpayer_logo'), + ] + + operations = [ + migrations.CreateModel( + name='ClientVatCondition', + fields=[ + ('code', models.IntegerField(primary_key=True, serialize=False, verbose_name='code')), + ('description', models.CharField(max_length=48, verbose_name='description')), + ('cmp_clase', models.CharField(help_text='Receipt class this VAT condition applies to (A, B, C, or M).', max_length=5, verbose_name='cmp clase')), + ], + options={ + 'verbose_name': 'Client VAT condition', + 'verbose_name_plural': 'Client VAT conditions', + 'unique_together': {('code', 'cmp_clase')}, + }, + ), + migrations.AddField( + model_name='receipt', + name='client_vat_condition', + field=models.ForeignKey(help_text='The VAT condition of the recipient of this receipt. It should match the receipt type.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='receipts', to='afip.clientvatcondition', verbose_name='client vat condition'), + ), + ] diff --git a/django_afip/models.py b/django_afip/models.py index 6e8942f1..97a380fe 100644 --- a/django_afip/models.py +++ b/django_afip/models.py @@ -85,7 +85,7 @@ def load_metadata() -> None: """Loads metadata from fixtures into the database.""" - for model in GenericAfipType.SUBCLASSES: + for model in GenericAfipType.SUBCLASSES + [ClientVatCondition]: label = model._meta.label.split(".")[1].lower() management.call_command("loaddata", label, app="afip") @@ -351,6 +351,30 @@ class Meta: verbose_name_plural = _("optional types") +class ClientVatCondition(models.Model): + code = models.IntegerField( + _("code"), + primary_key=True, + ) + description = models.CharField( + _("description"), + max_length=48, + ) + cmp_clase = models.CharField( + _("cmp clase"), + max_length=5, + help_text=_("Receipt class this VAT condition applies to (A, B, C, or M)."), + ) + + def __str__(self) -> str: + return self.description + + class Meta: + verbose_name = _("Client VAT condition") + verbose_name_plural = _("Client VAT conditions") + unique_together = ("code", "cmp_clase") # Ensure unique combination + + class TaxPayer(models.Model): """Represents an AFIP TaxPayer. @@ -1136,6 +1160,17 @@ class Receipt(models.Model): help_text=_("The document type of the recipient of this receipt."), on_delete=models.PROTECT, ) + client_vat_condition = models.ForeignKey( + ClientVatCondition, + verbose_name=_("client vat condition"), + help_text=_( + "The VAT condition of the recipient of this receipt. It should match the receipt type." + ), + related_name="receipts", + on_delete=models.PROTECT, + null=True, + ) + document_number = models.BigIntegerField( _("document number"), help_text=_("The document number of the recipient of this receipt."), diff --git a/django_afip/serializers.py b/django_afip/serializers.py index 34fa8b45..c0887159 100644 --- a/django_afip/serializers.py +++ b/django_afip/serializers.py @@ -94,6 +94,7 @@ def serialize_receipt(receipt: Receipt): # noqa: ANN201 ImpTrib=sum(tax.amount for tax in taxes), MonId=receipt.currency.code, MonCotiz=receipt.currency_quote, + CondicionIVAReceptorId=receipt.client_vat_condition.code, ) if int(receipt.concept.code) in (2, 3): serialized.FchServDesde = serialize_date(receipt.service_start)