feat: integrar microservicio externo DemostrationTaxAPI via docker y requests
This commit is contained in:
@@ -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
|
||||
|
||||
+72
-22
@@ -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': net_amount_clp,
|
||||
'tax_rate_percent': Decimal('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': 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 []
|
||||
|
||||
|
||||
Submodule
+1
Submodule tax_api added at 60c958e96b
Reference in New Issue
Block a user