preparar_json del 501 reventaba (TypeError/AttributeError) cuando OPCIONDESTINO o CLAVETRANSFER venian NULL: get(k, "") no protege porque un NULL de BD llega como None y el default no aplica. Pasa con pedimentos que baja SCAII_Sync (dejan esos campos en NULL); WinDev los llenaba. Se usa "or ''" en SCAII y SCAF. Ademas, el hilo de sincronizacion tragaba la excepcion con print (invisible en el exe windowed), por lo que parecia un cuelgue. Ahora se registra en el log con traza (log.error exc_info). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
203 lines
8.7 KiB
Python
203 lines
8.7 KiB
Python
import flet as ft
|
|
import threading
|
|
from typing import Optional
|
|
from logger_utils import log
|
|
|
|
class InterfazFlet:
|
|
def __init__(self, cliente_sinc=None):
|
|
self.cliente = cliente_sinc
|
|
self.page: Optional[ft.Page] = None
|
|
|
|
self.COLOR_TEXTO_TITULO = "#0066CC"
|
|
self.COLOR_FRANJA_CELESTE = "#7CE0F6"
|
|
|
|
self.lista_resultados = ft.Column(
|
|
expand=True, spacing=2, scroll=ft.ScrollMode.ALWAYS, auto_scroll=True
|
|
)
|
|
|
|
self.cabecera_lista = ft.Container(
|
|
content=ft.Row([
|
|
ft.Text("Pedimento", width=150, color=ft.Colors.WHITE, weight="bold", size=12),
|
|
ft.Text("Factura", width=120, color=ft.Colors.WHITE, weight="bold", size=12),
|
|
ft.Text("Remesa", width=80, color=ft.Colors.WHITE, weight="bold", size=12),
|
|
ft.Text("Estatus de Sincronización", expand=True, color=ft.Colors.WHITE, weight="bold", size=12),
|
|
]),
|
|
bgcolor=ft.Colors.BLUE_GREY_900,
|
|
padding=ft.padding.only(left=20, top=10, right=20, bottom=10),
|
|
border_radius=ft.border_radius.only(top_left=5, top_right=5)
|
|
)
|
|
|
|
self.barra_progreso = ft.ProgressBar(value=0, color=ft.Colors.BLUE_600, bgcolor=ft.Colors.GREY_300)
|
|
self.status_label = ft.Text("Listo", color=ft.Colors.BLACK, size=13, weight="w500")
|
|
self.porcentaje_label = ft.Text("0%", color=ft.Colors.BLUE_700, weight="bold", size=14)
|
|
|
|
def main(self, page: ft.Page):
|
|
self.page = page
|
|
page.title = "Sistema de Control de Aduanas e Inventarios"
|
|
page.theme_mode = ft.ThemeMode.LIGHT
|
|
page.window_width = 1000
|
|
page.window_height = 650
|
|
page.padding = 20
|
|
page.bgcolor = ft.Colors.GREY_50
|
|
|
|
titulo = ft.Text("Comunicación SCAII - WINSAAI.", size=24, color=self.COLOR_TEXTO_TITULO, weight="bold")
|
|
|
|
franjas_decorativas = ft.Column([
|
|
ft.Container(height=3, bgcolor=ft.Colors.BLACK),
|
|
ft.Container(height=15, bgcolor=self.COLOR_FRANJA_CELESTE)
|
|
], spacing=0)
|
|
|
|
lista_container = ft.Container(
|
|
content=ft.Column([self.cabecera_lista, self.lista_resultados], spacing=0),
|
|
expand=True, bgcolor=ft.Colors.WHITE,
|
|
border=ft.border.all(1, ft.Colors.GREY_300), border_radius=8
|
|
)
|
|
|
|
footer = ft.Container(
|
|
content=ft.Row([
|
|
ft.Column([
|
|
ft.Row([self.status_label, ft.Container(expand=True), self.porcentaje_label]),
|
|
self.barra_progreso
|
|
], expand=True, spacing=5)
|
|
], alignment=ft.MainAxisAlignment.START, vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
|
padding=15, bgcolor=ft.Colors.WHITE, border_radius=8,
|
|
border=ft.border.all(1, ft.Colors.GREY_200),
|
|
shadow=ft.BoxShadow(blur_radius=5, color=ft.Colors.GREY_200, offset=ft.Offset(0, 2))
|
|
)
|
|
|
|
page.add(titulo, franjas_decorativas, lista_container,
|
|
ft.Divider(height=10, color=ft.Colors.TRANSPARENT), footer)
|
|
|
|
if self.cliente:
|
|
threading.Thread(target=self._hilo_sincronizacion, daemon=True).start()
|
|
|
|
# ------------------------------------------------------------------ #
|
|
def agregar_fila(self, pedimento, factura, remesa, datos_completos, estatus):
|
|
log.info(f"[UI] agregar_fila llamado (page={'OK' if self.page else 'None'}): pedi={pedimento} fac={factura}")
|
|
async def _do():
|
|
try:
|
|
# Panel de detalles (oculto inicialmente)
|
|
campos_relevantes = {
|
|
"NumFactura": "Factura",
|
|
"Remesa": "Remesa",
|
|
"IdProveedor": "ID Proveedor",
|
|
"ValorDolares": "Valor Dólares",
|
|
"PesoBruto": "Peso Bruto",
|
|
"Transportista": "Transportista",
|
|
"NumTrailer": "Caja/Trailer",
|
|
"Incoterm": "Incoterm",
|
|
"Consecutivo": "Folio Interno"
|
|
}
|
|
|
|
filas_detalles = [
|
|
ft.Row([
|
|
ft.Text(f"{etiqueta}:", weight="bold", width=120, size=12, color=ft.Colors.GREY_700),
|
|
ft.Text(str(datos_completos.get(clave, "N/A")), size=12, color=ft.Colors.BLUE_700),
|
|
], spacing=10)
|
|
for clave, etiqueta in campos_relevantes.items()
|
|
]
|
|
|
|
panel_detalles = ft.Container(
|
|
content=ft.Column(filas_detalles, spacing=6),
|
|
visible=False, # <-- oculto por defecto
|
|
bgcolor=ft.Colors.BLUE_50,
|
|
padding=ft.padding.only(left=30, top=10, right=20, bottom=10),
|
|
border=ft.border.only(
|
|
bottom=ft.BorderSide(1, ft.Colors.BLUE_100),
|
|
left=ft.BorderSide(3, ft.Colors.BLUE_400), # acento visual izquierdo
|
|
),
|
|
)
|
|
|
|
icono_expand = ft.Icon(ft.Icons.KEYBOARD_ARROW_DOWN, size=16, color=ft.Colors.GREY_400)
|
|
|
|
def toggle_detalles(e, p=panel_detalles, i=icono_expand):
|
|
p.visible = not p.visible
|
|
i.name = ft.Icons.KEYBOARD_ARROW_UP if p.visible else ft.Icons.KEYBOARD_ARROW_DOWN
|
|
self.page.update()
|
|
|
|
fila_header = ft.Container(
|
|
content=ft.Row([
|
|
ft.Text(pedimento, width=150, size=12, color=ft.Colors.BLACK),
|
|
ft.Text(factura, width=120, size=12, color=ft.Colors.BLACK),
|
|
ft.Text(remesa, width=80, size=12, color=ft.Colors.BLACK),
|
|
ft.Text(estatus, expand=True, size=12, color=ft.Colors.BLUE_700, weight="bold"),
|
|
icono_expand,
|
|
]),
|
|
padding=ft.padding.symmetric(horizontal=20, vertical=8),
|
|
border=ft.border.only(bottom=ft.BorderSide(1, ft.Colors.GREY_100)),
|
|
on_click=toggle_detalles,
|
|
ink=True,
|
|
tooltip="Clic para ver detalles"
|
|
)
|
|
|
|
# Agrupamos header + panel en una Column
|
|
fila_completa = ft.Column([fila_header, panel_detalles], spacing=0)
|
|
self.lista_resultados.controls.append(fila_completa)
|
|
self.page.update()
|
|
log.info(f"[UI] fila agregada OK. Total filas en tabla: {len(self.lista_resultados.controls)}")
|
|
except Exception as ex:
|
|
log.error(f"[UI] Error en agregar_fila._do: {ex}")
|
|
|
|
if self.page:
|
|
self.page.run_task(_do)
|
|
|
|
|
|
# ------------------------------------------------------------------ #
|
|
def actualizar_estado_fila(self, indice, estatus):
|
|
async def _do():
|
|
if indice < len(self.lista_resultados.controls):
|
|
# controls[indice] = Column → controls[0] = header Container → Row → controls[3] = Text estatus
|
|
fila_header = self.lista_resultados.controls[indice].controls[0]
|
|
fila_header.content.controls[3].value = estatus
|
|
self.page.update()
|
|
if self.page:
|
|
self.page.run_task(_do)
|
|
|
|
|
|
def actualizar_progreso(self, valor):
|
|
async def _do():
|
|
self.barra_progreso.value = valor
|
|
self.porcentaje_label.value = f"{int(valor * 100)}%"
|
|
self.page.update()
|
|
if self.page:
|
|
self.page.run_task(_do)
|
|
|
|
def actualizar_estado_texto(self, texto):
|
|
async def _do():
|
|
self.status_label.value = texto
|
|
self.page.update()
|
|
if self.page:
|
|
self.page.run_task(_do)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
def _hilo_sincronizacion(self):
|
|
try:
|
|
if self.cliente:
|
|
self.cliente.proceso_principal()
|
|
|
|
async def _done():
|
|
self.status_label.value = "Listo"
|
|
self.barra_progreso.value = 1.0
|
|
self.porcentaje_label.value = "100%"
|
|
self.page.update()
|
|
if self.page:
|
|
self.page.run_task(_done)
|
|
|
|
except Exception as ex:
|
|
log.error(f"[UI ERROR] Fallo en el hilo de sincronizacion: {ex}", exc_info=True)
|
|
async def _err():
|
|
self.status_label.value = f"Error: {ex}"
|
|
self.page.update()
|
|
if self.page:
|
|
self.page.run_task(_err)
|
|
|
|
|
|
def lanzar_interfaz(cliente=None):
|
|
app = InterfazFlet(cliente)
|
|
if cliente:
|
|
cliente.gui = app
|
|
ft.app(target=app.main)
|
|
return app
|
|
|
|
if __name__ == "__main__":
|
|
ft.app(target=InterfazFlet().main) |