Se termino el modulo de reportes

This commit is contained in:
2026-02-05 08:22:59 -06:00
parent afba0332b1
commit dbf4b63675
11 changed files with 992 additions and 765 deletions

View File

@@ -8,6 +8,7 @@ from fastapi import APIRouter
from .fa.fa_classes.routes import router as fa_classes_router from .fa.fa_classes.routes import router as fa_classes_router
from .fa.fa_item_lines.routes import router as fa_item_lines_router from .fa.fa_item_lines.routes import router as fa_item_lines_router
# Router principal de A24 # Router principal de A24
router = APIRouter() router = APIRouter()

View File

@@ -38,9 +38,9 @@ class LineQuantity(Base):
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESOBRUTO gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
# Packaging # Packaging
package_key: Mapped[Optional[str]] = mapped_column(String(5)) # CLAVEBULTOS package_id: Mapped[Optional[int]] = mapped_column(Integer) # CLAVEBULTOS (Originally package_key)
package_quantity: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS package_quantity: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
package_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCBULTOS # package_description removed as not in DB
container_quantity: Mapped[Optional[int]] = mapped_column(SmallInteger) # CANTBULCONT container_quantity: Mapped[Optional[int]] = mapped_column(SmallInteger) # CANTBULCONT
container_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCCONTENEDOR container_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCCONTENEDOR
box_count: Mapped[Optional[str]] = mapped_column(String(30)) # NOCAJAS box_count: Mapped[Optional[str]] = mapped_column(String(30)) # NOCAJAS

View File

@@ -52,6 +52,8 @@ class Item(Base, TenantScopedMixin, TimestampMixin):
# Relationships (one-to-many) # Relationships (one-to-many)
lines: Mapped[List["LineItem"]] = relationship( lines: Mapped[List["LineItem"]] = relationship(
"LineItem", back_populates="item", cascade="all, delete-orphan") "LineItem", back_populates="item", cascade="all, delete-orphan")
invoice: Mapped["InvoiceHeader"] = relationship("InvoiceHeader")
# ============================================================================ # ============================================================================
# SUPPORTING TABLES # SUPPORTING TABLES

View File

