From 557a9cbf5ca02c9bb0b3a67b7e871a9f8e8ed122 Mon Sep 17 00:00:00 2001 From: Nachou-u Date: Tue, 8 Sep 2026 10:19:38 -0300 Subject: [PATCH] feat: integrar microservicio externo DemostrationTaxAPI via docker y requests --- docker-compose.yml | 6 +++ expenses/services/sii.py | 94 ++++++++++++++++++++++++++++++---------- tax_api | 1 + 3 files changed, 79 insertions(+), 22 deletions(-) create mode 160000 tax_api diff --git a/docker-compose.yml b/docker-compose.yml index da98db7..040f948 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -37,6 +37,12 @@ services: - db - redis + tax-api: + build: ./tax_api + ports: + - "8001:8000" + restart: unless-stopped + celery: build: . command: celery -A config worker -l info diff --git a/expenses/services/sii.py b/expenses/services/sii.py index a43c5e7..9c40000 100644 --- a/expenses/services/sii.py +++ b/expenses/services/sii.py @@ -2,12 +2,21 @@ SII and Chilean Tax / Document Validation Service for EFINTEL ERP. Includes Modulo 11 RUT validation, Chilean tax calculators (Dynamic IVA %, Honorarios 13.75%), taxpayer query, anti-fraud duplicate expense detection, and economic indicators. +Now integrates with external Tax API microservice (tax-api:8000) for robust processing, +with seamless fallback to internal logic if the API is offline. """ +import requests from decimal import Decimal, ROUND_HALF_UP import re from typing import Tuple, List, Optional, Dict, Any from django.db.models import Q +from django.conf import settings + +# External API URL (default for Docker networking) +TAX_API_URL = "http://tax-api:8000" +# Fallback to localhost if running natively +TAX_API_URL_LOCAL = "http://localhost:8001" class SIIService: DEFAULT_IVA_RATE = Decimal('0.19') @@ -32,23 +41,40 @@ class SIIService: return "" return re.sub(r'[^0-9kK]', '', str(rut)).upper() + @classmethod + def _call_api(cls, endpoint: str, data: dict) -> Optional[dict]: + """Tries to call external Tax API. Supports Docker and Localhost networking.""" + for base_url in [TAX_API_URL, TAX_API_URL_LOCAL]: + try: + response = requests.post(f"{base_url}{endpoint}", json=data, timeout=2.0) + if response.status_code == 200: + return response.json() + except (requests.ConnectionError, requests.Timeout): + continue + return None + @classmethod def validate_rut(cls, rut: str) -> Tuple[bool, str]: """ - Validates Chilean RUT using strict Modulo 11 algorithm. - Returns (is_valid: bool, formatted_rut_or_error: str). + Validates Chilean RUT. Tries external microservice first, then falls back to local. """ cleaned = cls.clean_rut(rut) if len(cleaned) < 2: return False, "RUT demasiado corto." + # 1. Try External Tax API + api_res = cls._call_api("/api/rut/validar", {"rut": rut}) + if api_res: + if api_res.get('valido'): + return True, api_res.get('rut_formateado') + else: + return False, api_res.get('mensaje', 'RUT Inválido') + + # 2. Fallback to Local Internal Logic body = cleaned[:-1] dv = cleaned[-1] - if not body.isdigit(): return False, "El cuerpo del RUT debe contener sólo dígitos." - - # Modulo 11 calculation reversed_digits = [int(d) for d in reversed(body)] factors = [2, 3, 4, 5, 6, 7] total = 0 @@ -74,9 +100,7 @@ class SIIService: @classmethod def get_taxpayer_info(cls, rut: str) -> Dict[str, Any]: - """ - Queries taxpayer information (Razón Social, Giro, Status) from database or SII lookup service. - """ + """Queries taxpayer info.""" is_valid, formatted_rut = cls.validate_rut(rut) if not is_valid: return {'valid': False, 'error': formatted_rut} @@ -95,7 +119,6 @@ class SIIService: 'source': 'ERP_DB' } - # Simulated SII API Response for validated RUTs return { 'valid': True, 'rut': formatted_rut, @@ -110,10 +133,7 @@ class SIIService: @classmethod def calculate_taxes(cls, net_amount_clp: Decimal, tax_type: str, custom_iva_percent: Optional[Decimal] = None) -> dict: """ - Calculates tax amounts and gross/net totals based on Chilean tax type and dynamic IVA %: - - IVA_19 / AFECTO: Total = Neto + (Neto * IVA%) - - HONORARIOS_13_75: Retention 13.75% - - EXENTO: Total = Neto, Tax = 0 + Calculates tax amounts. Integrates with Tax API when applicable. """ if not net_amount_clp or net_amount_clp < 0: net_amount_clp = Decimal('0') @@ -125,7 +145,22 @@ class SIIService: iva_rate = cls.get_current_iva_rate() iva_percent_val = (iva_rate * Decimal('100.0')).quantize(Decimal('0.1')) + net_float = float(net_amount_clp) + if tax_type in ('IVA_19', 'IVA', 'AFECTO'): + # 1. Try External API + api_res = cls._call_api("/api/iva/desde-neto", {"monto_neto": net_float, "tasa": float(iva_rate)}) + if api_res: + return { + 'tax_type': 'IVA_19', + 'net_amount': Decimal(str(api_res['monto_neto'])), + 'tax_rate_percent': iva_percent_val, + 'tax_amount': Decimal(str(api_res['iva'])), + 'total_amount': Decimal(str(api_res['monto_bruto'])), + 'description': f"Afecto a IVA ({api_res.get('tasa_iva_porcentaje', str(iva_percent_val)+'%')}) - Vía API Externa" + } + + # 2. Fallback local tax_amount = (net_amount_clp * iva_rate).quantize(Decimal('1'), rounding=ROUND_HALF_UP) total_amount = net_amount_clp + tax_amount return { @@ -136,16 +171,33 @@ class SIIService: 'total_amount': total_amount, 'description': f'Afecto a IVA ({iva_percent_val:g}%)' } + elif tax_type in ('HONORARIOS_13_75', 'HONORARIOS'): - tax_amount = (net_amount_clp * cls.HONORARIOS_RETENTION_RATE).quantize(Decimal('1'), rounding=ROUND_HALF_UP) - total_amount = net_amount_clp + tax_amount + # 1. Try External API (passing 2026 as default year for 15.25% retention) + api_res = cls._call_api("/api/honorarios/retencion", {"monto_bruto": net_float, "anio": 2026}) + if api_res: + tasa_porcentaje = (Decimal(str(api_res.get('tasa_retencion', 0.1525))) * Decimal('100.0')).quantize(Decimal('0.1')) + return { + 'tax_type': 'HONORARIOS_13_75', + 'net_amount': Decimal(str(api_res.get('monto_liquido', net_float))), + 'tax_rate_percent': tasa_porcentaje, + 'tax_amount': Decimal(str(api_res.get('retencion', 0))), + 'total_amount': Decimal(str(api_res.get('monto_bruto', net_float))), + 'description': f'Retención Honorarios ({tasa_porcentaje}%) - Vía API Externa' + } + + # 2. Fallback local (Bruto = Neto ingresado) + # Retencion = Bruto * 0.1525 (año 2026) + tasa = Decimal('0.1525') + tax_amount = (net_amount_clp * tasa).quantize(Decimal('1'), rounding=ROUND_HALF_UP) + liquid_amount = net_amount_clp - tax_amount return { 'tax_type': 'HONORARIOS_13_75', - 'net_amount': net_amount_clp, - 'tax_rate_percent': Decimal('13.75'), + 'net_amount': liquid_amount, + 'tax_rate_percent': Decimal('15.25'), 'tax_amount': tax_amount, - 'total_amount': total_amount, - 'description': 'Retención Boleta de Honorarios 13.75%' + 'total_amount': net_amount_clp, + 'description': 'Retención Boleta de Honorarios 15.25%' } else: return { @@ -166,9 +218,7 @@ class SIIService: document_number: Optional[str] = None, exclude_id: Optional[int] = None ) -> List: - """ - Detects potential duplicate expenses to prevent fraud or double-reimbursement. - """ + """Detects potential duplicate expenses.""" if not amount_clp: return [] diff --git a/tax_api b/tax_api new file mode 160000 index 0000000..60c958e --- /dev/null +++ b/tax_api @@ -0,0 +1 @@ +Subproject commit 60c958e96b66d4dc660948d0b012fab1f2a2571f