Se integro la plantilla de aviso de consolidado
This commit is contained in:
@@ -27,9 +27,8 @@ class LineQuantityBase(BaseModel):
|
||||
gross_weight: Optional[Decimal] = Field(None, description="Gross weight (PESOBRUTO)")
|
||||
|
||||
# Packaging
|
||||
package_key: Optional[str] = Field(None, max_length=5, description="Package key (CLAVEBULTOS)")
|
||||
package_id: Optional[int] = Field(None, description="Package ID (GBultos)")
|
||||
package_quantity: Optional[int] = Field(None, description="Package quantity (CANTBULTOS)")
|
||||
package_description: Optional[str] = Field(None, max_length=40, description="Package description (DESCBULTOS)")
|
||||
container_quantity: Optional[int] = Field(None, description="Container quantity (CANTBULCONT)")
|
||||
container_description: Optional[str] = Field(None, max_length=40, description="Container description (DESCCONTENEDOR)")
|
||||
box_count: Optional[str] = Field(None, max_length=30, description="Box count (NOCAJAS)")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
|
||||
from typing import Dict, Any
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from celery.result import AsyncResult
|
||||
from core.celery_app import celery_app
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from .task import generar_pdf_aviso_consolidado_exp_async
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/tasks/{task_id}")
|
||||
async def get_task_status(
|
||||
task_id: str,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db)
|
||||
):
|
||||
task_result = AsyncResult(task_id, app=celery_app)
|
||||
|
||||
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
|
||||
|
||||
@router.post("/{invoice_id}/download-async")
|
||||
async def trigger_descarga_aviso_consolidado_exp(
|
||||
invoice_id: int,
|
||||
company_id: int = Query(..., description="ID de la empresa"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
db: Session = Depends(get_core_db)
|
||||
):
|
||||
validate_access_to_resource(db, company_id, current_user)
|
||||
task = generar_pdf_aviso_consolidado_exp_async.delay(invoice_id, company_id)
|
||||
return {"task_id": task.id, "message": "Generación iniciada"}
|
||||
@@ -0,0 +1,305 @@
|
||||
|
||||
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
|
||||
|
||||
# --- MODELOS ---
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx, InvoiceFinancials, InvoiceLogistics
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker, CustomsBrokerPersonnel
|
||||
from api.v1.modules.a76.clients_and_providers.models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
|
||||
# --- SCHEMAS FOR TEMPLATE CONTEXT ---
|
||||
class EmpresaSchema(BaseModel):
|
||||
rfc: str
|
||||
razon_social: str
|
||||
direccion_completa: str
|
||||
tax_id: Optional[str] = None # Extra info just in case
|
||||
|
||||
class PersonaSchema(BaseModel):
|
||||
nombre: str
|
||||
rfc: str
|
||||
curp: str
|
||||
|
||||
class AvisoSchema(BaseModel):
|
||||
pedimento_completo: str
|
||||
tipo_operacion: str
|
||||
clave_pedimento: str
|
||||
acus_valor: str
|
||||
aduana_seccion: str
|
||||
numero_remesa: str
|
||||
peso_bruto: str
|
||||
codigo_aceptacion: str
|
||||
codigo_barras_b64: Optional[str] = None
|
||||
clave_seccion: str
|
||||
marcas_numeros_bultos: str
|
||||
candados: List[str]
|
||||
vehiculo_placas: str
|
||||
vehiculo_tipo: str
|
||||
observaciones: str
|
||||
numero_certificado: str
|
||||
tipo_documento: str # NEW: Invoice Type
|
||||
firma_electronica: str
|
||||
|
||||
class AvisoConsolidadoContext(BaseModel):
|
||||
aviso: AvisoSchema
|
||||
empresa: EmpresaSchema
|
||||
agente: PersonaSchema
|
||||
mandatario: PersonaSchema
|
||||
|
||||
class AvisoConsolidadoExportacionService:
|
||||
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('avcon_exp.html')
|
||||
|
||||
def _get_wkhtmltopdf_config(self):
|
||||
path = shutil.which("wkhtmltopdf") or "/usr/local/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) -> AvisoConsolidadoContext:
|
||||
try:
|
||||
if progress_callback: progress_callback(10, "Buscando factura...")
|
||||
|
||||
# Fetch minimal real data if possible, or use placeholders as requested
|
||||
header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first()
|
||||
if not header:
|
||||
# We can't strictly raise 404 if we want to support testing with non-existent IDs for pure UI check,
|
||||
# but valid workflow requires a real invoice. Raising 404 is better practice.
|
||||
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, "Preparando datos...")
|
||||
|
||||
# --- FETCHING REAL DATA ---
|
||||
|
||||
# 1. Compliance & Pedimento
|
||||
compliance = header.compliance_mx
|
||||
pedimento = None
|
||||
if compliance and compliance.pedimento_id:
|
||||
pedimento = db.query(Pedimentos).filter(Pedimentos.id == compliance.pedimento_id).first()
|
||||
|
||||
# Pedimento Completo Construction
|
||||
pedimento_txt = "S/P"
|
||||
clave_ped = ""
|
||||
if pedimento:
|
||||
# Format: YY OFF LIC NUMBER
|
||||
year = pedimento.year or ""
|
||||
office = pedimento.customs_office or ""
|
||||
lic = pedimento.license or ""
|
||||
num = pedimento.pedimento_number or ""
|
||||
pedimento_txt = f"{year} {office} {lic} {num}"
|
||||
clave_ped = pedimento.pedimento_code or ""
|
||||
|
||||
# 2. Company Address
|
||||
direccion_empresa = "DOMICILIO NO REGISTRADO"
|
||||
if company and company.addresses:
|
||||
# Try to find fiscal address or first available
|
||||
addr = company.addresses[0] # Default
|
||||
# TODO: Check if there's a specific flag for fiscal address in submodel
|
||||
|
||||
parts = []
|
||||
if addr.street: parts.append(addr.street)
|
||||
if addr.exterior_number: parts.append(f"No. {addr.exterior_number}")
|
||||
if addr.interior_number: parts.append(f"Int. {addr.interior_number}")
|
||||
if addr.neighborhood: parts.append(f"Col. {addr.neighborhood}")
|
||||
if addr.postal_code: parts.append(f"CP {addr.postal_code}")
|
||||
if addr.city: parts.append(addr.city)
|
||||
if addr.state: parts.append(addr.state)
|
||||
if addr.country: parts.append(addr.country)
|
||||
|
||||
if parts:
|
||||
direccion_empresa = ", ".join(parts).upper()
|
||||
|
||||
# Determine Mexican Entity based on Operation Type
|
||||
# IMP -> Client (Sold To/Consignee)
|
||||
# EXP -> Company (Tenant)
|
||||
|
||||
target_entity_data = {
|
||||
"rfc": getattr(company, 'rfc', "") or "",
|
||||
"razon_social": getattr(company, 'name', "") or "",
|
||||
"direccion_completa": direccion_empresa
|
||||
}
|
||||
|
||||
op_type = header.operation_type.upper() if header.operation_type else "EXP"
|
||||
|
||||
if op_type == "IMP" and compliance and compliance.sold_to_id:
|
||||
# Fetch Client Data
|
||||
client_id = compliance.sold_to_id
|
||||
client_obj = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
|
||||
if client_obj:
|
||||
# Fetch Address
|
||||
c_addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first()
|
||||
# Fetch Fiscal Data (RFC)
|
||||
c_prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first()
|
||||
|
||||
c_rfc = ""
|
||||
if c_prog and c_prog.tax_id: c_rfc = c_prog.tax_id
|
||||
elif hasattr(client_obj, 'rfc'): c_rfc = client_obj.rfc
|
||||
|
||||
c_dir_str = "DOMICILIO NO REGISTRADO"
|
||||
if c_addr:
|
||||
parts_c = []
|
||||
if c_addr.streets: parts_c.append(c_addr.streets)
|
||||
if c_addr.exterior_number: parts_c.append(f"No. {c_addr.exterior_number}")
|
||||
if c_addr.neighborhood: parts_c.append(f"Col. {c_addr.neighborhood}")
|
||||
if c_addr.city: parts_c.append(c_addr.city)
|
||||
if c_addr.state: parts_c.append(c_addr.state)
|
||||
if c_addr.postal_code: parts_c.append(f"CP {c_addr.postal_code}")
|
||||
if parts_c:
|
||||
c_dir_str = ", ".join(parts_c).upper()
|
||||
|
||||
target_entity_data = {
|
||||
"rfc": c_rfc or "",
|
||||
"razon_social": client_obj.name or client_obj.short_name or "",
|
||||
"direccion_completa": c_dir_str
|
||||
}
|
||||
|
||||
empresa = EmpresaSchema(
|
||||
rfc=target_entity_data["rfc"],
|
||||
razon_social=target_entity_data["razon_social"],
|
||||
direccion_completa=target_entity_data["direccion_completa"]
|
||||
)
|
||||
|
||||
|
||||
|
||||
# 3. Datos Aviso (Invoice/Compliance/Logistics/Financials)
|
||||
financials = header.financials
|
||||
logistics = header.logistics
|
||||
|
||||
# Peso Bruto
|
||||
peso_bruto_val = "0.0"
|
||||
if financials and financials.gross_weight:
|
||||
peso_bruto_val = f"{financials.gross_weight:,.2f}"
|
||||
elif pedimento and pedimento.gross_weight:
|
||||
peso_bruto_val = f"{pedimento.gross_weight:,.2f}"
|
||||
|
||||
# Candados (Seals)
|
||||
candados_list = []
|
||||
if logistics and logistics.seal_number:
|
||||
# Split by comma or space if multiple
|
||||
candados_list = [s.strip() for s in logistics.seal_number.replace(',', ' ').split() if s.strip()]
|
||||
|
||||
# Vehiculo
|
||||
placas_val = ""
|
||||
tipo_veh_val = ""
|
||||
if logistics:
|
||||
placas_val = logistics.license_plate or logistics.vehicle_num or logistics.trailer_num or ""
|
||||
tipo_veh_val = logistics.transport_type or ""
|
||||
|
||||
aviso = AvisoSchema(
|
||||
pedimento_completo=pedimento_txt,
|
||||
tipo_operacion=header.operation_type.upper() if header.operation_type else "EXP",
|
||||
clave_pedimento=clave_ped,
|
||||
acus_valor=compliance.edocument if (compliance and compliance.edocument) else "",
|
||||
aduana_seccion=compliance.aduana if (compliance and compliance.aduana) else "",
|
||||
numero_remesa=str(compliance.remesa) if (compliance and compliance.remesa) else "",
|
||||
peso_bruto=peso_bruto_val,
|
||||
codigo_aceptacion="", # TODO: Clarify source. Using empty for now or Edocument?
|
||||
codigo_barras_b64=None,
|
||||
clave_seccion=compliance.aduana if (compliance and compliance.aduana) else "", # Using Aduana as Section Key
|
||||
marcas_numeros_bultos=f"{financials.bundle_count} BULTOS" if (financials and financials.bundle_count) else "1 BULTOS",
|
||||
candados=candados_list,
|
||||
vehiculo_placas=placas_val,
|
||||
vehiculo_tipo=tipo_veh_val,
|
||||
observaciones=header.observation_es or "",
|
||||
numero_certificado=compliance.certificate_number if (compliance and compliance.certificate_number) else "",
|
||||
tipo_documento=header.document_type or "FACTURA", # Default
|
||||
firma_electronica=compliance.electronic_signature if (compliance and compliance.electronic_signature) else ""
|
||||
)
|
||||
|
||||
# 4. Agente Aduanal
|
||||
nombre_agente = ""
|
||||
rfc_agente = ""
|
||||
curp_agente = ""
|
||||
|
||||
if compliance and compliance.customs_broker_id:
|
||||
broker = db.query(CustomsBroker).filter(CustomsBroker.id == compliance.customs_broker_id).first()
|
||||
if broker:
|
||||
nombre_agente = broker.name or ""
|
||||
rfc_agente = broker.tax_id or ""
|
||||
curp_agente = broker.personal_id or ""
|
||||
|
||||
agente = PersonaSchema(
|
||||
nombre=nombre_agente,
|
||||
rfc=rfc_agente,
|
||||
curp=curp_agente
|
||||
)
|
||||
|
||||
# 5. Mandatario (CustomsBrokerPersonnel)
|
||||
mandatario = PersonaSchema(nombre="", rfc="", curp="")
|
||||
|
||||
if broker:
|
||||
# Try to find personnel associated with this broker
|
||||
# Using direct query to ensure specific order if needed, typically just the first valid one
|
||||
personnel = db.query(CustomsBrokerPersonnel).filter(
|
||||
CustomsBrokerPersonnel.customs_broker_id == broker.id
|
||||
).first()
|
||||
|
||||
if personnel:
|
||||
# Construct name if main field is empty
|
||||
full_name = personnel.name
|
||||
if not full_name:
|
||||
parts = []
|
||||
if personnel.first_name: parts.append(personnel.first_name)
|
||||
if personnel.last_name: parts.append(personnel.last_name)
|
||||
if personnel.middle_name: parts.append(personnel.middle_name)
|
||||
full_name = " ".join(parts)
|
||||
|
||||
mandatario = PersonaSchema(
|
||||
nombre=full_name or "",
|
||||
rfc=personnel.tax_id or "",
|
||||
curp=personnel.personal_id or ""
|
||||
)
|
||||
|
||||
return AvisoConsolidadoContext(
|
||||
aviso=aviso,
|
||||
empresa=empresa,
|
||||
agente=agente,
|
||||
mandatario=mandatario
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error Service A76 Export Aviso Consolidado: {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"AvisoConsolidado_Exp_{invoice_id}.pdf"
|
||||
|
||||
if progress_callback: progress_callback(90, "Generando PDF final...")
|
||||
|
||||
options = {
|
||||
'page-size': 'Letter',
|
||||
'margin-top': '0.5in',
|
||||
'margin-right': '0.5in',
|
||||
'margin-bottom': '0.5in',
|
||||
'margin-left': '0.5in',
|
||||
'encoding': "UTF-8",
|
||||
'enable-local-file-access': None
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from core.celery_app import celery_app
|
||||
from core.database import CoreSessionLocal
|
||||
|
||||
from .service import AvisoConsolidadoExportacionService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@celery_app.task(name="generar_pdf_aviso_consolidado_exp_async", bind=True)
|
||||
def generar_pdf_aviso_consolidado_exp_async(self, invoice_id: int, company_id: int):
|
||||
# 1. Abrimos conexión a la DB
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
logger.info(f"Worker procesando Aviso Consolidado Exp {invoice_id}...")
|
||||
|
||||
# 2. Instanciamos el servicio
|
||||
service = AvisoConsolidadoExportacionService()
|
||||
|
||||
# Update state to PROCESSING
|
||||
self.update_state(state='PROCESSING', meta={'current': 5, 'total': 100, 'status': 'Iniciando generación...'})
|
||||
|
||||
def progress_callback(progress: int, status: str):
|
||||
self.update_state(state='PROCESSING', meta={'current': progress, 'total': 100, 'status': status})
|
||||
|
||||
# 3. Generamos los bytes del PDF
|
||||
pdf_bytes, nombre, media_type = service.generar_pdf(
|
||||
db=db,
|
||||
invoice_id=invoice_id,
|
||||
company_id=company_id,
|
||||
progress_callback=progress_callback
|
||||
)
|
||||
|
||||
# 4. Codificamos a base64
|
||||
pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"file_name": nombre,
|
||||
"content": pdf_base64,
|
||||
"media_type": media_type
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error en Celery Worker Aviso Consolidado Exp: {str(e)}")
|
||||
return {"status": "error", "message": str(e)}
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,357 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es" xml:lang="es">
|
||||
|
||||
<head>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
|
||||
<title>Aviso Consolidado - {{ aviso.pedimento_completo }}</title>
|
||||
<style type="text/css">
|
||||
/* ESTILOS EXACTOS DE SCAPII (Copiados de tu archivo) */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: Tahoma, sans-serif;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.titulo {
|
||||
font-size: 14pt;
|
||||
padding: 3pt 0 0 6pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.grande {
|
||||
font-size: 13pt;
|
||||
padding-left: 5pt;
|
||||
}
|
||||
|
||||
.medio-bold {
|
||||
font-size: 9pt;
|
||||
font-weight: bold;
|
||||
padding: 3pt 0 0 3pt;
|
||||
}
|
||||
|
||||
.normal {
|
||||
font-size: 8pt;
|
||||
}
|
||||
|
||||
.small-bold {
|
||||
font-size: 8pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tiny-bold {
|
||||
font-size: 7pt;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tiny {
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.mini {
|
||||
font-size: 5pt;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.border {
|
||||
border: 1pt solid black;
|
||||
}
|
||||
|
||||
/* Ajusté a negro puro para que se vea como el formato oficial */
|
||||
|
||||
/* Utilidades de Padding/Margin del original */
|
||||
.p-t-1 {
|
||||
padding-top: 1pt;
|
||||
}
|
||||
|
||||
.p-t-2 {
|
||||
padding-top: 2pt;
|
||||
}
|
||||
|
||||
.p-t-3 {
|
||||
padding-top: 3pt;
|
||||
}
|
||||
|
||||
.p-l-2 {
|
||||
padding-left: 2pt;
|
||||
}
|
||||
|
||||
.p-l-3 {
|
||||
padding-left: 3pt;
|
||||
}
|
||||
|
||||
.p-r-2 {
|
||||
padding-right: 2pt;
|
||||
}
|
||||
|
||||
.h-10 {
|
||||
height: 10pt;
|
||||
}
|
||||
|
||||
.h-14 {
|
||||
height: 14pt;
|
||||
}
|
||||
|
||||
/* Estilos específicos para el Grid del Aviso */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.bg-grey {
|
||||
background-color: #E4E4E4;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-size: 8pt;
|
||||
font-weight: bold;
|
||||
background-color: #CCCCCC;
|
||||
text-align: center;
|
||||
border: 1pt solid black;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
border-bottom: 1pt solid black;
|
||||
min-height: 10pt;
|
||||
}
|
||||
|
||||
.cell-pad {
|
||||
padding: 2pt 4pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<table cellspacing="0">
|
||||
<tr>
|
||||
<td class="border bg-grey center" style="width: 80%;">
|
||||
<p class="titulo center">AVISO CONSOLIDADO</p>
|
||||
</td>
|
||||
<td class="border center" style="width: 20%;">
|
||||
<p class="normal">Página <span class="small-bold">1</span> de <span class="small-bold">1</span></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="border cell-pad" style="width: 40%;">
|
||||
<span class="tiny-bold">NUM. PEDIMENTO: </span>
|
||||
<span class="tiny">{{ aviso.pedimento_completo }}</span>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 20%;">
|
||||
<span class="tiny-bold">T. OPER: </span>
|
||||
<span class="tiny">{{ aviso.tipo_operacion }}</span>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 20%;">
|
||||
<span class="tiny-bold">CVE. PEDIMENTO: </span>
|
||||
<span class="tiny">{{ aviso.clave_pedimento }}</span>
|
||||
</td>
|
||||
<td class="border bg-grey center" style="width: 20%;">
|
||||
<span class="tiny-bold">CERTIFICACIONES</span>
|
||||
<br>
|
||||
<span class="tiny-bold" style="font-size: 6pt;">TIPO: {{ aviso.tipo_documento }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="border cell-pad" colspan="3">
|
||||
<span class="tiny-bold">NUMERO DE ACUSE DE VALOR: </span>
|
||||
<span class="tiny">{{ aviso.acus_valor }}</span>
|
||||
</td>
|
||||
<td class="border" rowspan="4" style="vertical-align: top;">
|
||||
<p class="mini"> </p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border cell-pad" style="width: 25%;">
|
||||
<span class="tiny-bold">ADUANA E/S: </span>
|
||||
<span class="tiny">{{ aviso.aduana_seccion }}</span>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 25%;">
|
||||
<span class="tiny-bold">NUM. REMESA: </span>
|
||||
<span class="tiny">{{ aviso.numero_remesa }}</span>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 30%;">
|
||||
<span class="tiny-bold">PESO BRUTO: </span>
|
||||
<span class="tiny">{{ aviso.peso_bruto }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="3" class="section-header">DATOS DEL IMPORTADOR/EXPORTADOR</td>
|
||||
</tr>
|
||||
<tr style="height: 40pt;">
|
||||
<td colspan="3" class="border cell-pad" style="vertical-align: top;">
|
||||
<div style="float: left; width: 30%;">
|
||||
<p class="tiny-bold">RFC:</p>
|
||||
<p class="tiny">{{ empresa.rfc }}</p>
|
||||
</div>
|
||||
<div style="float: left; width: 70%;">
|
||||
<p class="tiny-bold">NOMBRE, DENOMINACION O RAZON SOCIAL:</p>
|
||||
<p class="tiny">{{ empresa.razon_social }}</p>
|
||||
<p class="mini">{{ empresa.direccion_completa }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr style="height: 60pt;">
|
||||
<td class="border cell-pad" style="width: 25%; vertical-align: top;">
|
||||
<p class="tiny-bold">CODIGO DE ACEPTACION:</p>
|
||||
<p class="normal center p-t-3">{{ aviso.codigo_aceptacion }}</p>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 55%; vertical-align: top;">
|
||||
<p class="tiny-bold">CODIGO DE BARRAS</p>
|
||||
<div class="center p-t-2">
|
||||
{% if aviso.codigo_barras_b64 %}
|
||||
<img src="{{ aviso.codigo_barras_b64 }}" style="height: 40pt; max-width: 90%;" />
|
||||
{% else %}
|
||||
<br><br><br>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="border cell-pad" style="width: 20%; vertical-align: top;">
|
||||
<p class="tiny-bold">CLAVE DE LA SECCION ADUANERA DE DESPACHO:</p>
|
||||
<p class="grande center p-t-3">{{ aviso.clave_seccion }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="section-header" style="text-align: left; padding-left: 5pt;">MARCAS, NUMEROS Y TOTAL DE BULTOS:
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border cell-pad" style="height: 20pt; vertical-align: top;">
|
||||
<p class="tiny">{{ aviso.marcas_numeros_bultos }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="section-header"
|
||||
style="width: 25%; text-align: left; padding-left: 5pt; background-color: #E4E4E4;">NUMERO DE CANDADO:
|
||||
</td>
|
||||
<td class="border center tiny" style="width: 15%;">{{ aviso.candados[0] if aviso.candados|length > 0 }}</td>
|
||||
<td class="border center tiny" style="width: 15%;">{{ aviso.candados[1] if aviso.candados|length > 1 }}</td>
|
||||
<td class="border center tiny" style="width: 15%;">{{ aviso.candados[2] if aviso.candados|length > 2 }}</td>
|
||||
<td class="border center tiny" style="width: 15%;">{{ aviso.candados[3] if aviso.candados|length > 3 }}</td>
|
||||
<td class="border center tiny" style="width: 15%;">{{ aviso.candados[4] if aviso.candados|length > 4 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="section-header"
|
||||
style="text-align: left; padding-left: 5pt; background-color: #E4E4E4; border-bottom: 0;">1RA. REVISION
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border h-14"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="section-header"
|
||||
style="text-align: left; padding-left: 5pt; background-color: #E4E4E4; border-bottom: 0;">2DA. REVISION
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border h-14"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="section-header"
|
||||
style="width: 25%; text-align: left; padding-left: 5pt; background-color: #CCCCCC;">NUMERO/TIPO:</td>
|
||||
<td class="border cell-pad tiny" style="width: 25%;">{{ aviso.vehiculo_placas }}</td>
|
||||
<td class="border cell-pad tiny" style="width: 25%;">{{ aviso.vehiculo_tipo }}</td>
|
||||
<td class="border cell-pad tiny" style="width: 25%;"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="section-header" style="text-align: center; background-color: #CCCCCC;">OBSERVACIONES</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="border" style="height: 100pt; vertical-align: top; padding: 5pt;">
|
||||
<p class="tiny">{{ aviso.observaciones }}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<table cellspacing="0" style="margin-top: -1pt;">
|
||||
<tr>
|
||||
<td class="border cell-pad" style="vertical-align: top; height: 90pt;">
|
||||
<p class="tiny-bold">AGENTE ADUANAL, APODERADO ADUANAL:</p>
|
||||
|
||||
<div style="margin-top: 5pt;">
|
||||
<span class="tiny-bold">NOMBRE: </span>
|
||||
<span class="tiny">{{ agente.nombre }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2pt;">
|
||||
<div style="display: inline-block; width: 45%;">
|
||||
<span class="tiny-bold">RFC: </span>
|
||||
<span class="tiny">{{ agente.rfc }}</span>
|
||||
</div>
|
||||
<div style="display: inline-block; width: 50%;">
|
||||
<span class="tiny-bold">CURP: </span>
|
||||
<span class="tiny">{{ agente.curp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 10pt;">
|
||||
<span class="tiny-bold">MANDATARIO/PERSONA AUTORIZADA:</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2pt;">
|
||||
<span class="tiny-bold">NOMBRE: </span>
|
||||
<span class="tiny">{{ mandatario.nombre }}</span>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2pt;">
|
||||
<div style="display: inline-block; width: 45%;">
|
||||
<span class="tiny-bold">RFC: </span>
|
||||
<span class="tiny">{{ mandatario.rfc }}</span>
|
||||
</div>
|
||||
<div style="display: inline-block; width: 50%;">
|
||||
<span class="tiny-bold">CURP: </span>
|
||||
<span class="tiny">{{ mandatario.curp }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 5pt; border-top: 1pt solid black; padding-top: 2pt;">
|
||||
<span class="tiny-bold">NUMERO DE SERIE DEL CERTIFICADO: </span>
|
||||
<span class="tiny">{{ aviso.numero_certificado }}</span>
|
||||
</div>
|
||||
<div style="margin-top: 2pt;">
|
||||
<span class="tiny-bold">e.firma: </span>
|
||||
<p class="mini" style="word-wrap: break-word;">{{ aviso.firma_electronica }}</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="mini center p-t-5">*********************************************************************** FIN DE LA
|
||||
IMPRESION ***********************************************************************</p>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -316,7 +316,7 @@ class FacturaImportacionMexService:
|
||||
cantidad_importacion=self.formatear_numero(qty.quantity if qty else 0),
|
||||
unidad_medida=qty.weight_unit if qty else "PZA",
|
||||
cantidad_bultos=int(qty.package_quantity) if qty and qty.package_quantity else 0,
|
||||
clave_bultos=(qty.package_key or "") if qty else "",
|
||||
clave_bultos=(qty.package_info.key if (qty and qty.package_info and qty.package_info.key) else "PZA"),
|
||||
peso_neto=self.formatear_numero(qty.net_weight if qty else 0),
|
||||
peso_bruto=self.formatear_numero(qty.gross_weight if qty else 0),
|
||||
valor_costo_unitario=self.formatear_numero(v_unitario),
|
||||
|
||||
@@ -52,6 +52,7 @@ from api.v1.modules.public.reference_data.material_types.routes import router as
|
||||
from .reports.importacion.facturas.routes import router as invoices_reports_router
|
||||
from .reports.importacion.consolidados.routes import router as consolidated_reports_router
|
||||
from .reports.importacion.packing_list.routes import router as packing_list_router
|
||||
from .reports.exportacion.aviso_consolidado.routes import router as aviso_consolidado_export_router
|
||||
|
||||
|
||||
|
||||
@@ -138,4 +139,10 @@ router.include_router(
|
||||
packing_list_router,
|
||||
prefix="/a76/reports/importacion/packing-lists",
|
||||
tags=["a76 / reports"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
aviso_consolidado_export_router,
|
||||
prefix="/a76/reports/exportacion/aviso_consolidado",
|
||||
tags=["a76 / reports"]
|
||||
)
|
||||
@@ -11,7 +11,8 @@ celery_app = Celery(
|
||||
include=[
|
||||
"api.v1.modules.a76.reports.importacion.facturas.task",
|
||||
"api.v1.modules.a76.reports.importacion.consolidados.task",
|
||||
"api.v1.modules.a76.reports.importacion.packing_list.task"
|
||||
"api.v1.modules.a76.reports.importacion.packing_list.task",
|
||||
"api.v1.modules.a76.reports.exportacion.aviso_consolidado.task"
|
||||
] # Ruta al módulo donde están las tareas
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any, Dict
|
||||
|
||||
from fastapi import Request, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
||||
|
||||
@@ -37,7 +38,7 @@ async def base_exception_handler(
|
||||
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content=exc.to_dict(),
|
||||
content=jsonable_encoder(exc.to_dict()),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -372,6 +372,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
import { avisoConsolidadoReportsApi } from '$lib/api/dashboard/a76/reports/reports-aviso-consolidado';
|
||||
|
||||
async function handleDownloadConsolidated(invoice: any) {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
@@ -379,7 +381,7 @@
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Trigger: Iniciar la tarea en Celery (Consolidado)
|
||||
// 1. Trigger: Iniciar la tarea en Celery (Consolidado Importación)
|
||||
const { task_id } = await consolidatedReportsApi.triggerPdfGeneration(
|
||||
invoice.id,
|
||||
companyStore.activeCompany.id
|
||||
@@ -394,7 +396,30 @@
|
||||
console.error(error);
|
||||
toast.error("No se pudo iniciar la descarga del consolidado");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownloadAvisoConsolidado(invoice: any) {
|
||||
if (!companyStore.activeCompany) {
|
||||
toast.error("No hay empresa seleccionada");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Trigger: Iniciar la tarea en Celery (Aviso Consolidado Exportación)
|
||||
const { task_id } = await avisoConsolidadoReportsApi.triggerPdfGeneration(
|
||||
invoice.id,
|
||||
companyStore.activeCompany.id
|
||||
);
|
||||
|
||||
// 2. Abrir diálogo de progreso
|
||||
currentTaskId = task_id;
|
||||
currentStatusFunction = avisoConsolidadoReportsApi.getTaskStatus;
|
||||
showProgressDialog = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("No se pudo iniciar la descarga del Aviso Consolidado");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDownloadPackingList(invoice: any) {
|
||||
@@ -655,10 +680,17 @@
|
||||
<FileText class="h-4 w-4 mr-2" />
|
||||
Factura
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadConsolidated(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<Boxes class="h-4 w-4 mr-2" />
|
||||
Consolidado
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadAvisoConsolidado(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<Boxes class="h-4 w-4 mr-2" />
|
||||
Aviso Consolidado
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="sm" onclick={() => selectedInvoice && handleDownloadPackingList(selectedInvoice)} disabled={!selectedInvoice}>
|
||||
<Package class="h-4 w-4 mr-2" />
|
||||
Packing List
|
||||
|
||||
Reference in New Issue
Block a user