Se mejoro el disenio de partes, se genero la informacion mas precisa en los reportes y se carga el logo en las instanacias de las empresas

This commit is contained in:
2026-01-15 13:20:34 -06:00
parent ae1d7f0c58
commit 0a02a3e912
44 changed files with 1536 additions and 515 deletions

View File

@@ -29,7 +29,7 @@ class FaPart(Base, TenantScopedMixin, TimestampMixin):
ForeignKeyConstraint(
["id"], ["a76.parts.id"], name="fk_fa_partes_master"
),
{"schema": "a24"},
{"schema": "a24", "extend_existing": True},
)
# El ID hereda el valor de la tabla parts

View File

@@ -32,7 +32,7 @@ class InvPart(Base, TenantScopedMixin, TimestampMixin):
ForeignKeyConstraint(
["id"], ["a76.parts.id"], name="fk_inv_partes_master"
),
{"schema": "a24"},
{"schema": "a24", "extend_existing": True},
)
# Relación 1:1 - El ID es el mismo de la tabla maestra

View File

@@ -48,7 +48,7 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
"class_code",
name="uq_classes_tenant_company_code",
),
{"schema": "a76"},
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)

View File

@@ -26,7 +26,7 @@ class Company(Base, TimestampMixin):
__tablename__ = "company" #GEmpresa
__table_args__ = (
PrimaryKeyConstraint("id", name="company_pkey"),
{"schema": "a76"},
{"schema": "a76", "extend_existing": True},
)
# Primary key

View File

