From f0a4fa64e8d81034e7c11ef8df1b99b01562e1fa Mon Sep 17 00:00:00 2001 From: Nachou-u Date: Wed, 9 Sep 2026 14:45:50 -0300 Subject: [PATCH] FEAT: re-integracion de tax-api y simulador F29 --- docker-compose.local.yml | 44 ++++++------ docker-compose.yml | 63 +++++++++-------- expenses/services/sii.py | 36 +++++++++- expenses/urls.py | 1 + expenses/views.py | 74 ++++++++++++++++++++ fix_base.py | 24 +++++++ patch_compose.py | 32 +++++++++ templates/base.html | 3 + templates/expenses/f29_simulator.html | 99 +++++++++++++++++++++++++++ 9 files changed, 324 insertions(+), 52 deletions(-) create mode 100644 fix_base.py create mode 100644 patch_compose.py create mode 100644 templates/expenses/f29_simulator.html diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 9b53ec6..ca42c23 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,5 +1,4 @@ -version: "3.8" - +version: '3.8' services: web: build: @@ -7,37 +6,40 @@ services: dockerfile: Dockerfile command: python manage.py runserver 0.0.0.0:8000 volumes: - - .:/app - - local_media:/app/media + - .:/app + - local_media:/app/media ports: - - "8000:8000" + - 8000:8000 environment: - - DEBUG=1 - - USE_S3=False - - REDIS_URL=redis://redis:6379/0 - # Forzamos SQLite local para que ignore el .env de producción si existe - - DATABASE_URL=sqlite:////app/db.sqlite3 + - DEBUG=1 + - USE_S3=False + - REDIS_URL=redis://redis:6379/0 + - DATABASE_URL=sqlite:////app/db.sqlite3 depends_on: - - redis - + - redis celery: build: context: . dockerfile: Dockerfile command: celery -A config worker -l info volumes: - - .:/app + - .:/app environment: - - DEBUG=1 - - USE_S3=False - - REDIS_URL=redis://redis:6379/0 + - DEBUG=1 + - USE_S3=False + - REDIS_URL=redis://redis:6379/0 depends_on: - - redis - + - redis redis: image: redis:7-alpine ports: - - "6379:6379" - + - 6379:6379 + tax-api: + build: + context: ./tax_api + dockerfile: Dockerfile + ports: + - 8001:8000 + mem_limit: 256m volumes: - local_media: + local_media: null diff --git a/docker-compose.yml b/docker-compose.yml index 26f1115..0d9c734 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,43 +1,41 @@ -version: "3.8" - +version: '3.8' services: web: build: context: . dockerfile: Dockerfile - command: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 3 + command: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --bind + 0.0.0.0:8000 --workers 3 restart: unless-stopped environment: REDIS_URL: redis://redis:6379/0 - CSRF_TRUSTED_ORIGINS: "https://${APP_DOMAIN}" + CSRF_TRUSTED_ORIGINS: https://${APP_DOMAIN} volumes: - - erp_media:/app/media - - erp_static:/app/static + - erp_media:/app/media + - erp_static:/app/static networks: - - controlmc_proxy - - internal + - controlmc_proxy + - internal labels: - - "controlmc.public=true" - - "traefik.enable=true" - - "traefik.docker.network=controlmc_proxy" - - "traefik.http.routers.${ROUTER_NAME}.rule=Host(`${APP_DOMAIN}`)" - - "traefik.http.routers.${ROUTER_NAME}.entrypoints=websecure" - - "traefik.http.routers.${ROUTER_NAME}.tls=true" - - "traefik.http.routers.${ROUTER_NAME}.tls.certresolver=le" - - "traefik.http.services.${ROUTER_NAME}.loadbalancer.server.port=8000" + - controlmc.public=true + - traefik.enable=true + - traefik.docker.network=controlmc_proxy + - traefik.http.routers.${ROUTER_NAME}.rule=Host(`${APP_DOMAIN}`) + - traefik.http.routers.${ROUTER_NAME}.entrypoints=websecure + - traefik.http.routers.${ROUTER_NAME}.tls=true + - traefik.http.routers.${ROUTER_NAME}.tls.certresolver=le + - traefik.http.services.${ROUTER_NAME}.loadbalancer.server.port=8000 depends_on: - - redis + - redis mem_limit: 1g - redis: image: redis:7-alpine restart: unless-stopped volumes: - - erp_redis_data:/data + - erp_redis_data:/data networks: - - internal + - internal mem_limit: 512m - celery: build: context: . @@ -47,17 +45,24 @@ services: environment: REDIS_URL: redis://redis:6379/0 networks: - - internal + - internal depends_on: - - redis + - redis mem_limit: 512m - + tax-api: + build: + context: ./tax_api + dockerfile: Dockerfile + restart: unless-stopped + networks: + - controlmc_proxy + - internal + mem_limit: 256m networks: controlmc_proxy: external: true - internal: - + internal: null volumes: - erp_media: - erp_static: - erp_redis_data: + erp_media: null + erp_static: null + erp_redis_data: null diff --git a/expenses/services/sii.py b/expenses/services/sii.py index 1ff8574..fbe8655 100644 --- a/expenses/services/sii.py +++ b/expenses/services/sii.py @@ -205,12 +205,44 @@ class SIIService: return { 'tax_type': 'EXENTO', 'net_amount': net_amount_clp, - 'tax_rate_percent': Decimal('0.0'), + 'tax_rate_percent': Decimal('0'), 'tax_amount': Decimal('0'), 'total_amount': net_amount_clp, - 'description': 'Exento / No Afecto a Impuestos' + 'description': 'Exento de Impuestos' } + @classmethod + def simulate_f29(cls, f29_data: dict) -> dict: + """ + Llama al endpoint /api/f29/simular del tax-api. + f29_data debe contener: ventas_netas, compras_netas, etc. + """ + api_res = cls._call_api("/api/f29/simular", f29_data) + if api_res: + return api_res + + # Fallback local simplificado si la API no está disponible + ventas_netas = Decimal(str(f29_data.get('ventas_netas', 0))) + compras_netas = Decimal(str(f29_data.get('compras_netas', 0))) + retenciones = Decimal(str(f29_data.get('retenciones_honorarios', 0))) + tasa_ppm = Decimal(str(f29_data.get('tasa_ppm', 0.01))) + + debito = ventas_netas * Decimal('0.19') + credito = compras_netas * Decimal('0.19') + ppm = ventas_netas * tasa_ppm + iva_pagar = max(Decimal('0'), debito - credito) + + return { + "iva_debito": float(debito), + "iva_credito": float(credito), + "iva_a_pagar": float(iva_pagar), + "ppm_a_pagar": float(ppm), + "total_retenciones_honorarios": float(retenciones), + "total_a_pagar": float(iva_pagar + ppm + retenciones), + "remanente_siguiente_mes": float(max(Decimal('0'), credito - debito)), + "nota": "Simulación local de respaldo. API no disponible." + } + @staticmethod def check_duplicate_expense( expense_model, diff --git a/expenses/urls.py b/expenses/urls.py index 978e42e..3a4b9d3 100644 --- a/expenses/urls.py +++ b/expenses/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('gastos/exportar/csv/', views.export_expenses_csv, name='export_csv'), path('gastos/exportar/transferencias/', views.export_bank_transfer_csv, name='export_bank_transfer_csv'), + path('f29/simulador/', views.f29_simulator, name='f29_simulator'), path('proyectos/', views.project_list, name='project_list'), path('proyectos//', views.project_detail, name='project_detail'), path('proyectos//fase/nueva/', views.project_add_phase, name='project_add_phase'), diff --git a/expenses/views.py b/expenses/views.py index 9a90757..fb3bc7d 100644 --- a/expenses/views.py +++ b/expenses/views.py @@ -1014,3 +1014,77 @@ def api_sii_calculate_taxes(request): 'description': res['description'] }) + +from datetime import datetime, date +import calendar +from django.db.models import Sum +from procurement.models import PurchaseOrder + +@login_required +def f29_simulator(request): + year = request.GET.get('year', date.today().year) + month = request.GET.get('month', date.today().month) + ventas_netas = request.GET.get('ventas_netas', 0) + ventas_exentas = request.GET.get('ventas_exentas', 0) + tasa_ppm = request.GET.get('tasa_ppm', 0.01) + + try: + year, month = int(year), int(month) + ventas_netas = float(ventas_netas) + ventas_exentas = float(ventas_exentas) + tasa_ppm = float(tasa_ppm) + except ValueError: + year, month = date.today().year, date.today().month + ventas_netas, ventas_exentas, tasa_ppm = 0, 0, 0.01 + + _, last_day = calendar.monthrange(year, month) + start_date = date(year, month, 1) + end_date = date(year, month, last_day) + + # 1. Compras Netas (Facturas con IVA 19%) + compras_expenses = Expense.objects.filter( + date__range=(start_date, end_date), + tax_type='IVA_19', + approval_status='LIBERADO_PAGADO' + ).aggregate(total=Sum('amount_clp'))['total'] or 0 + + compras_po = PurchaseOrder.objects.filter( + order_date__range=(start_date, end_date), + status='LIBERADA_PAGADA' + ).aggregate(total=Sum('net_amount_clp'))['total'] or 0 + + compras_netas = float(compras_expenses) + float(compras_po) + + # 2. Honorarios + honorarios_expenses = Expense.objects.filter( + date__range=(start_date, end_date), + tax_type='HONORARIOS_13_75', + approval_status='LIBERADO_PAGADO' + ).aggregate(total=Sum('tax_amount_clp'))['total'] or 0 + retenciones_honorarios = float(honorarios_expenses) + + f29_data = { + 'ventas_netas': ventas_netas, + 'compras_netas': compras_netas, + 'ventas_exentas': ventas_exentas, + 'compras_activo_fijo': 0, + 'remanente_anterior': 0, + 'tasa_ppm': tasa_ppm, + 'retenciones_honorarios': retenciones_honorarios + } + + simulation = SIIService.simulate_f29(f29_data) + + context = { + 'year': year, + 'month': month, + 'months': range(1, 13), + 'years': range(date.today().year - 2, date.today().year + 2), + 'ventas_netas': ventas_netas, + 'ventas_exentas': ventas_exentas, + 'tasa_ppm': tasa_ppm, + 'simulation': simulation, + 'compras_netas': compras_netas, + 'retenciones_honorarios': retenciones_honorarios + } + return render(request, 'expenses/f29_simulator.html', context) diff --git a/fix_base.py b/fix_base.py new file mode 100644 index 0000000..a39e2ff --- /dev/null +++ b/fix_base.py @@ -0,0 +1,24 @@ +with open('templates/base.html', 'r') as f: + lines = f.readlines() + +new_lines = [] +skip = False +for i, line in enumerate(lines): + if 'url \'accounts_payable\'' in line: + new_lines.append(line) + new_lines.append(' Cuentas por Pagar (CXP)\n \n') + new_lines.append(' \n Simulador F29\n \n') + skip = True + continue + + if skip: + if 'Cuentas por Pagar (CXP)' in line: + pass + elif '' in line: + skip = False + continue + + new_lines.append(line) + +with open('templates/base.html', 'w') as f: + f.writelines(new_lines) diff --git a/patch_compose.py b/patch_compose.py new file mode 100644 index 0000000..d593123 --- /dev/null +++ b/patch_compose.py @@ -0,0 +1,32 @@ +import yaml + +with open('docker-compose.yml', 'r') as f: + data = yaml.safe_load(f) + +data['services']['tax-api'] = { + 'build': { + 'context': './tax_api', + 'dockerfile': 'Dockerfile' + }, + 'restart': 'unless-stopped', + 'networks': ['controlmc_proxy', 'internal'], + 'mem_limit': '256m' +} + +with open('docker-compose.yml', 'w') as f: + yaml.dump(data, f, sort_keys=False, default_flow_style=False) + +with open('docker-compose.local.yml', 'r') as f: + data_local = yaml.safe_load(f) + +data_local['services']['tax-api'] = { + 'build': { + 'context': './tax_api', + 'dockerfile': 'Dockerfile' + }, + 'ports': ['8001:8000'], + 'mem_limit': '256m' +} + +with open('docker-compose.local.yml', 'w') as f: + yaml.dump(data_local, f, sort_keys=False, default_flow_style=False) diff --git a/templates/base.html b/templates/base.html index 5eba1c3..04f2a73 100644 --- a/templates/base.html +++ b/templates/base.html @@ -69,6 +69,9 @@ Cartera Talento {% if 'Administrador Finanzas' in request.user.groups.all.0.name or 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %} + + Simulador F29 + Cuentas por Pagar (CXP) diff --git a/templates/expenses/f29_simulator.html b/templates/expenses/f29_simulator.html new file mode 100644 index 0000000..a8fd16a --- /dev/null +++ b/templates/expenses/f29_simulator.html @@ -0,0 +1,99 @@ +{% extends 'base.html' %} +{% load humanize %} + +{% block content %} +
+
+
+

