diff --git a/backend/api/v1/modules/a24/discharges/models.py b/backend/api/v1/modules/a24/discharges/models.py index 25152746..2c8aed7f 100644 --- a/backend/api/v1/modules/a24/discharges/models.py +++ b/backend/api/v1/modules/a24/discharges/models.py @@ -26,7 +26,6 @@ import datetime from decimal import Decimal from enum import Enum from typing import TYPE_CHECKING, List, Optional -from ..discharges.models import DischargeDetail from sqlalchemy import ( BigInteger, diff --git a/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py b/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py index 9ab0af50..1e96b84a 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py @@ -9,12 +9,14 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_ if invoice.status == InvoiceStatus.PROCESSED: errors.add_error( "status", - "La factura ya fue procesada y no puede ser exportada", - solution=["Verifique el estatus de la factura antes de intentar exportarla"], + "La factura ya fue procesada y no puede volver a actualizarse. Desactualícela primero.", + solution=["Use el botón 'Desactualizar' antes de volver a procesar la factura."], code="ALREADY_PROCESSED", value=invoice.status, ) - + errors.raise_if_errors() + return + if not invoice.invoice_date: errors.add_required_error("invoice_date") diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py b/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py new file mode 100644 index 00000000..8f71e2b2 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/revert/main_process.py @@ -0,0 +1,234 @@ +from decimal import Decimal +from typing import List + +from sqlalchemy.orm import Session + +from api.v1.modules.a24.discharges.models import DischargeDetail, DischargeHeader, DischargeStatus +from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus +from api.v1.modules.a76.items.models import LineItem +from core.exceptions import ErrorCollector + +from .sub_process.review_rule_octave import borra_saldos_regla_octava + + +# ───────────────────────────────────────────────────────────────────────────── +# Validación de cantidades retornadas con detalle de exportaciones activas +# ───────────────────────────────────────────────────────────────────────────── + +def _validate_returned_quantities( + db: Session, + invoice: InvoiceHeader, + lines: List[LineItem], + errors: ErrorCollector, +) -> None: + """ + Verifica que ninguna partida tenga saldos pendientes por exportaciones + activas que la afecten. + Paridad: bloque 'REVISA CANTIDADES RETORNADAS' (Clarion SCAII). + + El Clarion recorre las partidas con (CantRetornadaTemp + CantRetornada + + CantExistencia) <> 0 y luego busca en QEqeMaq (exportaciones definitivas), + QEqeMaqRep (exportaciones de reparación) y QEqiMaqRep (importaciones de + reparación) para identificar qué factura de exportación activa (Estatus='AC') + tiene esa partida descargada. + + En Python los mismos vínculos viven en: + DischargeDetail.import_item_line_id → la partida de importación consumida + DischargeDetail.header → DischargeHeader + DischargeHeader.source_invoice_id → InvoiceHeader (la factura de exportación) + InvoiceHeader.status → InvoiceStatus.PROCESSED (≡ Estatus='AC') + + Para cada partida con saldo se buscan DischargeDetail con status APPLIED en + una factura de exportación procesada y se reporta qué factura debe + desactualizarse primero. + """ + lines_with_balance = [ + line for line in lines + if line.quantity is not None and ( + (line.quantity.quantity_returned_temp or Decimal(0)) + + (line.quantity.quantity_returned or Decimal(0)) + + (line.quantity.quantity_existence or Decimal(0)) + ) != Decimal(0) + ] + + if not lines_with_balance: + return + + for line in lines_with_balance: + qty_ret_temp = line.quantity.quantity_returned_temp or Decimal(0) + qty_ret = line.quantity.quantity_returned or Decimal(0) + qty_exist = line.quantity.quantity_existence or Decimal(0) + + # Buscar DischargeDetail vinculados a esta partida de importación + # cuya factura de exportación esté activa (PROCESSED). + # Paridad: bucle sobre QEqeMaq/QEqeMaqRep donde Descarga=1 y + # encabezado de exportación con Estatus='AC'. + details: List[DischargeDetail] = ( + db.query(DischargeDetail) + .join(DischargeDetail.header) + .filter( + DischargeDetail.import_item_line_id == line.id, + DischargeHeader.status == DischargeStatus.APPLIED, + ) + .all() + ) + + active_details = [ + d for d in details + if d.header + and d.header.source_invoice is not None + and d.header.source_invoice.status == InvoiceStatus.PROCESSED + ] + + if active_details: + # Reportar un error por cada factura de exportación activa distinta + # (equivale a QueErr en el Clarion). + seen_export_invoices: set = set() + for detail in active_details: + src_invoice = detail.header.source_invoice + src_number = src_invoice.invoice_number or str(src_invoice.id) + + if src_number in seen_export_invoices: + continue + seen_export_invoices.add(src_number) + + export_line_number = ( + detail.export_line.line_number if detail.export_line else "?" + ) + + errors.add_error( + field=f"line[{line.line_number}].discharge", + message=( + f"La Línea: {line.line_number} se ha descargado " + f"{detail.quantity_discharged} en la factura: {src_number} " + f"de exportación con Línea: {export_line_number}." + ), + solution=[ + f"Desactualizar la factura: {src_number} " + "para regresar saldos a la partida." + ], + code="LINE_HAS_ACTIVE_DISCHARGE", + ) + else: + # La partida tiene saldo pero no hay descarga activa rastreable — + # reportar el saldo directamente para que el usuario lo investigue. + errors.add_error( + field=f"line[{line.line_number}].quantities", + message=( + f"La Línea: {line.line_number} tiene saldos pendientes " + f"(retornada: {qty_ret}, retornada temp: {qty_ret_temp}, " + f"existencia: {qty_exist}) y no se puede desactualizar." + ), + solution=[ + "Verifique las exportaciones que afectan a esta partida " + "y desactualícelas primero." + ], + code="LINE_HAS_BALANCE", + ) + + +# ───────────────────────────────────────────────────────────────────────────── +# Reset de la factura e inventario +# ───────────────────────────────────────────────────────────────────────────── + +def _reset_invoice_financials(invoice: InvoiceHeader) -> None: + """ + Reinicia los totales financieros del encabezado de la factura a cero y + cambia el estatus a PENDING (equivalente a Estatus='NA' en Clarion). + Paridad: UPDATE QFacImp SET CantImpo=0, PesoNeto=0, PesoBruto=0, + Cantbultos=0, ValorImpoMN=0, ValorImpoME=0, ValorImpoMC=0, + ValorAduanasMN=0, ValorAduanasME=0, Estatus='NA', + ComofueProcesada='', ValorIVAMN=0, ValorIVAME=0 (Clarion SCAII). + """ + fin = invoice.financials + if fin is None: + return + + fin.total_quantity = 0.0 + fin.net_weight = 0.0 + fin.gross_weight = 0.0 + fin.total_packages = 0 + fin.value_mn = 0.0 + fin.value_me = 0.0 + fin.value_mc = 0.0 + fin.customs_value_mn = 0.0 + fin.customs_value_me = 0.0 + fin.iva_mn = 0.0 + fin.iva_me = 0.0 + + invoice.status = InvoiceStatus.PENDING + invoice.process_method = None + + +def _reset_line_quantities(lines: List[LineItem]) -> None: + """ + Reinicia los contadores de inventario de cada partida a cero. + Paridad: UPDATE QEqiMaq SET CantRetornada=0, CantRetornadaTemp=0, + ValorRetornadoMN=0, ValorRetornadoME=0, CantExistencia=0, + ValorIVAMNUsado=0, ValorIVAMEUsado=0 (Clarion SCAII). + """ + for line in lines: + if line.quantity is not None: + line.quantity.quantity_returned = Decimal(0) + line.quantity.quantity_returned_temp = Decimal(0) + line.quantity.quantity_existence = Decimal(0) + + if line.financial is not None: + line.financial.value_returned_mxn = Decimal(0) + line.financial.value_returned_usd = Decimal(0) + line.financial.vat_used_mxn = Decimal(0) + line.financial.vat_used_usd = Decimal(0) + + +# ───────────────────────────────────────────────────────────────────────────── +# Proceso principal de reversión +# ───────────────────────────────────────────────────────────────────────────── + +def revert_process( + db: Session, + invoice: InvoiceHeader, + lines: List[LineItem], + tenant_id: str, + company_id: str, + errors: ErrorCollector, +) -> list: + """ + Proceso principal de des-actualización de una factura de importación + temporal. + Paridad: rutina principal 'DESACTUALIZAR FACTURA IMPO TEMP' (Clarion SCAII). + + Flujo: + 1. Verifica que ninguna partida tenga saldos activos por exportaciones + procesadas (QueueErrorAct en Clarion). Si los hay → ValidationException. + 2. Si no hay errores de validación: + a. Do BORRASALDOS_REGLA_OCTAVA — revierte cupos de Regla Octava. + b. UPDATE QFacImp — reinicia totales del encabezado (status → PENDING). + c. UPDATE QEqiMaq — reinicia contadores de inventario por partida. + 3. Retorna sql_errors (errores no-bloqueantes de BD, equivalente a + QueueErrorSQL en Clarion). + + Raises: + ValidationException: si hay partidas con descargas activas + (equivale a Records(QueueErrorAct) <> 0). + """ + # ── Paso 1: REVISA CANTIDADES RETORNADAS ────────────────────────────────── + _validate_returned_quantities(db, invoice, lines, errors) + errors.raise_if_errors() + + # ── Paso 2a: Do BORRASALDOS_REGLA_OCTAVA ───────────────────────────────── + sql_errors: list = [] + borra_saldos_regla_octava( + db=db, + invoice_import=invoice.invoice_number or "", + tenant_id=tenant_id, + company_id=company_id, + sql_errors=sql_errors, + ) + + # ── Paso 2b: UPDATE QFacImp ─────────────────────────────────────────────── + _reset_invoice_financials(invoice) + + # ── Paso 2c: UPDATE QEqiMaq ─────────────────────────────────────────────── + _reset_line_quantities(lines) + + return sql_errors diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/pre_validators.py b/backend/api/v1/modules/a76/invoices/imports/revert/pre_validators.py new file mode 100644 index 00000000..cc9b6f8e --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/revert/pre_validators.py @@ -0,0 +1,42 @@ +from typing import List + +from sqlalchemy.orm import Session + +from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceStatus +from api.v1.modules.a76.items.models import LineItem +from core.exceptions import ErrorCollector + + +def pre_validators( + db: Session, + invoice: InvoiceHeader, + tenant_id: str, + company_id: str, + errors: ErrorCollector, +) -> List[LineItem]: + """ + Validaciones previas a la reversión de una factura de importación temporal. + + - Verifica que la factura esté en estatus PROCESSED. + - Carga y retorna las partidas asociadas a la factura. + """ + if invoice.status != InvoiceStatus.PROCESSED: + errors.add_error( + "status", + "La factura no fue procesada y no puede ser revertida", + solution=["Verifique el estatus de la factura antes de intentar deshacer el proceso"], + code="NOT_PROCESSED", + value=invoice.status, + ) + + lines: List[LineItem] = ( + db.query(LineItem) + .filter( + LineItem.invoice_id == invoice.id, + LineItem.tenant_id == tenant_id, + LineItem.company_id == company_id, + ) + .all() + ) + + return lines diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/routes.py b/backend/api/v1/modules/a76/invoices/imports/revert/routes.py new file mode 100644 index 00000000..e380d746 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/revert/routes.py @@ -0,0 +1,76 @@ +from typing import Any, Dict + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +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 revert_invoice_task + +router = APIRouter() + + +@router.post("/invoices/{invoice_id}/revert") +def trigger_invoice_revert( + invoice_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """ + Inicia la des-actualización de una factura de importación temporal como + tarea Celery. + Retorna el task_id para hacer polling del progreso. + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + + task = revert_invoice_task.apply_async( + args=[invoice_id, str(tenant_id), str(company_id)] + ) + + return {"task_id": task.id} + + +@router.get("/invoices/revert/{task_id}/status") +def get_invoice_revert_status(task_id: str): + """ + Consulta el estado de progreso de una tarea de des-actualización de + factura. + + Retorna: + - state: 'PROCESSING' | 'SUCCESS' | 'FAILURE' + - info: { current: int, status: str } (cuando state == 'PROCESSING') + - result: dict (cuando state == 'SUCCESS' o 'FAILURE') + """ + task_result = celery_app.AsyncResult(task_id) + + if task_result.state in ("PENDING", "STARTED"): + return { + "state": "PROCESSING", + "info": {"current": 0, "status": "Iniciando..."}, + } + + if task_result.state == "PROGRESS": + return { + "state": "PROCESSING", + "info": task_result.info or {"current": 0, "status": "Procesando..."}, + } + + if task_result.state == "SUCCESS": + return { + "state": "SUCCESS", + "result": task_result.result, + } + + error_info = task_result.result + if isinstance(error_info, Exception): + error_msg = str(error_info) + else: + error_msg = str(error_info) if error_info else "Error desconocido" + + return { + "state": "FAILURE", + "result": error_msg, + } diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/sub_process/review_rule_octave.py b/backend/api/v1/modules/a76/invoices/imports/revert/sub_process/review_rule_octave.py new file mode 100644 index 00000000..94805475 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/revert/sub_process/review_rule_octave.py @@ -0,0 +1,94 @@ +from decimal import Decimal +from typing import List + +from sqlalchemy.orm import Session + +from api.v1.modules.a76.rule_octave.balances.models import OctaveBalance +from api.v1.modules.a76.rule_octave.fractions.models import FractionRuleOctave + + +def borra_saldos_regla_octava( + db: Session, + invoice_import: str, + tenant_id: str, + company_id: str, + sql_errors: list, +) -> None: + """ + Revierte los saldos de Regla Octava registrados al procesar una factura de + importación temporal. + Paridad: BORRASALDOS_REGLA_OCTAVA (Clarion SCAII). + + Por cada registro en SSaldosReglaOctava (OctaveBalance) con origin='TEM' y + system='SCAF' que corresponda a la factura: + 1. Resta de vuelta la cantidad y el valor en GFracROctava (FractionRuleOctave). + 2. Elimina el registro de OctaveBalance. + + Los errores de actualización se acumulan en sql_errors como dicts con las + claves 'consecutive' y 'error'. + """ + consecutive_ref = [0] + + balances: List[OctaveBalance] = ( + db.query(OctaveBalance) + .filter( + OctaveBalance.tenant_id == tenant_id, + OctaveBalance.company_id == company_id, + OctaveBalance.invoice_import == invoice_import, + OctaveBalance.origin == "TEM", + OctaveBalance.system == "SCAF", + ) + .all() + ) + + for balance in balances: + fra_oct = ( + db.query(FractionRuleOctave) + .filter( + FractionRuleOctave.tenant_id == tenant_id, + FractionRuleOctave.company_id == company_id, + FractionRuleOctave.permission == balance.octave_permit, + FractionRuleOctave.line == balance.line, + ) + .first() + ) + + # REGRESAR EL SALDO AL PERMISO DE REGLA OCTAVA + # Si el permiso no existe (TryFetch falla en Clarion), se omite el PUT + # pero el DELETE del saldo se ejecuta de todas formas — paridad Clarion. + if fra_oct is not None: + qty_back = balance.quantity_stock or Decimal(0) + val_back = balance.value_me or Decimal(0) + + fra_oct.quantity_used = max( + Decimal(0), + (fra_oct.quantity_used or Decimal(0)) - qty_back, + ) + fra_oct.value_used = max( + Decimal(0), + (fra_oct.value_used or Decimal(0)) - val_back, + ) + + try: + db.flush([fra_oct]) + except Exception as exc: + consecutive_ref[0] += 1 + sql_errors.append({ + "consecutive": consecutive_ref[0], + "error": ( + f"Error al regresar el Cupo en (Permiso de Regla Octava) {exc}" + ), + }) + + # DELETE(SSaldosReglaOctava) — siempre se intenta, igual que en Clarion + try: + db.delete(balance) + db.flush([balance]) + except Exception as exc: + consecutive_ref[0] += 1 + sql_errors.append({ + "consecutive": consecutive_ref[0], + "error": ( + f"Error al Eliminar en (SSaldosReglaOctava) {exc}" + ), + }) diff --git a/backend/api/v1/modules/a76/invoices/imports/revert/task.py b/backend/api/v1/modules/a76/invoices/imports/revert/task.py new file mode 100644 index 00000000..039baba7 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/revert/task.py @@ -0,0 +1,82 @@ +from celery import Task + +from core.celery_app import celery_app +from core.database import CoreSessionLocal +from core.exceptions import ErrorCollector, ValidationException + +from api.v1.modules.a76.invoices.models import InvoiceHeader +from .pre_validators import pre_validators +from .main_process import revert_process + + +def _progress(task: Task, current: int, status: str) -> None: + task.update_state(state="PROGRESS", meta={"current": current, "status": status}) + + +@celery_app.task(bind=True, name="revert_invoice_task") +def revert_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id: str) -> dict: + """ + Des-actualiza una factura de importación temporal ejecutando todas las + validaciones y reversiones del proceso principal (revert/main_process) con + reporte de progreso. + """ + db = CoreSessionLocal() + try: + # ── Paso 1: Cargar factura ──────────────────────────────────────────── + _progress(self, 5, "Cargando factura...") + invoice: InvoiceHeader | None = db.get(InvoiceHeader, invoice_id) + if invoice is None: + return { + "status": "error", + "message": f"Factura con id {invoice_id} no encontrada.", + "errors": [], + } + + errors = ErrorCollector() + + # ── Paso 2: Pre-validaciones ────────────────────────────────────────── + _progress(self, 10, "Validando estatus de la factura...") + lines = pre_validators(db, invoice, tenant_id, company_id, errors) + if not lines: + errors.add_error( + field="line_items", + message="La factura no contiene partidas para revertir", + solution=["Verifique que la factura tenga partidas antes de intentar revertirla"], + code="NO_LINE_ITEMS", + ) + errors.raise_if_errors() + + # ── Paso 3: Validar cantidades y ejecutar reversión ─────────────────── + _progress(self, 40, "Verificando saldos de partidas...") + sql_errors = revert_process( + db=db, + invoice=invoice, + lines=lines, + tenant_id=tenant_id, + company_id=company_id, + errors=errors, + ) + + # ── Paso 4: Confirmar transacción ───────────────────────────────────── + _progress(self, 95, "Confirmando cambios...") + db.flush() + db.commit() + + return { + "status": "success", + "invoice_id": invoice_id, + "sql_errors": sql_errors, + } + + except ValidationException as exc: + db.rollback() + return { + "status": "validation_error", + "message": exc.message, + "errors": exc.errors, + } + except Exception as exc: + db.rollback() + raise exc + finally: + db.close() diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 0b8283ae..33e0dd48 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -11,6 +11,7 @@ from .customs_brokers.routes import router as customs_broker_router from .general_catalogs.router import router as general_catalogs_router from .invoices.routes import router as invoices_router from .invoices.imports.process.routes import router as invoice_process_router +from .invoices.imports.revert.routes import router as invoice_revert_router from .items.routes import router as items_router from .classes.routes import router as classes_router @@ -59,6 +60,7 @@ router = APIRouter() router.include_router(general_catalogs_router, prefix="/a76", tags=["a76 / general_catalogs"]) router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"]) router.include_router(invoice_process_router, prefix="/a76", tags=["a76 / invoices"]) +router.include_router(invoice_revert_router, prefix="/a76", tags=["a76 / invoices"]) router.include_router(items_router, prefix="/a76", tags=["a76 / items"]) router.include_router(imports_router, prefix="/a76/imports", tags=["a76 / imports"]) router.include_router(exportacion_imports_router, prefix="/a76/imports/exportacion", tags=["a76 / imports / exportacion"]) diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index ef6c93e3..1df5980b 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -16,6 +16,9 @@ from api.v1.modules.public.reference_data.customs_sections.models import Customs # CRITICAL: FaLineItem must be imported BEFORE LineItem from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem # noqa: F401 from api.v1.modules.a76.items.models import LineItem # noqa: F401 +# CRITICAL: BalanceMovement must be loaded before DischargeDetail (FK a24.balance_movement) +from api.v1.modules.a24.balance_movements.models import BalanceMovement # noqa: F401 +from api.v1.modules.a24.discharges.models import DischargeHeader, DischargeDetail # noqa: F401 valkey_url = os.getenv("VALKEY_URL", "redis://valkey:6379/0") print(f"DEBUG: Celery Broker URL: {valkey_url}") @@ -60,6 +63,7 @@ celery_app.conf.update( "api.v1.modules.core.help_center.tasks", "api.v1.modules.core.help_center.tasks", "api.v1.modules.a76.invoices.imports.process.task", + "api.v1.modules.a76.invoices.imports.revert.task", ] # Ruta al módulo donde están las tareas ) diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 4cf0a679..371a8159 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -515,5 +515,29 @@ export const invoicesApi = { sql_errors?: Array<{ consecutive: number; error: string }>; }; }>(`/v1/a76/invoices/process/${taskId}/status`); + }, + + revertInvoice: (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post<{ task_id: string }>( + `/v1/a76/invoices/${invoiceId}/revert?${params.toString()}`, + {} + ); + }, + + getRevertStatus: (taskId: string) => { + return api.get<{ + state: 'PROCESSING' | 'SUCCESS' | 'FAILURE'; + info?: { current: number; status: string }; + result?: { + status: 'success' | 'validation_error' | 'error'; + invoice_id?: number; + message?: string; + errors?: Array<{ field: string; message: string; code?: string; solution?: string[] }>; + sql_errors?: Array<{ consecutive: number; error: string }>; + }; + }>(`/v1/a76/invoices/revert/${taskId}/status`); } }; diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index 62148a60..9c4ffd83 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -86,6 +86,26 @@ export function createColumns( enableSorting: false, enableHiding: false, }, + { + id: "processed", + header: "Procesada", + cell: ({ row }) => { + const s = row.original.status; + const isProcessed = s === "processed" || s === true; + + const processedCheckSnippet = createRawSnippet<[{ checked: boolean }]>((getProps) => { + const { checked } = getProps(); + return { + render: () => `