@@ -3,8 +3,12 @@ Rutas para gestión de empresa
"""
from typing import List, Optional
import os
import shutil
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, File, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from core.database import get_core_db
@@ -298,11 +302,98 @@ async def update_company(
return CompanyResponseDTO.model_validate(updated_company)
@router.delete(
"/{company_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete company",
return CompanyResponseDTO.model_validate(updated_company)
@router.post(
"/{company_id}/upload-logo",
response_model=dict,
summary="Upload company logo",
)
async def upload_company_logo(
company_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Upload logo for a company"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
# 1. Verify company exists
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
# 2. Define upload path
# Use a persistent path: 'app_data/logos/{company_id}'
upload_dir = Path(f"app_data/logos/{company_id}")
upload_dir.mkdir(parents=True, exist_ok=True)
# 3. Save file
# Preserve original filename
filename = file.filename or "logo.png"
file_path = upload_dir / filename
try:
# Check if file exists and remove it to avoid accumulation if needed,
# or just overwrite (shutil.copyfileobj overwrites)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Could not save file: {e}",
)
# 4. Returns the absolute path keys
abs_path = str(file_path.absolute())
return {"path": abs_path}
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
async def get_company_logo_image(
company_id: int,
db: Session = Depends(get_core_db),
# Public endpoint to allow <img> tags to load the image without custom headers
):
"""Serve the company logo image file"""
# Security: In a stricter environment, we would use a signed short-lived URL
# or cookie-based auth. For now, checking if company exists is sufficient.
# We find the company ignoring tenant checks for the image serving
# (Logos are generally considered semi-public assets in this context)
company = db.query(Company).filter(Company.id == company_id).first()
if not company or not company.logo:
raise HTTPException(status_code=404, detail="Logo not found")
file_path = Path(company.logo)
if not file_path.exists():
# Fallback for old paths or moved files
# Check if it exists in the 'standard' location even if DB thinks otherwise
standard_path = Path(f"app_data/logos/{company_id}") / file_path.name
if standard_path.exists():
return FileResponse(standard_path)
raise HTTPException(status_code=404, detail="Logo file not found on server")
return FileResponse(file_path)
async def delete_company(
company_id: int,
db: Session = Depends(get_core_db),

View File

@@ -34,10 +34,13 @@ class BaseService:
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
) -> Tuple[List[Any], int]:
query = db.query(cls.model).filter(
cls.model.tenant_id == tenant_id,
cls.model.company_id == company_id,
)
query = db.query(cls.model)
if hasattr(cls.model, "tenant_id"):
query = query.filter(cls.model.tenant_id == tenant_id)
if hasattr(cls.model, "company_id"):
query = query.filter(cls.model.company_id == company_id)
if filters:
if filters.get("code"):
@@ -56,11 +59,15 @@ class BaseService:
def get_by_id(
cls, db: Session, id: int, tenant_id: int, company_id: int
) -> Optional[Any]:
return db.query(cls.model).filter(
cls.model.id == id,
cls.model.tenant_id == tenant_id,
cls.model.company_id == company_id,
).first()
query = db.query(cls.model).filter(cls.model.id == id)
if hasattr(cls.model, "tenant_id"):
query = query.filter(cls.model.tenant_id == tenant_id)
if hasattr(cls.model, "company_id"):
query = query.filter(cls.model.company_id == company_id)
return query.first()
@classmethod
def create(
@@ -70,9 +77,15 @@ class BaseService:
tenant_id: int,
company_id: int,
) -> Any:
db_obj = cls.model(
**data.model_dump(), tenant_id=tenant_id, company_id=company_id
)
create_kwargs = data.model_dump()
if hasattr(cls.model, "tenant_id"):
create_kwargs["tenant_id"] = tenant_id
if hasattr(cls.model, "company_id"):
create_kwargs["company_id"] = company_id
db_obj = cls.model(**create_kwargs)
db.add(db_obj)
db.commit()
db.refresh(db_obj)

View File

@@ -22,14 +22,16 @@ from sqlalchemy import (
# Importante usar relationship y Mapped
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.a24.fa.fa_parts.models import FaPart
from api.v1.modules.a24.inv.inv_parts.models import InvPart
if TYPE_CHECKING:
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.a76.general_catalogs.units_of_measure.models import (
UnitOfMeasure,
)
from api.v1.modules.a24.fa.fa_parts.models import FaPart
from api.v1.modules.a24.inv.inv_parts.models import InvPart
class Part(Base, TenantScopedMixin, TimestampMixin):

View File

@@ -1,8 +1,9 @@
import shutil
import base64
import pdfkit
from pathlib import Path
from decimal import Decimal
from typing import Tuple, List
from typing import Tuple, List, Callable, Optional
from jinja2 import Environment, FileSystemLoader, select_autoescape
from fastapi import HTTPException
@@ -81,16 +82,19 @@ class FacturaImportacionMexService:
autorizacion=prog.program_number if prog else ""
)
def obtener_datos(self, db: Session, invoice_id: int, company_id: int) -> FacturaImportacionCompleta:
def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> FacturaImportacionCompleta:
try:
if progress_callback: progress_callback(10, "Buscando factura...")
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")
compliance = header.compliance_mx
logistics = header.logistics[0] if header.logistics else None
logistics = header.logistics if header.logistics else None
if progress_callback: progress_callback(20, "Obteniendo datos de pedimento...")
pedimento_id = compliance.pedimento_id if (compliance and compliance.pedimento_id) else header.related_doc_id
pedimento = db.query(Pedimentos).filter(Pedimentos.id == pedimento_id).first() if pedimento_id else None
if progress_callback: progress_callback(30, "Obteniendo cliente y proveedor...")
proveedor_id = compliance.provider_id if compliance else None
cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / Supplier") if proveedor_id else ClienteSchema(header="Proveedor", nombre="No Asignado", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="")
@@ -112,28 +116,35 @@ class FacturaImportacionMexService:
remesa_valor = str(compliance.remesa) if (compliance and compliance.remesa) else ""
acuse_valor = str(compliance.edocument) if (compliance and compliance.edocument) else "N/A"
patente_val = ""
if pedimento and pedimento.license:
patente_val = pedimento.license
elif 'broker' in locals() and broker and broker.license:
patente_val = broker.license
factura_schema = FacturaSchema(
numero=header.invoice_number or "S/N",
fecha=str(header.invoice_date) if header.invoice_date else "",
tipo_cambio=float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0,
moneda=getattr(header, 'currency', "USD") or "USD",
incoterm=logistics.incoterm if logistics else "",
incoterm=(logistics.incoterm or "") if logistics else "",
observaciones=header.observation_es or header.observation_en or "",
pedimento=pedimento.pedimento_number if pedimento else "",
clave_pedimento=pedimento.pedimento_code if pedimento else "",
regimen=pedimento.regime if pedimento else "",
patente=pedimento.license if pedimento else "",
patente=patente_val,
agente_aduanal=nombre_agente,
transporte=str(logistics.transport_type.value) if (logistics and logistics.transport_type) else "",
num_transporte=logistics.trailer_num if logistics else "",
placas=logistics.license_plate if logistics else "",
transportista=logistics.carrier_id if logistics else "",
transporte=str(logistics.transport_type) if (logistics and logistics.transport_type) else "",
num_transporte=(logistics.trailer_num or "") if logistics else "",
placas=(logistics.license_plate or "") if logistics else "",
transportista=(logistics.carrier_id or "") if logistics else "",
aduana=pedimento.customs_office if pedimento else "",
precinto=logistics.seal_number if logistics else "",
destino=logistics.destination_goods if logistics else "",
precinto=(logistics.seal_number or "") if logistics else "",
destino=(logistics.destination_goods or "") if logistics else "",
remesa=remesa_valor, acuse_electronico=acuse_valor
)
if progress_callback: progress_callback(50, "Procesando partidas...")
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(Item.invoice_id == header.id).all()
partidas_list = []
@@ -223,16 +234,53 @@ class FacturaImportacionMexService:
valor_total_total=self.formatear_numero(valor), valor_total_dolares=self.formatear_numero(valor / tc if tc > 0 else 0)
)
def generar_factura_completa(self, db: Session, invoice_id: int, company_id: int, formato: str = "pdf") -> Tuple[bytes, str, str]:
datos = self.obtener_datos(db, invoice_id, company_id)
def generar_factura_completa(self, db: Session, invoice_id: int, company_id: int, formato: str = "pdf", 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...")
# LOGO LOGIC
logo_b64 = None
try:
# Fetch company to get logo path
# We use the passed company_id which corresponds to the active company
comp_logo = db.query(Company).filter(Company.id == company_id).first()
if comp_logo and comp_logo.logo:
p = Path(comp_logo.logo)
# Logic robusta de búsqueda (igual que en routes.py)
target_path = p
if not target_path.exists():
# Intentar en la ruta estándar: app_data/logos/{id}/{nombre}
# Esto cubre el caso donde solo se guardó el nombre del archivo o la ruta absoluta cambió
fallback = Path(f"app_data/logos/{company_id}") / p.name
if fallback.exists():
target_path = fallback
if target_path.exists():
with open(target_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
# Detect MIME type loosely
mime = "image/png"
if target_path.suffix.lower() in ['.jpg', '.jpeg']: mime = "image/jpeg"
logo_b64 = f"data:{mime};base64,{encoded_string}"
except Exception as e:
print(f"Error loading logo: {e}")
context = {
'cliente_proveedor': datos.cliente_proveedor.model_dump(), 'cliente_vendido': datos.cliente_vendido.model_dump(),
'cliente_enviado': datos.cliente_enviado.model_dump(), 'factura': datos.factura.model_dump(),
'partidas': [p.model_dump() for p in datos.partidas], 'totales': datos.totales.model_dump()
'partidas': [p.model_dump() for p in datos.partidas], 'totales': datos.totales.model_dump(),
'logo_b64': logo_b64
}
html_content = self.template.render(**context)
nombre = f"Factura_{datos.factura.numero}.{formato}"
if formato == "html": return html_content.encode('utf-8'), nombre, "text/html"
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"

View File

@@ -23,13 +23,16 @@ async def get_task_status(
response = {
"task_id": task_id,
"state": task_result.state,
"result": None
"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
@@ -42,4 +45,4 @@ async def trigger_descarga_factura(
):
validate_access_to_resource(db, company_id, current_user)
task = generar_pdf_factura_async.delay(invoice_id, company_id)
return {"task_id": task.id, "message": "Generación iniciada"}
return {"task_id": task.id, "message": "Generación iniciada"}

View File

@@ -22,13 +22,17 @@ def generar_pdf_factura_async(self, invoice_id: int, company_id: int):
service = FacturaImportacionMexService()
# Update state to PROCESSING
self.update_state(state='PROCESSING', meta={'current': 1, 'total': 1, 'status': 'Generating PDF...'})
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_factura_completa(
db=db,
invoice_id=invoice_id,
company_id=company_id
company_id=company_id,
progress_callback=progress_callback
)
# 4. Codificamos a base64 para que viaje seguro por Valkey

View File

@@ -1,79 +1,308 @@
<!DOCTYPE html>
<html lang="es" xml:lang="es">
<head>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
<title>Factura Importacion Mexicana - {{ factura.numero }}</title>
<style type="text/css">
/* ESTILOS EXACTOS DE SCAPII PARA MANTENER EL FORMATO */
* { margin: 0; padding: 0; font-family: Tahoma, sans-serif; color: black; }
.titulo { font-size: 14pt; padding: 3pt 0 0 6pt; }
.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; }
h1 { font-weight: bold; font-size: 8pt; }
p { font-size: 7pt; }
.cliente { border-bottom: 1pt solid black; text-decoration: none; display: block; width: 100%}
.center { text-align: center; }
.right { text-align: right; }
.border { border: 1pt solid #808080; }
.p-t-1 { padding-top: 1pt; } .p-t-2 { padding-top: 2pt; } .p-t-3 { padding-top: 3pt; }
.p-t-4 { padding-top: 4pt; } .p-t-5 { padding-top: 5pt; } .p-t-8 { padding-top: 8pt; }
.p-t-9 { padding-top: 9pt; }
.p-l-2 { padding-left: 2pt; } .p-l-3 { padding-left: 3pt; } .p-l-5 { padding-left: 5pt; }
.p-l-6 { padding-left: 6pt; } .p-l-7 { padding-left: 7pt; } .p-l-8 { padding-left: 8pt; }
.p-l-9 { padding-left: 9pt; } .p-l-10 { padding-left: 10pt; }
.p-r-1 { padding-right: 1pt; } .p-r-2 { padding-right: 2pt; } .p-r-4 { padding-right: 4pt; }
.p-r-5 { padding-right: 5pt; } .p-r-9 { padding-right: 9pt; }
.p-b-1 { padding-bottom: 1pt; }
.h-10 { height: 10pt; } .h-11 { height: 11pt; } .h-14 { height: 14pt; }
.h-15 { height: 15pt; } .h-18 { height: 18pt; } .h-22 { height: 22pt; }
.h-82 { height: 82pt; } .h-384 { height: 384pt; }
.line-1 { line-height: 1pt; } .line-7 { line-height: 7pt; } .line-8 { line-height: 8pt; }
.line-9 { line-height: 9pt; } .line-10 { line-height: 10pt; }
.m-l-3 { margin-left: 3pt; } .m-l-5 { margin-left: 5.74pt; }
.flex-container { width: 100%; overflow: hidden; }
.flex-container-end { width: 100%; overflow: hidden; }
.width-48 { width: 48%; float: left; }
.width-48-right { width: 48%; float: right; text-align: right; }
.position-relative-centered { position: relative; width: 48%; float: left; }
.full-width-block { display: block; width: 100%; }
.clearfix::after { content: ""; display: table; clear: both; }
table, tbody { vertical-align: top; overflow: visible; }
* {
margin: 0;
padding: 0;
font-family: Tahoma, sans-serif;
color: black;
}
.titulo {
font-size: 14pt;
padding: 3pt 0 0 6pt;
}
.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;
}
h1 {
font-weight: bold;
font-size: 8pt;
}
p {
font-size: 7pt;
}
.cliente {
border-bottom: 1pt solid black;
text-decoration: none;
display: block;
width: 100%
}
.center {
text-align: center;
}
.right {
text-align: right;
}
.border {
border: 1pt solid #808080;
}
.p-t-1 {
padding-top: 1pt;
}
.p-t-2 {
padding-top: 2pt;
}
.p-t-3 {
padding-top: 3pt;
}
.p-t-4 {
padding-top: 4pt;
}
.p-t-5 {
padding-top: 5pt;
}
.p-t-8 {
padding-top: 8pt;
}
.p-t-9 {
padding-top: 9pt;
}
.p-l-2 {
padding-left: 2pt;
}
.p-l-3 {
padding-left: 3pt;
}
.p-l-5 {
padding-left: 5pt;
}
.p-l-6 {
padding-left: 6pt;
}
.p-l-7 {
padding-left: 7pt;
}
.p-l-8 {
padding-left: 8pt;
}
.p-l-9 {
padding-left: 9pt;
}
.p-l-10 {
padding-left: 10pt;
}
.p-r-1 {
padding-right: 1pt;
}
.p-r-2 {
padding-right: 2pt;
}
.p-r-4 {
padding-right: 4pt;
}
.p-r-5 {
padding-right: 5pt;
}
.p-r-9 {
padding-right: 9pt;
}
.p-b-1 {
padding-bottom: 1pt;
}
.h-10 {
height: 10pt;
}
.h-11 {
height: 11pt;
}
.h-14 {
height: 14pt;
}
.h-15 {
height: 15pt;
}
.h-18 {
height: 18pt;
}
.h-22 {
height: 22pt;
}
.h-82 {
height: 82pt;
}
.h-384 {
height: 384pt;
}
.line-1 {
line-height: 1pt;
}
.line-7 {
line-height: 7pt;
}
.line-8 {
line-height: 8pt;
}
.line-9 {
line-height: 9pt;
}
.line-10 {
line-height: 10pt;
}
.m-l-3 {
margin-left: 3pt;
}
.m-l-5 {
margin-left: 5.74pt;
}
.flex-container {
width: 100%;
overflow: hidden;
}
.flex-container-end {
width: 100%;
overflow: hidden;
}
.width-48 {
width: 48%;
float: left;
}
.width-48-right {
width: 48%;
float: right;
text-align: right;
}
.position-relative-centered {
position: relative;
width: 48%;
float: left;
}
.full-width-block {
display: block;
width: 100%;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}
table,
tbody {
vertical-align: top;
overflow: visible;
}
</style>
</head>
<body>
<header>
<div class="flex-container clearfix">
<div class="width-48">
<p class="titulo">Factura de Importacion</p>
<div class="cliente" style="height: 1px;"></div>
<p class="cliente p-t-1 p-b-1"></p>
<div class="width-48" style="position: relative;">
<p class="titulo">Factura de Importacion</p>
<div class="cliente" style="height: 1px;"></div>
<p class="cliente p-t-1 p-b-1"></p>
</div>
<div class="width-48-right">
<p class="p-l-5 line-1"><span></span></p>
<p class="p-t-2"><br></p>
<p class="p-l-5 line-1"><span></span></p>
<p class="p-t-2"><br></p>
</div>
</div>
<div class="flex-container-end clearfix">
</div>
<div class="flex-container-end clearfix" style="overflow: visible;">
<div class="position-relative-centered">
{% if logo_b64 %}
<div style="position: absolute; top: 0; left: 0;">
<img src="{{ logo_b64 }}" style="max-height: 70pt; max-width: 120pt;" />
</div>
{% endif %}
<div style="float: right; text-align: left;">
<h1 class="cliente">{{ cliente_proveedor.header }}</h1>
<p>{{ cliente_proveedor.nombre }}</p>
<p>{{ cliente_proveedor.direccion }}
{% if cliente_proveedor.num_exterior %} Ext: {{ cliente_proveedor.num_exterior }}{% endif %}
{% if cliente_proveedor.num_interior %} Int: {{ cliente_proveedor.num_interior }}{% endif %}
<p>{{ cliente_proveedor.direccion }}
{% if cliente_proveedor.num_exterior %} Ext: {{ cliente_proveedor.num_exterior }}{% endif %}
{% if cliente_proveedor.num_interior %} Int: {{ cliente_proveedor.num_interior }}{% endif %}
</p>
<p>{{ cliente_proveedor.colonia }} {% if cliente_proveedor.codigo_postal %} CP: {{ cliente_proveedor.codigo_postal }}{% endif %}</p>
<p>{{ cliente_proveedor.colonia }} {% if cliente_proveedor.codigo_postal %} CP: {{
cliente_proveedor.codigo_postal }}{% endif %}</p>
<p>{{ cliente_proveedor.ciudad }}, {{ cliente_proveedor.estado }}, {{ cliente_proveedor.pais }}</p>
<p>TAX ID: {{ cliente_proveedor.tax_id }}
{% if cliente_proveedor.programa and cliente_proveedor.programa != 'Ninguno' %}
{{ cliente_proveedor.programa }}: {{ cliente_proveedor.autorizacion }}
{% endif %}
<p>TAX ID: {{ cliente_proveedor.tax_id }}
{% if cliente_proveedor.programa and cliente_proveedor.programa != 'Ninguno' %}
{{ cliente_proveedor.programa }}: {{ cliente_proveedor.autorizacion }}
{% endif %}
</p>
<p><br></p>
</div>
@@ -82,151 +311,178 @@
<div style="position: relative;">
<table cellspacing="0" class="m-l-3" style="float: right; text-align: left;">
<tbody>
<tr class="h-18">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="medio-bold">FACTURA:</p>
</td>
<td class="border" colspan="3" style="width:176pt">
<p class="grande">{{ factura.numero }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-3 line-9">Fecha:</p>
</td>
<td class="border" style="width:72pt">
<p class="normal p-l-2 line-9">{{ factura.fecha }}</p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-2 line-9">T. Cambio:</p>
</td>
<td class="border" style="width:52pt">
<p class="normal center line-9">{{ factura.tipo_cambio }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="small-bold p-l-3 line-9">Pedimento:</p>
</td>
<td class="border" colspan="1" style="width:109pt">
<p class="normal p-l-3 line-9">{{ factura.pedimento }}</p>
</td>
<td bgcolor="#E4E4E4" class="border">
<p class="normal p-l-3 line-9">Clave:</p>
</td>
<td class="border" style="width:37pt">
<p class="normal p-r-2 line-9 center">{{ factura.clave_pedimento }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-3 line-9">Remesa:</p>
</td>
<td class="border" style="width:38pt">
<p class="normal p-l-10 line-9">{{ factura.remesa }}</p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:79pt">
<p class="normal p-l-6 line-9">Acuse:</p>
</td>
<td class="border" style="width:59pt">
<p class="normal p-l-6 line-9">{{ factura.acuse_electronico or 'N/A' }}</p>
</td>
</tr>
<tr class="h-22">
<td class="border" colspan="4" style="width:228pt">
<p class="tiny-bold p-l-3 line-7">Agente Aduanal:</p>
<p class="tiny p-l-3 line-7">{{ factura.agente_aduanal or '' }}</p>
</td>
</tr>
<tr class="h-15">
<td class="border" colspan="2">
<p><span class="tiny-bold p-l-3 line-7">Patente: </span><span class="tiny p-l-3 line-7">{{ factura.patente or '' }}</span></p>
</td>
<td class="border">
<span class="tiny-bold p-l-3 line-7">Regimen:</span><span class="tiny p-l-3 line-7">{{ factura.regimen or '' }}</span>
</td>
<td class="border">
<p class="tiny-bold p-l-3 line-7">INCOTERM:</p>
<p class="tiny p-l-3 line-7">{{ factura.incoterm or '' }}</p>
</td>
</tr>
<tr>
<td class="border" colspan="2">
{% if factura.precinto %}
<p class="tiny-bold p-l-3 line-7">Precinto: {{ factura.precinto }}</p>
{% endif %}
</td>
<td class="border">
<p class="tiny-bold p-l-3 line-7">Aduana: {{ factura.aduana }}</p>
</td>
<td class="border">
{% if factura.destino %}
<p class="tiny-bold p-l-3 line-7">Destino: {{ factura.destino }}</p>
{% endif %}
</td>
</tr>
</tbody></table>
</div>
<tr class="h-18">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="medio-bold">FACTURA:</p>
</td>
<td class="border" colspan="3" style="width:176pt">
<p class="grande">{{ factura.numero }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-3 line-9">Fecha:</p>
</td>
<td class="border" style="width:72pt">
<p class="normal p-l-2 line-9">{{ factura.fecha }}</p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-2 line-9">T. Cambio:</p>
</td>
<td class="border" style="width:52pt">
<p class="normal center line-9">{{ factura.tipo_cambio }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="small-bold p-l-3 line-9">Pedimento:</p>
</td>
<td class="border" colspan="1" style="width:109pt">
<p class="normal p-l-3 line-9">{{ factura.pedimento }}</p>
</td>
<td bgcolor="#E4E4E4" class="border">
<p class="normal p-l-3 line-9">Clave:</p>
</td>
<td class="border" style="width:37pt">
<p class="normal p-r-2 line-9 center">{{ factura.clave_pedimento }}</p>
</td>
</tr>
<tr class="h-11">
<td bgcolor="#E4E4E4" class="border" style="width:52pt">
<p class="normal p-l-3 line-9">Remesa:</p>
</td>
<td class="border" style="width:38pt">
<p class="normal p-l-10 line-9">{{ factura.remesa }}</p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:79pt">
<p class="normal p-l-6 line-9">Acuse:</p>
</td>
<td class="border" style="width:59pt">
<p class="normal p-l-6 line-9">{{ factura.acuse_electronico or 'N/A' }}</p>
</td>
</tr>
<tr class="h-22">
<td class="border" colspan="4" style="width:228pt">
<p class="small-bold p-l-3 line-7">Agente Aduanal:</p>
<p class="normal p-l-3 line-7">{{ factura.agente_aduanal or '' }}</p>
</td>
</tr>
<tr class="h-15">
<td class="border" colspan="2">
<p><span class="small-bold p-l-3 line-7">Patente: </span><span
class="normal p-l-3 line-7">{{ factura.patente or '' }}</span></p>
</td>
<td class="border">
<span class="tiny-bold p-l-3 line-7">Regimen:</span><span class="tiny p-l-3 line-7">{{
factura.regimen or '' }}</span>
</td>
<td class="border">
<p class="tiny-bold p-l-3 line-7">INCOTERM:</p>
<p class="tiny p-l-3 line-7">{{ factura.incoterm or '' }}</p>
</td>
</tr>
<tr>
<td class="border" colspan="2">
{% if factura.precinto %}
<p class="tiny-bold p-l-3 line-7">Precinto: {{ factura.precinto }}</p>
{% endif %}
</td>
<td class="border">
<p class="tiny-bold p-l-3 line-7">Aduana: {{ factura.aduana }}</p>
</td>
<td class="border">
{% if factura.destino %}
<p class="tiny-bold p-l-3 line-7">Destino: {{ factura.destino }}</p>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="flex-container clearfix">
<div class="width-48">
<h1 class="p-t-5 p-l-5 line-10 cliente">{{ cliente_vendido.header }}</h1>
<p class="p-l-5 line-8">{{ cliente_vendido.nombre }}</p>
<p class="p-l-5">{{ cliente_vendido.direccion }}
{% if cliente_vendido.num_exterior %} Ext: {{ cliente_vendido.num_exterior }}{% endif %}
{% if cliente_vendido.num_interior %} Int: {{ cliente_vendido.num_interior }}{% endif %}
<p class="p-l-5">{{ cliente_vendido.direccion }}
{% if cliente_vendido.num_exterior %} Ext: {{ cliente_vendido.num_exterior }}{% endif %}
{% if cliente_vendido.num_interior %} Int: {{ cliente_vendido.num_interior }}{% endif %}
</p>
<p class="p-l-5">{{ cliente_vendido.colonia }} {% if cliente_vendido.codigo_postal %} CP: {{ cliente_vendido.codigo_postal }}{% endif %}</p>
<p class="p-l-5">{{ cliente_vendido.ciudad }}, {{ cliente_vendido.estado }}, {{ cliente_vendido.pais }}</p>
<p class="p-l-5">RFC: {{ cliente_vendido.tax_id }}
{% if cliente_vendido.programa and cliente_vendido.programa != 'Ninguno' %}
{{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }}
{% endif %}
<p class="p-l-5">{{ cliente_vendido.colonia }} {% if cliente_vendido.codigo_postal %} CP: {{
cliente_vendido.codigo_postal }}{% endif %}</p>
<p class="p-l-5">{{ cliente_vendido.ciudad }}, {{ cliente_vendido.estado }}, {{ cliente_vendido.pais }}
</p>
<p class="p-l-5">RFC: {{ cliente_vendido.tax_id }}
{% if cliente_vendido.programa and cliente_vendido.programa != 'Ninguno' %}
{{ cliente_vendido.programa }}: {{ cliente_vendido.autorizacion }}
{% endif %}
</p>
</div>
<div class="width-48-right" style="text-align: left;">
<h1 class="p-t-5 p-l-5 line-10 cliente full-width-block">{{ cliente_enviado.header }}</h1>
<p class="p-l-5 line-8">{{ cliente_enviado.nombre }}</p>
<p class="p-l-5">{{ cliente_enviado.direccion }}
{% if cliente_enviado.num_exterior %} Ext: {{ cliente_enviado.num_exterior }}{% endif %}
{% if cliente_enviado.num_interior %} Int: {{ cliente_enviado.num_interior }}{% endif %}
<p class="p-l-5">{{ cliente_enviado.direccion }}
{% if cliente_enviado.num_exterior %} Ext: {{ cliente_enviado.num_exterior }}{% endif %}
{% if cliente_enviado.num_interior %} Int: {{ cliente_enviado.num_interior }}{% endif %}
</p>
<p class="p-l-5">{{ cliente_enviado.colonia }} {% if cliente_enviado.codigo_postal %} CP: {{
cliente_enviado.codigo_postal }}{% endif %}</p>
<p class="p-l-5">{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}
</p>
<p class="p-l-5">{{ cliente_enviado.colonia }} {% if cliente_enviado.codigo_postal %} CP: {{ cliente_enviado.codigo_postal }}{% endif %}</p>
<p class="p-l-5">{{ cliente_enviado.ciudad }}, {{ cliente_enviado.estado }}, {{ cliente_enviado.pais }}</p>
<p class="p-l-5">RFC: {{ cliente_enviado.tax_id }}</p>
</div>
</div>
<p class="p-t-8"><br /></p>
</header>
<table cellspacing="0" class="m-l-5" style="width: 99%; max-width: 580pt;">
<thead>
<table cellspacing="0" class="m-l-5" style="width: 99%; max-width: 580pt;">
<thead>
<tr class="h-10">
<td bgcolor="#E4E4E4" class="border" style="width:60pt"><p class="tiny line-9">Transportista:</p></td>
<td class="border" colspan="4" style="width:120pt"><p class="tiny">{{ factura.transportista }}</p></td>
<td class="border" style="width:40pt"><p class="tiny">SCAC: {{ factura.scac }}</p></td>
<td bgcolor="#E4E4E4" class="border" style="width:40pt"><p class="tiny">INCOTERM:</p></td>
<td class="border" style="width:40pt"><p class="tiny">{{ factura.incoterm }}</p></td>
<td bgcolor="#E4E4E4" class="border" style="width:60pt">
<p class="tiny line-9">Transportista:</p>
</td>
<td class="border" colspan="4" style="width:120pt">
<p class="tiny">{{ factura.transportista }}</p>
</td>
<td class="border" style="width:40pt">
<p class="tiny">SCAC: {{ factura.scac }}</p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:40pt">
<p class="tiny">INCOTERM:</p>
</td>
<td class="border" style="width:40pt">
<p class="tiny">{{ factura.incoterm }}</p>
</td>
<td class="border" style="width:50pt">
<p class="tiny line-9">Aduana: <span class="mini">{{ factura.aduana }}</span></p>
</td>
<td bgcolor="#E4E4E4" class="border" style="width:20pt"><p><br /></p></td>
<td bgcolor="#E4E4E4" class="border" style="width:20pt">
<p><br /></p>
</td>
</tr>
<tr class="h-10">
<td bgcolor="#E4E4E4" class="border" style="width:60pt">
<p class="tiny line-9">Transporte:</p>
</td>
<td class="border" colspan="4" style="width:120pt"><p class="tiny">{{ factura.transporte }}: {{ factura.num_transporte }}</p></td>
<td class="border" colspan="3" style="width:80pt"><p class="tiny">CAAT: {{ factura.caat }}</p></td>
</td>
<td class="border" colspan="4" style="width:120pt">
<p class="tiny">{{ factura.transporte }}: {{ factura.num_transporte }}</p>
</td>
<td class="border" colspan="3" style="width:80pt">
<p class="tiny">CAAT: {{ factura.caat }}</p>
</td>
<td class="border" colspan="2" style="width:100pt">
<p class="tiny p-t-1 line-8">Placas: {{ factura.placas or 'N/A' }}</p>
</td>
</tr>
<tr class="h-10">
<td class="border" colspan="2" style="width:80pt"><p><br /></p></td>
<td class="border" colspan="8"><p><br /></p></td>
<td class="border" colspan="2" style="width:80pt">
<p><br /></p>
</td>
<td class="border" colspan="8">
<p><br /></p>
</td>
</tr>
<tr class="h-10">
@@ -273,51 +529,51 @@
<p class="mini center">Total</p>
</td>
</tr>
</thead>
</thead>
<tbody class="h-384" style="width: 100%;">
{% for partida in partidas %}
<tr>
<td class="border">
<p class="mini p-t-3 center">{{ loop.index }}</p>
</td>
<td class="border">
<p class="mini p-t-1" style="font-weight: bold;">{{ partida.numero_parte }}</p>
<p class="mini">{{ partida.descripcion }}</p>
<p class="mini">Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}</p>
<p class="mini">
{% if partida.advalorem %}ADV: {{ partida.advalorem }}{% endif %}
{% if partida.preferencia %} / PREF: {{ partida.preferencia }}{% endif %}
</p>
</td>
<td class="border" colspan="2">
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">{{ partida.unidad_medida }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">
{% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %}
{{ partida.clave_bultos }}
</p>
</td>
<td class="border">
<p class="mini p-t-3 center">{{ partida.peso_neto }}</p>
</td>
<td class="border">
<p class="mini p-t-3 center">{{ partida.peso_bruto }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">${{ partida.valor_costo_unitario }}</p>
</td>
<td class="border">
<p class="mini p-t-2 right p-r-2">${{ partida.valor_total }}</p>
</td>
</tr>
{% endfor %}
<tr>
<td class="border">
<p class="mini p-t-3 center">{{ loop.index }}</p>
</td>
<td class="border">
<p class="mini p-t-1" style="font-weight: bold;">{{ partida.numero_parte }}</p>
<p class="mini">{{ partida.descripcion }}</p>
<p class="mini">Frac: {{ partida.fraccion }} / Orig: {{ partida.origen or 'MEX' }}</p>
<p class="mini">
{% if partida.advalorem %}ADV: {{ partida.advalorem }}{% endif %}
{% if partida.preferencia %} / PREF: {{ partida.preferencia }}{% endif %}
</p>
</td>
<td class="border" colspan="2">
<p class="mini p-t-2 center">{{ partida.cantidad_importacion }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">{{ partida.unidad_medida }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">
{% if partida.cantidad_bultos != 0 %}{{ partida.cantidad_bultos }}{% endif %}
{{ partida.clave_bultos }}
</p>
</td>
<td class="border">
<p class="mini p-t-3 center">{{ partida.peso_neto }}</p>
</td>
<td class="border">
<p class="mini p-t-3 center">{{ partida.peso_bruto }}</p>
</td>
<td class="border">
<p class="mini p-t-2 center">${{ partida.valor_costo_unitario }}</p>
</td>
<td class="border">
<p class="mini p-t-2 right p-r-2">${{ partida.valor_total }}</p>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="h-14">
<td bgcolor="#E4E4E4" class="border" colspan="2" style="width:123pt">
@@ -332,7 +588,7 @@
<td class="border" style="width:22pt"></td>
<td class="border" style="width:40pt">
<p class="tiny p-t-4 center line-8">
{% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %}
{% if totales.bultos_total != 0 %}{{ totales.bultos_total }}{% endif %}
<span>{{ totales.clave_bultos or '' }}</span>
</p>
</td>
@@ -355,17 +611,20 @@
<p style="border-bottom: 1pt solid black; width: 80%; margin: 0 auto 2pt auto;"></p>
<p class="normal center" style="margin-bottom: 0;">{{ cliente_proveedor.nombre }}</p>
<p style="margin-bottom: 0;"><br /></p>
<p class="small-bold center line-7" style="margin-bottom: 0;">Los valores expresados en esta factura son en: {{ factura.moneda }}</p>
<p class="small-bold center line-7" style="margin-bottom: 0;">Los valores expresados en esta factura
son en: {{ factura.moneda }}</p>
</td>
</tr>
</tr>
<tr>
<td colspan="10" style="width:580pt; vertical-align: top; height: 100%;">
<p class="p-t-8"><br /></p>
<p class="p-t-8"><br /></p>
<p class="p-l-5 line-8 tiny-bold">Normal Por Parte</p>
<p class="normal p-l-5 line-9">Declaro bajo protesta de decir verdad que la información contenida en este documento es verdadera y me hago responsable de comprobar lo aquí declarado.</p>
<p class="normal p-l-5 line-9">Declaro bajo protesta de decir verdad que la información contenida en
este documento es verdadera y me hago responsable de comprobar lo aquí declarado.</p>
</td>
</tr>
</tfoot>
</table>
</tr>
</tfoot>
</table>
</body>
</html>

View File

@@ -7,11 +7,11 @@ class Container(Base):
__tablename__ = "containers" # GContenedores
__table_args__ = (
PrimaryKeyConstraint("key", name="containers_pkey"),
{"schema": "public", "extend_existing": True}, # opcional
{"extend_existing": True}, # opcional
)
key: Mapped[str] = mapped_column(
String(3), nullable=False
String(3), primary_key=True, nullable=False
) # mantiene ceros iniciales
description: Mapped[str] = mapped_column(
String(500), nullable=False

View File

@@ -11,7 +11,7 @@ class MaterialType(Base):
)
key: Mapped[str] = mapped_column(
String(10), nullable=False) # clave del material
String(10), primary_key=True, nullable=False) # clave del material
type: Mapped[str] = mapped_column(String(15), nullable=False) # tipo
description: Mapped[str] = mapped_column(
String(256), nullable=False