Simulador F29 (IVA y Retenciones)

+

Calcula la declaración mensual conectándose a la Tax API, utilizando gastos y compras pagadas del mes.

+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+
+

Datos Obtenidos del ERP

+
    +
  • + Total Compras Netas (Pagadas): + ${{ compras_netas|floatformat:"0"|intcomma }} +
  • +
  • + Total Retenciones Boletas (Pagadas): + ${{ retenciones_honorarios|floatformat:"0"|intcomma }} +
  • +
+
+ Sólo se incluyen Órdenes de Compra y Erogaciones en estado LIBERADO_PAGADO. +
+
+ +
+

Resultado Simulación F29

+
    +
  • + Débito Fiscal (IVA Ventas): + ${{ simulation.iva_debito|floatformat:"0"|intcomma }} +
  • +
  • + Crédito Fiscal (IVA Compras): + ${{ simulation.iva_credito|floatformat:"0"|intcomma }} +
  • +
  • + PPM a Pagar: + ${{ simulation.ppm_a_pagar|floatformat:"0"|intcomma }} +
  • +
  • + Retenciones a Pagar: + ${{ simulation.total_retenciones_honorarios|floatformat:"0"|intcomma }} +
  • +
  • + Remanente Siguiente Mes: + ${{ simulation.remanente_siguiente_mes|floatformat:"0"|intcomma }} +
  • +
  • + Total A Pagar F29: + ${{ simulation.total_a_pagar|floatformat:"0"|intcomma }} +
  • +
+
+ {{ simulation.nota|default:"Cálculo validado correctamente." }} +
+
+
+
+{% endblock %}