diff --git a/expenses/forms.py b/expenses/forms.py index 11c4c68..0506a89 100644 --- a/expenses/forms.py +++ b/expenses/forms.py @@ -137,6 +137,7 @@ class ExpenseForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['project'].queryset = Project.objects.filter(parent__parent__isnull=False) + self.fields['category'].queryset = Category.objects.filter(children__isnull=True) if self.instance and self.instance.pk: try: vlog = self.instance.vehicle_log @@ -218,10 +219,11 @@ class ExpenseForm(forms.ModelForm): raise forms.ValidationError("Si desea registrar un nuevo proveedor, debe ingresar tanto el RUT como la Razón Social.") # Recalculate taxes via SIIService - amount = cleaned_data.get('amount_clp') + amount_clp = cleaned_data.get('amount_clp') tax_type = cleaned_data.get('tax_type') - if amount and tax_type: - tax_res = SIIService.calculate_taxes(amount, tax_type) + if amount_clp and amount_clp > 0 and tax_type: + tax_res = SIIService.calculate_taxes(amount_clp, tax_type) + cleaned_data['amount_clp'] = tax_res['net_amount'] cleaned_data['tax_amount_clp'] = tax_res['tax_amount'] cleaned_data['total_amount_clp'] = tax_res['total_amount'] @@ -229,7 +231,7 @@ class ExpenseForm(forms.ModelForm): vendor = cleaned_data.get('vendor') doc_num = cleaned_data.get('document_number') date_val = cleaned_data.get('date') - if amount and (vendor or doc_num): + if amount_clp and (vendor or doc_num): duplicates = SIIService.check_duplicate_expense( expense_model=Expense, vendor_id=vendor.id if vendor else None, @@ -352,6 +354,10 @@ class ProjectForm(forms.ModelForm): 'payment_method': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Ej: 50% Anticipo / 50% Hito Final'}), } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['category'].queryset = Category.objects.filter(children__isnull=True) + class ProjectPhaseForm(forms.ModelForm): due_date = forms.DateField(widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), required=False, label="Fecha Pactada") @@ -381,10 +387,11 @@ class CategoryForm(forms.ModelForm): class VendorForm(forms.ModelForm): class Meta: model = Vendor - fields = ['rut', 'name', 'business_line', 'bank', 'account_type', 'account_number', 'email', 'payment_terms'] + fields = ['rut', 'name', 'provider_type', 'business_line', 'bank', 'account_type', 'account_number', 'email', 'payment_terms'] widgets = { 'rut': forms.TextInput(attrs={'class': 'form-control', 'required': 'required', 'pattern': '^[0-9]+-[0-9kK]$', 'title': 'Formato válido: 12345678-9'}), 'name': forms.TextInput(attrs={'class': 'form-control', 'required': 'required'}), + 'provider_type': forms.Select(attrs={'class': 'form-control'}), 'email': forms.EmailInput(attrs={'class': 'form-control'}), } diff --git a/expenses/migrations/0013_vendor_provider_type.py b/expenses/migrations/0013_vendor_provider_type.py new file mode 100644 index 0000000..1f139bb --- /dev/null +++ b/expenses/migrations/0013_vendor_provider_type.py @@ -0,0 +1,18 @@ +# Generated by Django 6.1 on 2026-09-09 17:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('expenses', '0012_alter_category_parent_alter_expense_project_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vendor', + name='provider_type', + field=models.CharField(choices=[('BIENES', 'Proveedor de Bienes'), ('SERVICIOS', 'Proveedor de Servicios'), ('AMBOS', 'Bienes y Servicios')], default='BIENES', max_length=20, verbose_name='Clasificación'), + ), + ] diff --git a/expenses/models.py b/expenses/models.py index 31e312c..8f33882 100644 --- a/expenses/models.py +++ b/expenses/models.py @@ -137,8 +137,14 @@ class Category(CatalogModel): class Vendor(TimeStampedModel): + PROVIDER_TYPE_CHOICES = [ + ('BIENES', 'Proveedor de Bienes'), + ('SERVICIOS', 'Proveedor de Servicios'), + ('AMBOS', 'Bienes y Servicios'), + ] rut = models.CharField(max_length=15, unique=True, verbose_name="RUT Proveedor") name = models.CharField(max_length=200, verbose_name="Razón Social / Nombre") + provider_type = models.CharField(max_length=20, choices=PROVIDER_TYPE_CHOICES, default='BIENES', verbose_name="Clasificación") business_line = models.CharField(max_length=200, blank=True, null=True, verbose_name="Giro Comercial") bank = models.CharField(max_length=100, blank=True, null=True, verbose_name="Banco") diff --git a/expenses/services/sii.py b/expenses/services/sii.py index 9c40000..1ff8574 100644 --- a/expenses/services/sii.py +++ b/expenses/services/sii.py @@ -186,17 +186,19 @@ class SIIService: 'description': f'Retención Honorarios ({tasa_porcentaje}%) - Vía API Externa' } - # 2. Fallback local (Bruto = Neto ingresado) - # Retencion = Bruto * 0.1525 (año 2026) + # 2. Fallback local (Neto ingresado = Líquido) + # Bruto = Líquido / (1 - 0.1525) + # Retencion = Bruto - Líquido tasa = Decimal('0.1525') - tax_amount = (net_amount_clp * tasa).quantize(Decimal('1'), rounding=ROUND_HALF_UP) - liquid_amount = net_amount_clp - tax_amount + liquid_amount = net_amount_clp + bruto_amount = (liquid_amount / (Decimal('1') - tasa)).quantize(Decimal('1'), rounding=ROUND_HALF_UP) + tax_amount = bruto_amount - liquid_amount return { 'tax_type': 'HONORARIOS_13_75', 'net_amount': liquid_amount, 'tax_rate_percent': Decimal('15.25'), 'tax_amount': tax_amount, - 'total_amount': net_amount_clp, + 'total_amount': bruto_amount, 'description': 'Retención Boleta de Honorarios 15.25%' } else: diff --git a/fix_db_data.py b/fix_db_data.py new file mode 100644 index 0000000..d523bcd --- /dev/null +++ b/fix_db_data.py @@ -0,0 +1,38 @@ +import os +import django + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +django.setup() + +from expenses.models import Category +from procurement.models import ItemCatalog + +# 1. Update Project Categories to exactly 1201-1207 +print("Actualizando Categorías a la serie 1200...") +Category.objects.filter(code__in=['3101', '3102', '3103', '3104', '3105', '3106', '3107']).delete() + +required_codes = { + '1201': 'Investigación Básica', + '1202': 'Investigación Aplicada', + '1203': 'Desarrollo Experimental', + '1204': 'Prototipado', + '1205': 'Pruebas de Concepto (PoC)', + '1206': 'Gestión de Proyectos', + '1207': 'Otros Servicios' +} +parent, _ = Category.objects.get_or_create(code='1200', defaults={'name': 'Portafolio de Proyectos'}) +for code, name in required_codes.items(): + cat, created = Category.objects.get_or_create(code=code, defaults={'name': name, 'parent': parent}) + if not created: + cat.name = name + cat.parent = parent + cat.save() + +# 3. Delete 'actv' from ItemCatalog in Procurement +print("Eliminando ítem 'actv' del catálogo...") +items_to_delete = ItemCatalog.objects.filter(code__iexact='actv') +count = items_to_delete.count() +items_to_delete.delete() +print(f"Se eliminaron {count} ítems 'actv'.") + +print("Migración de datos completada.") diff --git a/procurement/models.py b/procurement/models.py index 2ee62ee..3202db1 100644 --- a/procurement/models.py +++ b/procurement/models.py @@ -154,7 +154,8 @@ class PurchaseOrder(models.Model): def recalculate_totals(self): neto = sum(item.subtotal_net for item in self.items.all()) - iva = (neto * Decimal('0.19')).quantize(Decimal('1')) + iva_rate = Decimal(str(self.tax_rate or '19.00')) / Decimal('100.0') + iva = (neto * iva_rate).quantize(Decimal('1')) total = neto + iva self.net_amount_clp = neto self.tax_amount_clp = iva diff --git a/templates/base.html b/templates/base.html index 5cd3b1e..5eba1c3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -167,5 +167,29 @@ }); {% block extra_js %}{% endblock %} + + + diff --git a/templates/expenses/partials/expense_row.html b/templates/expenses/partials/expense_row.html index aec71cd..ebeb193 100644 --- a/templates/expenses/partials/expense_row.html +++ b/templates/expenses/partials/expense_row.html @@ -14,8 +14,11 @@
{% for doc in expense.documents.all %}
- - {{ doc.title|default:doc.file.name|truncatechars:20 }} + + {{ doc.title|default:doc.file.name|truncatechars:20 }} + + +
{% endfor %}