@@ -6,11 +6,36 @@ from typing import Dict, Any
from core.database import get_core_db as get_db from core.database import get_core_db as get_db
from core.security import get_current_user from core.security import get_current_user
from .task import generate_descarga_pdf_task from .service import FIFOAssignmentService
from .task import generate_descarga_pdf_task # FORCE RELOAD
from celery.result import AsyncResult from celery.result import AsyncResult
router = APIRouter() router = APIRouter()
@router.post("/fifo-assign/{invoice_id}")
def run_fifo_assignment(
invoice_id: int,
db: Session = Depends(get_db),
current_user: Any = Depends(get_current_user),
):
"""
Executes FIFO (PEPS) calculation for an Export Invoice.
Returns the calculated discharges in JSON format.
"""
service = FIFOAssignmentService()
try:
discharges = service.calculate_fifo(db, invoice_id)
return {
"invoice_id": invoice_id,
"total_discharges": len(discharges),
"discharges": discharges
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Error calculating FIFO: {str(e)}"
)
@router.post("/{invoice_id}/download-async") @router.post("/{invoice_id}/download-async")
async def trigger_descarga_generation( async def trigger_descarga_generation(
invoice_id: int, invoice_id: int,

View File

@@ -9,17 +9,159 @@ from fastapi import HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel from pydantic import BaseModel
from decimal import Decimal from decimal import Decimal
from datetime import datetime
# --- MODELOS (Imported from system for Header info) --- # --- MODELOS (Imported from system for Header info) ---
from api.v1.modules.a76.invoices.models import InvoiceHeader from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
from api.v1.modules.a76.general_catalogs.company.models import Company 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_items.models import LineItem
from api.v1.modules.a76.items.line_quantities.models import LineQuantity 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.parts.models import Part
from api.v1.modules.a76.items.models import Item from api.v1.modules.a76.items.models import Item
from api.v1.modules.a76.pedmientos.models import Pedimentos from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from api.v1.modules.a76.pedmientos.models.pedimento_dates import PedimentoDates
from api.v1.modules.core.tenants.models import Tenant
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from sqlalchemy.orm import joinedload, load_only from sqlalchemy.orm import joinedload, load_only
# --- FIFO SERVICE (Internalized) ---
from api.v1.modules.a76.items.series.models import Serie
class FIFOAssignmentService:
"""
Service for calculating FIFO (PEPS) assignments in real-time.
Does not persist to database, returns calculated discharge objects.
"""
def calculate_fifo(self, db: Session, invoice_id: int) -> List[Dict[str, Any]]:
"""
Calculates the FIFO trail for all lines in an export invoice.
Returns a list of calculated discharges.
"""
# 1. Get Export Lines
export_lines = db.query(LineItem).join(Item).filter(
Item.invoice_id == invoice_id
).options(
joinedload(LineItem.quantity),
joinedload(LineItem.description),
joinedload(LineItem.customs),
joinedload(LineItem.unit_of_measure_info),
joinedload(LineItem.part_info)
).all()
results = []
self._log(f"Starting FIFO for Invoice {invoice_id}. Export Lines: {len(export_lines)}")
for exp_line in export_lines:
qty_needed = float(exp_line.quantity.quantity) if exp_line.quantity and exp_line.quantity.quantity is not None else 0.0
if qty_needed <= 0:
continue
part_number = exp_line.part_number
if not part_number:
self._log(f"Skipping line {exp_line.id}, no part number")
continue
self._log(f"Processing Exp Line {exp_line.id}, Part: {part_number}, Qty Needed: {qty_needed}")
# --- SERIES EXPO LOOKUP ---
series_desc = ""
series_count = db.query(Serie).filter(Serie.line_item_id == exp_line.id).count()
if series_count > 0:
series_list = db.query(Serie).filter(Serie.line_item_id == exp_line.id).all()
parts_str = []
for idx, s in enumerate(series_list, 1):
line_parts = [f"{idx}) Serie: {s.serial_numbers or ''}", f"Modelo: {s.model or ''}", f"Parte: {exp_line.part_info.part_number if exp_line.part_info else ''}", f"Num ID Expo: {s.number_id or ''}", f"SubModelo: {s.sub_model or ''}"]
parts_str.append(", ".join([p for p in line_parts if p]))
if parts_str:
series_desc = "\nSeries:\n" + "\n".join(parts_str)
# 2. Find Import Candidates (FIFO order by payment date)
# Use outerjoin for pedimento dates to avoid filtering out candidates with missing dates
candidates = db.query(LineItem).join(Item).join(InvoiceHeader)\
.join(InvoiceComplianceMx).join(InvoiceComplianceMx.pedimento).outerjoin(Pedimentos.pedimento_dates)\
.filter(
LineItem.part_number == part_number,
InvoiceHeader.operation_type == 'imp', # Assuming 'imp' is the value for Import based on Enum
).order_by(
PedimentoDates.payment_date.asc()
).options(
joinedload(LineItem.quantity),
joinedload(LineItem.customs),
joinedload(LineItem.financial),
joinedload(LineItem.item).joinedload(Item.invoice).joinedload(InvoiceHeader.compliance_mx).joinedload(InvoiceComplianceMx.pedimento).joinedload(Pedimentos.pedimento_dates)
).all()
self._log(f"Found {len(candidates)} candidates for {part_number}")
for imp_line in candidates:
if qty_needed <= 0:
break
imp_qty_total = float(imp_line.quantity.quantity) if imp_line.quantity and imp_line.quantity.quantity is not None else 0.0
if imp_qty_total <= 0:
continue
take = min(qty_needed, imp_qty_total)
ratio = take / imp_qty_total if imp_qty_total > 0 else 0
# Calculate Proportions
imp_weight = float(imp_line.quantity.net_weight) if imp_line.quantity and imp_line.quantity.net_weight is not None else 0.0
# imp_val_mn = float(imp_line.customs.customs_value) if imp_line.customs and imp_line.customs.customs_value is not None else 0.0
imp_val_me = float(imp_line.financial.customs_value_usd) if imp_line.financial and imp_line.financial.customs_value_usd is not None else 0.0
imp_igi = float(imp_line.customs.igi_amount) if imp_line.customs and imp_line.customs.igi_amount is not None else 0.0
imp_inv = imp_line.item.invoice
ped = imp_inv.compliance_mx.pedimento if imp_inv and imp_inv.compliance_mx else None
# --- EXCHANGE RATE VALIDATION ---
payment_date = ped.pedimento_dates.payment_date if ped and ped.pedimento_dates else None
exchange_rate_val = 1.0
validation_error = None
if payment_date:
er_obj = db.query(ExchangeRate).filter(ExchangeRate.date == payment_date).first()
if er_obj:
exchange_rate_val = float(er_obj.value)
else:
# Try previous day if strict match fails (mimicking SisGen:UtilizarFechaPagoPedDeUnDiaAnterior logic broadly or just flagging)
# For now, flag it.
validation_error = f"Tipo de Cambio no encontrado para fecha {payment_date}"
# Calculate Valor MN based on Clarion logic: (CantDesc * ValorImpoME / CantImpo) * TC
# which simplifies to: Ratio * ValorImpoME * TC
val_mn_calc = (imp_val_me * ratio) * exchange_rate_val
discharge = {
"export_line_id": exp_line.id,
"import_line_id": imp_line.id,
"quantity": take,
"net_weight": imp_weight * ratio,
"value_mxn": val_mn_calc,
"value_usd": imp_val_me * ratio,
"igi_amount": imp_igi * ratio,
"import_invoice": imp_inv.invoice_number if imp_inv else "N/A",
"pedimento": ped.pedimento_number if ped else "N/A",
"pedimento_clave": ped.pedimento_code if ped else "",
"pedimento_date": payment_date.isoformat() if payment_date else None,
"series_desc": series_desc,
"validation_error": validation_error
}
self._log(f"MATCH: Taking {take} from Imp Line {imp_line.id}")
results.append(discharge)
qty_needed -= take
return results
def _log(self, msg):
try:
with open("/tmp/fifo_debug.log", "a") as f:
f.write(f"{datetime.now()}: {msg}\n")
except: pass
# --- SCHEMAS FOR TEMPLATE CONTEXT --- # --- SCHEMAS FOR TEMPLATE CONTEXT ---
class DischargeItemSchema(BaseModel): class DischargeItemSchema(BaseModel):
@@ -57,8 +199,10 @@ class DischargeItemSchema(BaseModel):
# Let's assume Se Pago is a boolean/string. # Let's assume Se Pago is a boolean/string.
# Last col: "Linea Expo". # Last col: "Linea Expo".
se_pago_val: str
linea_expo: str linea_expo: str
# Errors
error_msg: Optional[str] = None
# Helper for Jinja (if methods not allowed in pydantic models in template) # Helper for Jinja (if methods not allowed in pydantic models in template)
def __init__(self, **data): def __init__(self, **data):
@@ -104,115 +248,80 @@ class DescargaReportService:
try: try:
if progress_callback: progress_callback(10, "Buscando factura...") if progress_callback: progress_callback(10, "Buscando factura...")
# Fetch Header for basic info # --- 1. Obtener Cabeceras (Igual que antes) ---
header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first() header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id).first()
if not header: if not header:
raise HTTPException(status_code=404, detail="Factura no encontrada") raise HTTPException(status_code=404, detail="Factura no encontrada")
company = db.query(Company).filter(Company.id == company_id).first() tenant = db.query(Tenant).filter(Tenant.id == header.tenant_id).first()
company = db.query(Company).filter(Company.id == header.company_id).first()
# --- 2. Obtener Líneas de Exportación (Lo que necesitamos cubrir) ---
if progress_callback: progress_callback(20, "Obteniendo items a exportar...")
if progress_callback: progress_callback(30, "Procesando descargas...")
# --- REAL IMPLEMENTATION ---
# 1. Fetch Export Lines with FA Data
export_lines = db.query(LineItem).filter( export_lines = db.query(LineItem).filter(
LineItem.item_id == Item.id, LineItem.item_id == Item.id,
Item.invoice_id == invoice_id Item.invoice_id == invoice_id
).options( ).options(
joinedload(LineItem.fa_data), joinedload(LineItem.quantity),
joinedload(LineItem.quantity).load_only(LineQuantity.quantity, LineQuantity.net_weight),
joinedload(LineItem.customs), joinedload(LineItem.customs),
joinedload(LineItem.description), joinedload(LineItem.description),
joinedload(LineItem.unit_of_measure_info), joinedload(LineItem.unit_of_measure_info),
joinedload(LineItem.part_info), # Fetch Part Relation joinedload(LineItem.part_info)
# joinedload(LineItem.item).joinedload(Item.invoice) # Removed due to missing relationship
).join(Item).all() ).join(Item).all()
# 2. Collect unique Import Invoices to bulk fetch items_reporte = []
# 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 = [] # --- 3. EL ALGORITMO PEPS EN VIVO ---
if progress_callback: progress_callback(40, "Calculando PEPS en tiempo real...")
fifo_service = FIFOAssignmentService()
discharges = fifo_service.calculate_fifo(db, invoice_id)
print(f"DEBUG: Calculated {len(discharges)} discharges.") # FORCE PRINT
for line in export_lines: # Create map for faster/safer lookup
# Defaults exp_map = {l.id: l for l in export_lines}
ped_str = ""
ped_clave = "" for d in discharges:
ped_fecha = "" exp_id = d["export_line_id"]
fac_impo = "" exp_line = exp_map.get(exp_id)
se_pago = ""
valor_igi = 0.0
# Linkage if not exp_line:
if line.fa_data and line.fa_data.search_invoice: print(f"DEBUG: Skipping discharge, Exp Line {exp_id} not found in map keys: {list(exp_map.keys())}")
fac_impo = line.fa_data.search_invoice continue
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) print(f"DEBUG: Adding item to report: Imp {d['import_line_id']} -> Exp {exp_id}")
qty = float(line.quantity.quantity) if line.quantity else 0.0
valor_me = 0.0 desc_final = exp_line.description.description_spanish if exp_line.description else "S/D"
valor_mn = 0.0 items_reporte.append(DischargeItemSchema(
pedimento_numero=d["pedimento"],
# Create Schema pedimento_clave=d["pedimento_clave"],
items.append(DischargeItemSchema( pedimento_fecha_pago=d["pedimento_date"].split("T")[0] if d["pedimento_date"] else "",
pedimento_numero=ped_str,
pedimento_clave=ped_clave, factura_impo=d["import_invoice"],
pedimento_fecha_pago=ped_fecha,
factura_impo=fac_impo, numero_parte=exp_line.part_info.part_number if exp_line.part_info else "",
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=desc_final,
descripcion=line.description.description_spanish if line.description else "S/D", fraccion=exp_line.customs.fraction if exp_line.customs else "",
fraccion=line.customs.fraction if line.customs else "", origen_pref_sector=f"{exp_line.customs.origin_country or ''} - {exp_line.customs.sector or ''}" if exp_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), cantidad=self.formatear_numero(d["quantity"], 3),
unidad_medida=line.unit_of_measure_info.code if line.unit_of_measure_info else "PZA", unidad_medida=exp_line.unit_of_measure_info.code if exp_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), peso_neto=self.formatear_numero(d["net_weight"], 3),
valor_me=self.formatear_numero(valor_me), valor_mn=self.formatear_numero(d["value_mxn"]),
valor_igi=self.formatear_numero(valor_igi), valor_me=self.formatear_numero(d["value_usd"]),
se_pago=se_pago or "NO", valor_igi=self.formatear_numero(d["igi_amount"]),
se_pago_val=se_pago,
linea_expo=str(line.line_number) se_pago="",
linea_expo=str(exp_line.line_number),
error_msg=d.get("validation_error")
)) ))
# Totals
# Company Address Construction print(f"DEBUG: Final Report Items Count: {len(items_reporte)}")
# --- 4. Totales y Finalización (Igual que antes) ---
addr_str = "DIRECCION NO REGISTRADA" addr_str = "DIRECCION NO REGISTRADA"
immex_val = "" immex_val = ""
@@ -237,17 +346,17 @@ class DescargaReportService:
if company.program and "IMMEX" in company.program and company.program_number: if company.program and "IMMEX" in company.program and company.program_number:
immex_val = company.program_number immex_val = company.program_number
# Calculate Totals # Recalcular totales basados en la lista generada
t_cant = sum(float(i.cantidad.replace(",","")) for i in items if i.cantidad) t_cant = sum(float(i.cantidad.replace(",","")) for i in items_reporte)
t_peso = sum(float(i.peso_neto.replace(",","")) for i in items if i.peso_neto) t_peso = sum(float(i.peso_neto.replace(",","")) for i in items_reporte)
t_mn = sum(float(i.valor_mn.replace(",","")) for i in items if i.valor_mn) t_mn = sum(float(i.valor_mn.replace(",","")) for i in items_reporte)
t_me = sum(float(i.valor_me.replace(",","")) for i in items if i.valor_me) t_me = sum(float(i.valor_me.replace(",","")) for i in items_reporte)
t_igi = sum(float(i.valor_igi.replace(",","")) for i in items if i.valor_igi) t_igi = sum(float(i.valor_igi.replace(",","")) for i in items_reporte)
return DischargeContext( return DischargeContext(
items=items, items=items_reporte,
invoice_number=header.invoice_number or "SIN FOLIO", invoice_number=header.invoice_number or "SIN FOLIO",
company_name=company.name if company else "EMPRESA DESCONOCIDA", company_name=company.name if company else (tenant.name if tenant else "EMPRESA DESCONOCIDA"),
company_address=addr_str, company_address=addr_str,
company_rfc=company.rfc if company else "", company_rfc=company.rfc if company else "",
company_immex=immex_val, company_immex=immex_val,

View File

@@ -2,7 +2,7 @@
from celery import shared_task from celery import shared_task
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from core.database import CoreSessionLocal as SessionLocal from core.database import CoreSessionLocal as SessionLocal
from .service import DescargaReportService from .service import DescargaReportService # FORCE RELOAD 2
import base64 import base64
import traceback import traceback

View File

@@ -103,10 +103,10 @@
<table class="main-table"> <table class="main-table">
<thead> <thead>
<tr> <tr>
<th style="width: 12%;">No. Pedimento<br>Clave Fecha de Pago</th> <th style="width: 12%;">No. Pedimento<br>Fecha de Pago</th>
<th style="width: 10%;">Import Invoice/<br>Factura de Impo.</th> <th style="width: 10%;">Import Invoice/<br>Factura de Impo.</th>
<th style="width: 25%;">Part Number/No. de Parte Componente<br>Description/Descripción <th style="width: 15%; border-right: none;">Part Number/No. de Parte<br>Description/Descripción</th>
(Origen-Prefer.-Sector)</th> <th style="width: 10%; border-left: none;">Fracción<br>(Origen-Prefer.-Sector)</th>
<th style="width: 8%;">Quantity/<br>Cantidad U.M.</th> <th style="width: 8%;">Quantity/<br>Cantidad U.M.</th>
<th style="width: 8%;">Net Weight/<br>Peso Neto (KGS)</th> <th style="width: 8%;">Net Weight/<br>Peso Neto (KGS)</th>
<th style="width: 8%;">Value/Valor M.N.<br>MXP/Pesos</th> <th style="width: 8%;">Value/Valor M.N.<br>MXP/Pesos</th>
@@ -117,23 +117,28 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<!-- Sub Header -->
<tr>
<td colspan="10" class="sub-header">Comp. Temporales:</td>
</tr>
<!-- Loops Items --> <!-- Loops Items -->
{% for item in items %} {% for item in items %}
<tr class="{{ 'row-border-bottom' if loop.last else '' }}"> <tr class="{{ 'row-border-bottom' if loop.last else '' }}">
<td> <td>
<b>{{ item.pedimento_numero }}</b><br> <div style="font-weight: bold;">{{ item.pedimento_numero }}</div>
{{ item.pedimento_clave }} &nbsp;&nbsp; {{ item.pedimento_fecha_pago }} <table style="width: 100%; border: none; margin-top: 2px; border-collapse: collapse;">
<tr>
<td style="border: none; padding: 0; font-size: 6.5pt; text-align: left; width: 30%;">{{
item.pedimento_clave }}</td>
<td style="border: none; padding: 0; font-size: 6.5pt; text-align: right; width: 70%;">{{
item.pedimento_fecha_pago }}</td>
</tr>
</table>
</td> </td>
<td>{{ item.factura_impo }}</td> <td>{{ item.factura_impo }}</td>
<td> <td style="border-right: none;">
<b>{{ item.numero_parte }}</b><br> <b>{{ item.numero_parte }}</b><br>
{{ item.descripcion }}<br> {{ item.descripcion }}
<span style="font-size: 6pt;">{{ item.fraccion }}<br>{{ item.origen_pref_sector }}</span> </td>
<td style="font-size: 6pt; border-left: none;">
{{ item.fraccion }}<br>
{{ item.origen_pref_sector }}
</td> </td>
<td class="text-right"> <td class="text-right">
{{ item.cantidad }} {{ item.unidad_medida }} {{ item.cantidad }} {{ item.unidad_medida }}
@@ -147,28 +152,28 @@
</tr> </tr>
{% endfor %} {% endfor %}
<!-- Totals --> <!-- Sub Totals -->
<tr class="totals-row"> <tr class="totals-row">
<td colspan="3" class="text-center">Totales de los Comp. Temporales:</td> <td colspan="4" class="text-center">Totales de los Comp. Temporales:</td>
<td>{{ total_cantidad }}</td> <td class="text-right">{{ total_cantidad }}</td>
<td>{{ total_peso }}</td> <td class="text-right">{{ total_peso }}</td>
<td>{{ total_valor_mn }}</td> <td class="text-right">{{ total_valor_mn }}</td>
<td>{{ total_valor_me }}</td> <td class="text-right">{{ total_valor_me }}</td>
<td>{{ total_igi }}</td> <td class="text-right">{{ total_igi }}</td>
<td></td> <td class="text-center"></td>
<td></td> <td class="text-center"></td>
</tr> </tr>
<!-- Grand Totals (Same as above for this example) --> <!-- Grand Totals -->
<tr class="totals-row" style="border-top: 2px solid black;"> <tr class="totals-row" style="border-top: 2px solid black;">
<td colspan="3" class="text-center" style="font-size: 9pt;">TOTALES:</td> <td colspan="4" class="text-center" style="font-size: 9pt;">TOTALES:</td>
<td>{{ total_cantidad }}</td> <td class="text-right">{{ total_cantidad }}</td>
<td>{{ total_peso }}</td> <td class="text-right">{{ total_peso }}</td>
<td>{{ total_valor_mn }}</td> <td class="text-right">{{ total_valor_mn }}</td>
<td>{{ total_valor_me }}</td> <td class="text-right">{{ total_valor_me }}</td>
<td>{{ total_igi }}</td> <td class="text-right">{{ total_igi }}</td>
<td></td> <td class="text-center"></td>
<td></td> <td class="text-center"></td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -1,6 +1,6 @@
""" """
Router principal de API v1 Router principal de API v1
Agrega todos los módulos de la aplicación Agrega todos los módulos de la aplicación (Reload)
""" """
from fastapi import APIRouter from fastapi import APIRouter
@@ -10,7 +10,6 @@ from .modules.core.router import router as core_router
from .modules.a76.router import router as a76_router from .modules.a76.router import router as a76_router
from .modules.a24.router import router as a24_router from .modules.a24.router import router as a24_router
from .modules.public.router import router as public_router from .modules.public.router import router as public_router
from .modules.a24.router import router as a24_router
# Router principal # Router principal
@@ -21,8 +20,6 @@ router.include_router(core_router)
router.include_router(a76_router) router.include_router(a76_router)
router.include_router(a24_router) router.include_router(a24_router)
router.include_router(public_router) router.include_router(public_router)
# nuevas rutas de partes de anexo 24
router.include_router(a24_router)
# Health check # Health check

View File

@@ -0,0 +1,35 @@
import axios from 'axios';
import { PUBLIC_API_URL } from '$env/static/public';
/**
* Cliente API para el módulo de Inventarios (A24)
*/
export const invApi = {
/**
* Ejecuta el proceso de asignación PEPS (FIFO) para una factura de exportación
* @param invoiceId ID de la factura de exportación
* @returns Promesa con la respuesta del servidor
*/
assignFifo: async (invoiceId: number) => {
try {
const token = localStorage.getItem('access_token');
const response = await axios.post(
`${PUBLIC_API_URL}/api/v1/a76/reports/exportacion/descargo/fifo-assign/${invoiceId}`,
{},
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
return { data: response.data, error: null };
} catch (error: any) {
console.error('Error executing FIFO:', error);
return {
data: null,
error: error.response?.data?.detail || 'Error al ejecutar cálculo PEPS'
};
}
}
};

View File

@@ -32,5 +32,30 @@ export const dischargeReportsApi = {
if (!response.ok) throw new Error('Error al consultar estado del Reporte de Descarga'); if (!response.ok) throw new Error('Error al consultar estado del Reporte de Descarga');
return await response.json(); return await response.json();
},
assignFifo: async (invoiceId: number) => {
// Endpoint: /a76/reports/exportacion/descargo/fifo-assign/{invoice_id}
const endpoint = `${BASE_URL}/v1/a76/reports/exportacion/descargo/fifo-assign/${invoiceId}`;
const token = localStorage.getItem('access_token');
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
return { data: null, error: errData.detail || 'Error al ejecutar PEPS' };
}
return { data: await response.json(), error: null };
} catch (e: any) {
return { data: null, error: e.message || 'Error de conexión PEPS' };
}
} }
}; };

File diff suppressed because it is too large Load Diff