feature/botones-vu-more-actions

This commit is contained in:
2026-04-07 12:30:32 -06:00
parent df4024436b
commit 2e529a0026
4 changed files with 150 additions and 10 deletions

View File

@@ -59,11 +59,8 @@ def trigger_cove_for_invoice(
task=factura_cove_generate,
tenant_id=tenant_id,
company_id=body.company_id,
requested_by_user=(
current_user.get("preferred_username")
or current_user.get("email")
or current_user.get("sub")
),
# Hardcodeado a petición: siempre registrar esta tarea con el correo de Hugo Reyes.
requested_by_user="hreyes@aduanasoft.com.mx",
task_name="factura_cove_generate",
task_group="factura_cove",
task_origin="a76/factura_cove/invoices/cove",

View File

@@ -9,6 +9,7 @@ from celery import Task
from core.celery_app import celery_app
from core.database import CoreSessionLocal
from core.exceptions import ValidationException
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
from .service import FacturaCoveDomainService
from .schemas import GenerateCoveResult
@@ -21,6 +22,50 @@ def _progress(task: Task, current: int, status: str) -> None:
task.update_state(state="PROGRESS", meta={"current": current, "status": status})
def _save_cove_result(
db: "Session", invoice_id: int, final_external: CoveExternalResult
) -> None:
"""
Persiste en la factura el número de COVE y el número de operación VUCEM
cuando el servicio externo reporta SUCCESS.
"""
if final_external.status != "success":
return
if not final_external.cove_number and not final_external.vucem_operation_num:
return
invoice = db.get(InvoiceHeader, invoice_id)
if not invoice:
logger.error("No se encontró la factura %s para guardar COVE", invoice_id)
return
compliance = invoice.compliance_mx
if not compliance:
# Creamos un registro mínimo de compliance ligado a la factura.
compliance = InvoiceComplianceMx(
invoice_id=invoice.id,
tenant_id=invoice.tenant_id,
company_id=invoice.company_id,
)
db.add(compliance)
# Idempotencia básica: solo sobrescribir si está vacío o coincide.
if final_external.cove_number:
current_cove = compliance.edocument or ""
new_cove = final_external.cove_number or ""
if not current_cove or current_cove == new_cove:
compliance.edocument = new_cove
if final_external.vucem_operation_num:
current_op = compliance.vucem_operation_num or ""
new_op = final_external.vucem_operation_num or ""
if not current_op or current_op == new_op:
compliance.vucem_operation_num = new_op
db.commit()
def _poll_external_status(
task: Task, external: CoveExternalService, external_task_id: str, timeout_seconds: int = 300
) -> CoveExternalResult:
@@ -171,6 +216,13 @@ def factura_cove_generate(self: Task, invoice_id: int, tenant_id: int, company_i
# Fallback: usamos el resultado tal cual devolvió el endpoint de generación
final_external = external_result
# Intentar persistir COVE / número de operación en la factura cuando sea éxito.
try:
_save_cove_result(db, invoice_id, final_external)
except Exception:
# No fallamos la tarea por errores de persistencia; solo los registramos.
logger.exception("Error guardando COVE en la factura %s", invoice_id)
_progress(self, 100, "Proceso de COVE finalizado.")
result = GenerateCoveResult(

View File

@@ -217,6 +217,17 @@
let hasMore = $derived(allItems.length < totalItems);
let error = $state<string | null>(data.error || null);
// Submenú contextual para Interface VU (click derecho)
let showVuSubmenu = $state(false);
let vuSubmenuPosition = $state({ x: 0, y: 0 });
// Si se pierde la selección, cerramos el submenú contextual VU
$effect(() => {
if (!hasSelection) {
showVuSubmenu = false;
}
});
// Estado para selección de filas (múltiple)
let selectedInvoiceIds = $state<number[]>([]);
// Estado para los diálogos de acciones
@@ -1129,6 +1140,65 @@
</AlertDialog.Content>
</AlertDialog.Root>
{#if showVuSubmenu}
<!-- Capa para cerrar el submenú al hacer click fuera -->
<div
class="fixed inset-0 z-40"
on:click={() => (showVuSubmenu = false)}
on:contextmenu|preventDefault={() => (showVuSubmenu = false)}
/>
<!-- Submenú contextual de Interface VU -->
<div
class="fixed z-50 min-w-48 rounded-md border bg-popover p-1 text-sm shadow-md"
style={`top: ${vuSubmenuPosition.y}px; left: ${vuSubmenuPosition.x}px;`}
>
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent"
on:click={() => {
showVuSubmenu = false;
toast.info('Consulta VU - Próximamente');
}}
>
<Files class="mr-2 h-4 w-4" />
<span>Consulta</span>
</button>
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent"
on:click={() => {
showVuSubmenu = false;
toast.info('Adenda VU - Próximamente');
}}
>
<Files class="mr-2 h-4 w-4" />
<span>Adenda</span>
</button>
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent"
on:click={() => {
showVuSubmenu = false;
handleGenerateCove();
}}
>
<Files class="mr-2 h-4 w-4" />
<span>Acuse de COVE</span>
</button>
<button
type="button"
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left hover:bg-accent"
on:click={() => {
showVuSubmenu = false;
toast.info('COVE Masivos - Próximamente');
}}
>
<Files class="mr-2 h-4 w-4" />
<span>COVE Masivos</span>
</button>
</div>
{/if}
<!-- Footer fijo con botones de acción -->
<div
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
@@ -1200,7 +1270,13 @@
</DropdownMenu.Root>
<!-- Dropdown: Más Acciones -->
<DropdownMenu.Root>
<DropdownMenu.Root
onOpenChange={(open) => {
if (!open) {
showVuSubmenu = false;
}
}}
>
<DropdownMenu.Trigger>
{#snippet child({ props })}
<Button
@@ -1224,9 +1300,24 @@
<Send class="mr-2 h-4 w-4" />
Transferencia Electrónica
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleGenerateCove}>
<!-- Interface VU: click izquierdo = COVE, click derecho = submenú -->
<DropdownMenu.Item
class="cursor-pointer"
onclick={() => handleGenerateCove()}
oncontextmenu={(event) => {
event.preventDefault();
event.stopPropagation();
const target = event.currentTarget as HTMLElement;
const rect = target.getBoundingClientRect();
vuSubmenuPosition = {
x: rect.right,
y: rect.top
};
showVuSubmenu = true;
}}
>
<MonitorUp class="mr-2 h-4 w-4" />
Interface VU
<span>Interface VU</span>
</DropdownMenu.Item>
<DropdownMenu.Item onclick={() => toast.info('Cons SED - Próximamente')}>
<ScrollText class="mr-2 h-4 w-4" />

View File

@@ -329,8 +329,8 @@
electronic_signature: invoice.compliance_mx?.electronic_signature || '',
mandatory_person: invoice.compliance_mx?.mandatory_person || '',
contingency_mode: invoice.compliance_mx?.contingency_mode || false,
cove: invoice.compliance_mx?.cove || '',
operation_num: invoice.compliance_mx?.operation_num || '',
cove: invoice.compliance_mx?.edocument || '',
operation_num: invoice.compliance_mx?.vucem_operation_num || '',
adendas: invoice.compliance_mx?.adendas || '',
observations_vu: invoice.compliance_mx?.observations_vu || '',
certified_number: invoice.compliance_mx?.certified_number || '',