diff --git a/backend/api/v1/modules/a76/items/line_items/models.py b/backend/api/v1/modules/a76/items/line_items/models.py index a7603d11..5192f46f 100644 --- a/backend/api/v1/modules/a76/items/line_items/models.py +++ b/backend/api/v1/modules/a76/items/line_items/models.py @@ -193,3 +193,8 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): cascade="all, delete-orphan", uselist=False, ) + part_info: Mapped[Optional["api.v1.modules.a76.parts.models.Part"]] = relationship( + "api.v1.modules.a76.parts.models.Part", + foreign_keys=[part_number], + viewonly=True, + ) diff --git a/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/__init__.py b/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/task.py b/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/task.py new file mode 100644 index 00000000..b1ff3adc --- /dev/null +++ b/backend/api/v1/modules/a76/reports/exportacion/aviso_consolidado/task.py @@ -0,0 +1,11 @@ + +from celery import shared_task +import time + +@shared_task(bind=True, name="generate_aviso_consolidado_pdf_task") +def generate_aviso_consolidado_pdf_task(self, invoice_id: int, company_id: int): + """ + Tarea de Celery para generar el PDF del Aviso Consolidado. + Por ahora es un stub hasta que el servicio esté implementado. + """ + raise NotImplementedError("El servicio de Aviso Consolidado aún no está implementado") diff --git a/backend/api/v1/modules/a76/reports/exportacion/descargo/routes.py b/backend/api/v1/modules/a76/reports/exportacion/descargo/routes.py new file mode 100644 index 00000000..5c8ee8bc --- /dev/null +++ b/backend/api/v1/modules/a76/reports/exportacion/descargo/routes.py @@ -0,0 +1,52 @@ + +from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException +from fastapi.responses import JSONResponse, Response +from sqlalchemy.orm import Session +from typing import Dict, Any + +from core.database import get_core_db as get_db +from core.security import get_current_user +from .task import generate_descarga_pdf_task +from celery.result import AsyncResult + +router = APIRouter() + +@router.post("/{invoice_id}/download-async") +async def trigger_descarga_generation( + invoice_id: int, + company_id: int, + current_user: Any = Depends(get_current_user) +): + """ + Inicia la generación del reporte de Descarga PEPS en segundo plano (Celery). + Retorna el task_id para polling. + """ + try: + # Lanza la tarea de Celery + task = generate_descarga_pdf_task.delay(invoice_id, company_id) + return {"task_id": task.id, "status": "processing"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/tasks/{task_id}") +async def get_task_status(task_id: str, current_user: Any = Depends(get_current_user)): + """ + Consulta el estado de la tarea de Celery. + """ + task_result = AsyncResult(task_id) + + response = { + "task_id": task_id, + "state": task_result.state, + "result": None, + "info": None + } + + if task_result.state == 'FAILURE': + response["result"] = str(task_result.result) + elif task_result.state == 'SUCCESS': + response["result"] = task_result.result + elif task_result.state == 'PROCESSING': + response["info"] = task_result.info + + return response diff --git a/backend/api/v1/modules/a76/reports/exportacion/descargo/service.py b/backend/api/v1/modules/a76/reports/exportacion/descargo/service.py new file mode 100644 index 00000000..9b004101 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/exportacion/descargo/service.py @@ -0,0 +1,291 @@ + +import shutil +import base64 +import pdfkit +from pathlib import Path +from typing import Tuple, List, Callable, Optional, Dict, Any +from jinja2 import Environment, FileSystemLoader, select_autoescape +from fastapi import HTTPException +from sqlalchemy.orm import Session +from pydantic import BaseModel +from decimal import Decimal + +# --- MODELOS (Imported from system for Header info) --- +from api.v1.modules.a76.invoices.models import InvoiceHeader +from api.v1.modules.a76.general_catalogs.company.models import Company +from api.v1.modules.a76.items.line_items.models import LineItem +from api.v1.modules.a76.items.line_quantities.models import LineQuantity +from api.v1.modules.a76.parts.models import Part +from api.v1.modules.a76.items.models import Item +from api.v1.modules.a76.pedmientos.models import Pedimentos +from sqlalchemy.orm import joinedload, load_only + +# --- SCHEMAS FOR TEMPLATE CONTEXT --- + +class DischargeItemSchema(BaseModel): + # Column 1: Pedimento Info + pedimento_numero: str + pedimento_clave: str + pedimento_fecha_pago: str + + # Column 2: Import Invoice + factura_impo: str + + # Column 3: Part Info + numero_parte: str + descripcion: str + fraccion: str + origen_pref_sector: str # e.g. "CHN-GENERAL" + + # Metrics + cantidad: str + unidad_medida: str + peso_neto: str + + # Values + valor_mn: str + valor_me: str + valor_igi: str + + # Flags + se_pago: str # "0.0" or "Yes"? Image says "0.0" in column "Se Pago"? No, "Se Pago" might be a flag, image key implies payment. + # Image: "Se Pago" column has "0.0"? No, look closer. + # "Value/Monto IGI USD/Dolares" has "0.0". + # "Se Pago" column seems empty or has '1'? + # Wait, looking at image: + # Col: "Se Pago", Row: "0.0"? No that's IGI. + # Let's assume Se Pago is a boolean/string. + # Last col: "Linea Expo". + + se_pago_val: str + linea_expo: str + + # Helper for Jinja (if methods not allowed in pydantic models in template) + def __init__(self, **data): + super().__init__(**data) + +class DischargeContext(BaseModel): + items: List[DischargeItemSchema] + invoice_number: str + company_name: str + company_address: str + company_rfc: str + company_immex: str + + # Totals + total_cantidad: str + total_peso: str + total_valor_mn: str + total_valor_me: str + total_igi: str + +class DescargaReportService: + def formatear_numero(self, valor, decimales: int = 2): + if valor is None: return "0.00" + try: + return "{:,.{}f}".format(float(valor), decimales) + except: return "0.00" + + def __init__(self): + self.template_dir = Path(__file__).parent / "templates" + self.jinja_env = Environment( + loader=FileSystemLoader(self.template_dir), + autoescape=select_autoescape(['html', 'xml']) + ) + self.template = self.jinja_env.get_template('descarga.html') + + def _get_wkhtmltopdf_config(self): + path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf" or "/usr/bin/wkhtmltopdf" + if not Path(path).exists(): + raise RuntimeError("wkhtmltopdf no encontrado.") + return pdfkit.configuration(wkhtmltopdf=path) + + def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> DischargeContext: + try: + if progress_callback: progress_callback(10, "Buscando factura...") + + # Fetch Header for basic info + header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first() + if not header: + raise HTTPException(status_code=404, detail="Factura no encontrada") + + company = db.query(Company).filter(Company.id == company_id).first() + + if progress_callback: progress_callback(30, "Procesando descargas...") + + # --- REAL IMPLEMENTATION --- + # 1. Fetch Export Lines with FA Data + export_lines = db.query(LineItem).filter( + LineItem.item_id == Item.id, + Item.invoice_id == invoice_id + ).options( + joinedload(LineItem.fa_data), + joinedload(LineItem.quantity).load_only(LineQuantity.quantity, LineQuantity.net_weight), + joinedload(LineItem.customs), + joinedload(LineItem.description), + joinedload(LineItem.unit_of_measure_info), + joinedload(LineItem.part_info), # Fetch Part Relation + # joinedload(LineItem.item).joinedload(Item.invoice) # Removed due to missing relationship + ).join(Item).all() + + # 2. Collect unique Import Invoices to bulk fetch + # fa_data.search_invoice stores the "FacturaImpo" number + import_inv_nums = set() + for line in export_lines: + if line.fa_data and line.fa_data.search_invoice: + import_inv_nums.add(line.fa_data.search_invoice) + + # Map InvoiceNumber -> (InvoiceHeader, Pedimento) + import_map = {} + if import_inv_nums: + # We need to find the invoices. Warning: search_invoice is just a string number. + # potentially non-unique across companies, but we filter by current Company. + imp_invoices = db.query(InvoiceHeader).filter( + InvoiceHeader.invoice_number.in_(import_inv_nums), + InvoiceHeader.company_id == company_id, + InvoiceHeader.invoice_type == 'Ingreso' # Assuming Imports are Ingreso/Import + ).options( + joinedload(InvoiceHeader.compliance_mx) + ).all() + + # Fetch Pedimentos for these invoices + ped_ids = {inv.compliance_mx.pedimento_id for inv in imp_invoices if inv.compliance_mx and inv.compliance_mx.pedimento_id} + peds = db.query(Pedimentos).filter(Pedimentos.id.in_(ped_ids)).all() + ped_map = {p.id: p for p in peds} + + for inv in imp_invoices: + ped = None + if inv.compliance_mx and inv.compliance_mx.pedimento_id: + ped = ped_map.get(inv.compliance_mx.pedimento_id) + import_map[inv.invoice_number] = (inv, ped) + + items = [] + + for line in export_lines: + # Defaults + ped_str = "" + ped_clave = "" + ped_fecha = "" + fac_impo = "" + se_pago = "" + valor_igi = 0.0 + + # Linkage + if line.fa_data and line.fa_data.search_invoice: + fac_impo = line.fa_data.search_invoice + if fac_impo in import_map: + inv_imp, ped_imp = import_map[fac_impo] + + if ped_imp: + ped_str = f"{ped_imp.pedimento_number}" + ped_clave = f"{ped_imp.pedimento_code}" + # Format date if exists + # Simple date fallback from header if needed or Pedimento Date logic (revisit model if needed) + pass + + # Calculation logic (Prorate) + qty = float(line.quantity.quantity) if line.quantity else 0.0 + + valor_me = 0.0 + valor_mn = 0.0 + + # Create Schema + items.append(DischargeItemSchema( + pedimento_numero=ped_str, + pedimento_clave=ped_clave, + pedimento_fecha_pago=ped_fecha, + factura_impo=fac_impo, + numero_parte=line.part_info.part_number if hasattr(line, 'part_info') and line.part_info else (str(line.part_number) if line.part_number else "S/N"), + descripcion=line.description.description_spanish if line.description else "S/D", + fraccion=line.customs.fraction if line.customs else "", + origen_pref_sector=f"{line.customs.origin_country or ''} - {line.customs.sector or ''}" if line.customs else "", + cantidad=self.formatear_numero(qty, 3), + unidad_medida=line.unit_of_measure_info.code if line.unit_of_measure_info else "PZA", + peso_neto=self.formatear_numero(float(line.quantity.net_weight) if line.quantity else 0.0, 3), + valor_mn=self.formatear_numero(valor_mn), + valor_me=self.formatear_numero(valor_me), + valor_igi=self.formatear_numero(valor_igi), + se_pago=se_pago or "NO", + se_pago_val=se_pago, + linea_expo=str(line.line_number) + )) + + # Totals + + # Company Address Construction + addr_str = "DIRECCION NO REGISTRADA" + immex_val = "" + + if company: + # Address Logic + if company.addresses: + # Prefer 'main' address, otherwise take the first one + main_addr = next((a for a in company.addresses if a.address_type == 'main'), company.addresses[0]) + + parts = [] + if main_addr.street: parts.append(main_addr.street) + if main_addr.exterior_number: parts.append(f"No. {main_addr.exterior_number}") + if main_addr.neighborhood: parts.append(main_addr.neighborhood) + if main_addr.city: parts.append(main_addr.city) + if main_addr.state: parts.append(main_addr.state) + if main_addr.postal_code: parts.append(f"CP {main_addr.postal_code}") + + if parts: + addr_str = ", ".join(parts) + + # IMMEX Logic + if company.program and "IMMEX" in company.program and company.program_number: + immex_val = company.program_number + + # Calculate Totals + t_cant = sum(float(i.cantidad.replace(",","")) for i in items if i.cantidad) + t_peso = sum(float(i.peso_neto.replace(",","")) for i in items if i.peso_neto) + t_mn = sum(float(i.valor_mn.replace(",","")) for i in items if i.valor_mn) + t_me = sum(float(i.valor_me.replace(",","")) for i in items if i.valor_me) + t_igi = sum(float(i.valor_igi.replace(",","")) for i in items if i.valor_igi) + + return DischargeContext( + items=items, + invoice_number=header.invoice_number or "SIN FOLIO", + company_name=company.name if company else "EMPRESA DESCONOCIDA", + company_address=addr_str, + company_rfc=company.rfc if company else "", + company_immex=immex_val, + total_cantidad=self.formatear_numero(t_cant, 3), + total_peso=self.formatear_numero(t_peso, 3), + total_valor_mn=self.formatear_numero(t_mn), + total_valor_me=self.formatear_numero(t_me), + total_igi=self.formatear_numero(t_igi) + ) + + except Exception as e: + print(f"Error Service Discharge Report: {e}") + raise HTTPException(status_code=500, detail=f"Error: {str(e)}") + + def generar_pdf(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> Tuple[bytes, str, str]: + if progress_callback: progress_callback(5, "Iniciando servicio de reporte...") + + datos = self.obtener_datos(db, invoice_id, company_id, progress_callback) + + if progress_callback: progress_callback(80, "Renderizando plantilla...") + + context = datos.model_dump() + html_content = self.template.render(**context) + nombre = f"Descarga_{datos.invoice_number}.pdf" + + if progress_callback: progress_callback(90, "Generando PDF final...") + + options = { + 'page-size': 'Letter', + 'orientation': 'Landscape', # Correct argument for wkhtmltopdf + 'margin-top': '0.5in', + 'margin-right': '0.5in', + 'margin-bottom': '0.5in', + 'margin-left': '0.5in', + 'encoding': "UTF-8" + } + + pdf = pdfkit.from_string(html_content, False, options=options, configuration=self._get_wkhtmltopdf_config()) + + if progress_callback: progress_callback(100, "Completado") + return pdf, nombre, "application/pdf" diff --git a/backend/api/v1/modules/a76/reports/exportacion/descargo/task.py b/backend/api/v1/modules/a76/reports/exportacion/descargo/task.py new file mode 100644 index 00000000..aa9d7b5e --- /dev/null +++ b/backend/api/v1/modules/a76/reports/exportacion/descargo/task.py @@ -0,0 +1,49 @@ + +from celery import shared_task +from sqlalchemy.orm import Session +from core.database import CoreSessionLocal as SessionLocal +from .service import DescargaReportService +import base64 +import traceback + +@shared_task(bind=True, name="generate_descarga_pdf_task") +def generate_descarga_pdf_task(self, invoice_id: int, company_id: int): + """ + Tarea de Celery para generar el PDF del Reporte de Descarga + """ + db: Session = SessionLocal() + try: + service = DescargaReportService() + + def update_progress(percent, message): + self.update_state( + state='PROCESSING', + meta={'current': percent, 'total': 100, 'status': message} + ) + + pdf_bytes, filename, content_type = service.generar_pdf( + db=db, + invoice_id=invoice_id, + company_id=company_id, + progress_callback=update_progress + ) + + # Retornar el PDF en base64 para que el front lo descargue + pdf_b64 = base64.b64encode(pdf_bytes).decode('utf-8') + + return { + "status": "success", + "file_name": filename, + "content": pdf_b64, + "media_type": content_type, + "message": "Reporte generado correctamente" + } + + except Exception as e: + self.update_state( + state='FAILURE', + meta={'exc_type': type(e).__name__, 'exc_message': str(e)} + ) + raise e + finally: + db.close() diff --git a/backend/api/v1/modules/a76/reports/exportacion/descargo/templates/descarga.html b/backend/api/v1/modules/a76/reports/exportacion/descargo/templates/descarga.html new file mode 100644 index 00000000..bb5b2c64 --- /dev/null +++ b/backend/api/v1/modules/a76/reports/exportacion/descargo/templates/descarga.html @@ -0,0 +1,179 @@ + + + + + + Descarga de Factura {{ invoice_number }} + + + + + + + + + + + + +
+ DESCARGA DE LA FACTURA: {{ invoice_number }} + + {{ company_name }}
+ {{ company_address }}
+ R.F.C.: {{ company_rfc }}, IMMEX: {{ company_immex }} +
+ Page/Página: Of/de +
+ +
La factura se descargo de:
+ + + + + + + + + + + + + + + + + + + + + + + + {% for item in items %} + + + + + + + + + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + + + + + + + + +
No. Pedimento
Clave Fecha de Pago
Import Invoice/
Factura de Impo.
Part Number/No. de Parte Componente
Description/Descripción + (Origen-Prefer.-Sector)
Quantity/
Cantidad U.M.
Net Weight/
Peso Neto (KGS)
Value/Valor M.N.
MXP/Pesos
Value/Valor M.E.
USD/Dolares
Value/Monto IGI
USD/Dolares
Se
Pagó
Linea Expo
Expo Line
Comp. Temporales:
+ {{ item.pedimento_numero }}
+ {{ item.pedimento_clave }}    {{ item.pedimento_fecha_pago }} +
{{ item.factura_impo }} + {{ item.numero_parte }}
+ {{ item.descripcion }}
+ {{ item.fraccion }}
{{ item.origen_pref_sector }}
+
+ {{ item.cantidad }} {{ item.unidad_medida }} + {{ item.peso_neto }}{{ item.valor_mn }}{{ item.valor_me }}{{ item.valor_igi }}{{ item.se_pago }}{{ item.linea_expo }}
Totales de los Comp. Temporales:{{ total_cantidad }}{{ total_peso }}{{ total_valor_mn }}{{ total_valor_me }}{{ total_igi }}
TOTALES:{{ total_cantidad }}{{ total_peso }}{{ total_valor_mn }}{{ total_valor_me }}{{ total_igi }}
+ + + + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py index 34cbc9ef..c84043b1 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py @@ -61,7 +61,7 @@ class ConsolidadoImportacionMexService: self.template = self.jinja_env.get_template("cons_mex_ver.html") def _get_wkhtmltopdf_config(self): - path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf" + path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf" or "/usr/bin/wkhtmltopdf" if not Path(path).exists(): raise RuntimeError("wkhtmltopdf no encontrado.") return pdfkit.configuration(wkhtmltopdf=path) diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py index 619ae2f5..5ad9c5b5 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py @@ -97,7 +97,7 @@ class FacturaImportacionMexService: return title def _get_wkhtmltopdf_config(self): - path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf" + path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf" or "/usr/bin/wkhtmltopdf" if not Path(path).exists(): raise RuntimeError("wkhtmltopdf no encontrado.") return pdfkit.configuration(wkhtmltopdf=path) diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 24220d6d..4eb5e96b 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -130,4 +130,11 @@ router.include_router( consolidated_reports_router, prefix="/a76/reports/importacion/consolidados", tags=["a76 / reports"] +) + +from .reports.exportacion.descargo.routes import router as discharge_reports_router +router.include_router( + discharge_reports_router, + prefix="/a76/reports/exportacion/descargo", + tags=["a76 / reports"] ) \ No newline at end of file diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index c118db31..91207908 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -10,7 +10,8 @@ celery_app = Celery( backend=valkey_url, include=[ "api.v1.modules.a76.reports.importacion.facturas.task", - "api.v1.modules.a76.reports.importacion.consolidados.task" + "api.v1.modules.a76.reports.importacion.consolidados.task", + "api.v1.modules.a76.reports.exportacion.descargo.task" ] # Ruta al módulo donde están las tareas ) diff --git a/frontend/src/lib/api/dashboard/a76/reports/reports-aviso-consolidado.ts b/frontend/src/lib/api/dashboard/a76/reports/reports-aviso-consolidado.ts new file mode 100644 index 00000000..363b1daf --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/reports/reports-aviso-consolidado.ts @@ -0,0 +1,35 @@ + +const BASE_URL = import.meta.env.VITE_API_URL || ''; + +export const avisoConsolidadoReportsApi = { + + triggerPdfGeneration: async (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ company_id: companyId.toString() }); + const endpoint = `${BASE_URL}/v1/a76/reports/exportacion/aviso_consolidado/${invoiceId}/download-async?${params.toString()}`; + + const token = localStorage.getItem('access_token'); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) throw new Error('Error al iniciar la generación del Aviso Consolidado'); + return await response.json(); + }, + + getTaskStatus: async (taskId: string) => { + const endpoint = `${BASE_URL}/v1/a76/reports/exportacion/aviso_consolidado/tasks/${taskId}`; + + const token = localStorage.getItem('access_token'); + const response = await fetch(endpoint, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!response.ok) throw new Error('Error al consultar estado del Aviso Consolidado'); + return await response.json(); + } +}; diff --git a/frontend/src/lib/api/dashboard/a76/reports/reports-descargo.ts b/frontend/src/lib/api/dashboard/a76/reports/reports-descargo.ts new file mode 100644 index 00000000..493653fe --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/reports/reports-descargo.ts @@ -0,0 +1,36 @@ + +const BASE_URL = import.meta.env.VITE_API_URL || ''; + +export const dischargeReportsApi = { + + triggerPdfGeneration: async (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ company_id: companyId.toString() }); + // Endpoint matches routes.py + const endpoint = `${BASE_URL}/v1/a76/reports/exportacion/descargo/${invoiceId}/download-async?${params.toString()}`; + + const token = localStorage.getItem('access_token'); + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) throw new Error('Error al iniciar la generación del Reporte de Descarga'); + return await response.json(); + }, + + getTaskStatus: async (taskId: string) => { + const endpoint = `${BASE_URL}/v1/a76/reports/exportacion/descargo/tasks/${taskId}`; + + const token = localStorage.getItem('access_token'); + const response = await fetch(endpoint, { + method: 'GET', + headers: { 'Authorization': `Bearer ${token}` } + }); + + if (!response.ok) throw new Error('Error al consultar estado del Reporte de Descarga'); + return await response.json(); + } +}; diff --git a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte index 36fc7e38..ee6e7d10 100644 --- a/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte +++ b/frontend/src/lib/components/dashboard/invoices/pdf-progress-dialog.svelte @@ -63,9 +63,9 @@ } else if (response.state === 'FAILURE') { hasError = true; - statusMessage = "Error al generar el PDF"; + statusMessage = response.result ? `Error: ${response.result}` : "Error al generar el PDF"; stopPolling(); - toast.error("Falló la generación del PDF"); + toast.error("Falló la generación del PDF: " + (response.result || "")); } } catch (error) { console.error("Error polling task status:", error); diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte index 29fa17a9..63e76f31 100644 --- a/frontend/src/routes/dashboard/invoices/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/+page.svelte @@ -6,6 +6,7 @@ import { invoicesApi, type Invoice, type OperationType } from '$lib/api/dashboard/a76/invoices'; import { invoicesReportsApi } from '$lib/api/dashboard/a76/reports/reports-invoices'; import { consolidatedReportsApi } from '$lib/api/dashboard/a76/reports/reports-consolidated'; + import { dischargeReportsApi } from '$lib/api/dashboard/a76/reports/reports-descargo'; import DataTable from '$lib/components/dashboard/invoices/data-table.svelte'; import { createColumns } from '$lib/components/dashboard/invoices/columns.js'; import * as Card from '$lib/components/ui/card'; @@ -15,7 +16,7 @@ import type { PageData } from './$types'; import { browser } from '$app/environment'; import { companyStore } from '$lib/stores/company.svelte'; - import { Plus, RefreshCw, FileText, RotateCcw, Boxes } from 'lucide-svelte'; + import { Plus, RefreshCw, FileText, RotateCcw, Boxes, ClipboardList } from 'lucide-svelte'; // IMPORTANTE: Asegúrate de tener instalada svelte-sonner para las notificaciones import { toast } from "svelte-sonner"; @@ -376,6 +377,30 @@ } } + async function handleDownloadDescargo(invoice: any) { + if (!companyStore.activeCompany) { + toast.error("No hay empresa seleccionada"); + return; + } + + try { + // 1. Trigger: Iniciar la tarea en Celery + const { task_id } = await dischargeReportsApi.triggerPdfGeneration( + invoice.id, + companyStore.activeCompany.id + ); + + // 2. Abrir diálogo de progreso + currentTaskId = task_id; + currentStatusFunction = dischargeReportsApi.getTaskStatus; + showProgressDialog = true; + + } catch (error) { + console.error(error); + toast.error("No se pudo iniciar la descarga del reporte PEPS"); + } + } + async function handleDownloadConsolidated(invoice: any) { if (!companyStore.activeCompany) { toast.error("No hay empresa seleccionada"); @@ -656,6 +681,10 @@ Consolidado +