Novedades
Los últimos cambios de Paxapos: mejoras, funcionalidades nuevas y correcciones, tal como se van publicando en cada versión.
Versión 3.16.0 — 2026-10-03
Nuevo
- [Compras] Las órdenes de pago pueden mostrar otras deducciones, como el fondo de garantía o los aportes a cajas profesionales, con su nombre: se restan del neto a cobrar y el proveedor las ve en su portal.
- [Contabilidad] Cada clasificación de gastos puede marcarse como CAPEX (bien de uso) de forma explícita, sin depender del nombre del rubro.
- [Stock] Los movimientos de stock manejan cantidades con más precisión, hasta seis decimales.
- [Inicio] Pedidos de clientes, Recomendaciones IA y la Grilla de reservas tienen su propio ícono.
Cambios
- [Roles] Más control por rol en las pantallas de edición: cada usuario ve y modifica lo que su rol le habilita. El rol Auditor es de solo consulta, salvo en Contabilidad y Facturación electrónica.
- [Fichaje] Capa extra de verificación al registrar ingresos y egresos. Después de actualizar, si tenés abierta la pantalla "Trabajando ahora", recargala.
- [RRHH] Los catálogos compartidos (sindicatos, convenios, obras sociales, conceptos de AFIP, adicionales y plantillas) pasan a ser mantenidos por el equipo de PaxaPOS para todos por igual.
- [Facturación electrónica] Al dar de alta o modificar un punto de venta, el sistema renueva solo la conexión con ARCA y reintenta, para que una delegación recién cargada se reconozca sin esperas.
- [Listados] Los listados mantienen el mismo orden al pasar de página, y los filtros de Presupuestos de evento se conservan al paginar.
- [Salón] El listado de mesas carga más rápido.
- [Compras] El buscador de mercaderías de las órdenes de compra muestra primero la coincidencia exacta.
Corregido
- [Recetas] Los selectores de unidad reconocen las unidades personalizadas, por ejemplo "Kg".
_Incorpora lo que iba a salir como v3.11.7 y v3.11.8: ninguno de los dos tags se llegó a
publicar como imagen, así que 3.11.9 es la primera versión de esta tanda de cambios que
efectivamente se despliega._
- Sin cambios de schema (DDL): no hay migraciones nuevas desde 3.11.6.
pedimelo-onlinev3.1.13 tiene que salir antes o junto con esta versión: #975 saca el bloquepagosdepublic_configy lo reemplaza pormedios_pago; una versión vieja de pedimelo contra un cakephp 3.11.9 se queda sin leer los medios de pago online.apiv3.2.36 ybotv3.1.20 acompañan este release.- La limpieza de claves de configuración obsoletas (#970, Fix 11 de
update_tenants) no cambia el fingerprint del schema del tenant, así que el shell no la corre sola: hace falta--force. Es opcional y se puede aplicar tenant por tenant. - Comportamiento nuevo de #980 a tener en cuenta: un pago de Mercado Pago que llega aprobado sobre una mesa que ya estaba saldada se registra igual (pasa por la misma cadena que el webhook) y queda logueado como "cobro de más, revisar reembolso" — el reembolso en sí sigue siendo manual.
Nuevo
- [Proveedores] El listado de Finanzas › Proveedores muestra "Conectado" o "Pendiente" junto al proveedor cuando hay un vínculo entre comercios con él
- [Pedidos de clientes] Nueva sección "Clientes conectados" en la bandeja del proveedor, con el estado y la fecha de conexión de cada comercio vinculado
- [Compras] Botón "Enviar al proveedor" en la orden de compra, para compartir con un proveedor recién conectado una OC que ya existía de antes
- [Configuración] Nueva pantalla "Configuración general", con todas las opciones agrupadas por tema (Comercio, Fiscal y ARCA, Módulos, Ventas y salón, Impresión, Compras y stock, Caja, RRHH, Delivery, Reservas, Mensajería e IA, Cobros y pagos, Usuarios, Tablas maestras, Soporte) y buscador; reemplaza a las pantallas sueltas de configuración que había hasta ahora
- [Pagos] La ficha pública muestra los medios de pago online que el comercio tiene realmente conectados (antes decía siempre que no tenía ninguno); cualquier cobro online usa el saldo pendiente de la mesa en vez del total
Corregido
- [Salón] Crear un cliente nuevo desde la mesa rompía la consola con "Geolocalization is not defined" y dejaba una dirección vacía en el sidebar; al reemplazar el cliente de una mesa, el descuento y la dirección del cliente anterior quedaban pegados hasta refrescar
- [Salón] Un error del servidor al crear un cliente desde la adición inyectaba la página de error del admin en el POS y lo dejaba sin jQuery Mobile hasta recargar la página
- [Salón] Elegir una dirección de entrega ya cargada para el cliente no actualizaba el domicilio que se veía en la mesa hasta refrescar
- [Salón] "Borrar Cliente de Esta Mesa" dejaba el descuento y la dirección del cliente sacado pegados a la mesa, y el aviso de "Sincronizando…" quedaba trabado para siempre
- [Salón] Al borrar letras de una búsqueda de clientes en la adición, el listado se quedaba con los resultados de la búsqueda anterior más larga
- [Salón] Cambiar el descuento del cliente desde "Editar Cliente" no actualizaba el total de la mesa hasta refrescar
- [Salón] Arrastrar una mesa a otro mozo en el listado por mozo no la movía de verdad: volvía a aparecer bajo el mozo anterior en el próximo refresco
- [Salón] Una dirección de Google sin numeración (por ejemplo una ruta) podía romper la vista de la mesa
- [Clientes] El campo de fecha del alta rápida de cliente se veía distinto al resto del formulario
- [Clientes] Cargar "Depto/Casa/Lote" sin elegir una dirección de la lista de Google daba un error y dejaba el cliente creado sin asignar a ninguna mesa; ahora avisa el motivo y no queda nada a medio guardar
- [Clientes] El alta rápida de cliente desde el escáner de DNI o desde el calendario de reservas podía fallar sin mostrar el motivo (mail inválido, CUIT duplicado, etc.)
- [Configuración] Las pantallas de Impresoras y de Email guardaban el formulario completo sin distinguir qué campos son válidos, la pantalla de medios de cobro se veía sin pedir permiso, y las claves de Gemini/Resend quedaban visibles en el HTML de la página
- [RRHH] El tipo de empleador del F931 había quedado afuera de la Configuración unificada por error; vuelve a estar en Configuración › RRHH, con avisos si falta y el comercio factura electrónicamente
- [Pagos] Un pago de Mercado Pago rechazado sobre una mesa que ya estaba saldada podía mostrarse como aprobado al volver del pago
Cambios
- [Configuración] Se sacan de Configuración avanzada una docena de claves que ya no lee ningún módulo, y los campos de Delivery que estaban duplicados con su propia pantalla
- [Impresión] Para configurar la impresora de un dispositivo del salón, ahora se elige un perfil de impresión ya armado en vez de repetir los selects de impresora fiscal, remito, cajón y compras; el remapeo manual de una impresora física queda como opción avanzada
_Incluye lo de 3.11.5. Dos hotfijes seguidos, la misma noche, sobre el pool de bases
pre-creadas para el alta instantánea de comercio._
Corregido
- [Registro] Un alias de comercio que empezara con guion bajo podía generar una base con el mismo patrón de nombre que usa el pool interno de bases pre-creadas
- [Registro] El proceso que repone el pool de bases pre-creadas no cortaba al fallar la construcción de una base: reintentaba sin freno y podía saturar la base de datos con filas rotas en minutos
- Requiere
Risto.risto_schema update_genericantes de desplegar la imagen (tablas nuevastenant_poolytenant_provisioning_requestsen la base genérica). Sin las tablas el registro sigue funcionando por el camino de siempre. - Requiere el servicio
tenant-pool-crondel compose levantado en la misma ventana, conTENANT_POOL_SIZE(default 2) yPAXAPOS_APP_FULL_BASE_URL; después del deploy correr unInstall.TenantPool ticka mano y verificar que el pool quede en 2 bases listas.
Nuevo
- [Registro] Crear un comercio nuevo ya no hace esperar varios segundos: PaxaPos mantiene bases preparadas de antemano y el alta se completa en el momento. Si en ese instante no hay ninguna lista, el registro igual termina bien: el comercio se prepara en segundo plano, la pantalla de inicio avisa que se está armando y llega un mail "Tu comercio está listo" con el enlace para entrar (paxapos/paxapos#941)
Corregido
- [Compras/Proveedores] Al pedir la conexión con un proveedor, la pantalla terminaba en un "no encontrado" aunque la solicitud se hubiera enviado bien. Ahora te deja en la ficha de ese proveedor en tu catálogo, con el aviso de que la solicitud salió
- [Compras/Proveedores] Para elegir si el proveedor ya lo tenés cargado o hay que cargarlo nuevo había dos opciones sueltas: se podía escribir el nombre en una y quedar conectado al proveedor de la otra, sin darte cuenta. Ahora es una sola lista donde elegís el proveedor de tu catálogo o "Cargarlo como proveedor nuevo"
- [Pedidos de clientes] Un pedido rechazado se veía con el verde de "todo bien" y el detalle lo mostraba siempre en gris: ahora cada estado tiene un solo color en la bandeja y en el detalle (Nuevo, Confirmado, Entregado, Cobrado, Terminado, Cancelado por el cliente)
- [Pedidos de clientes] "Lo que compartimos" mostraba "Cliente #" con un código interno en vez del nombre: ahora muestra el nombre del cliente, su CUIT y el estado de la conexión
- [Compras] "Órden" pasa a "Orden" en las pantallas de órdenes de compra
- [Registro] Al crear un comercio nuevo desde el registro público, después de la espera aparecía un mensaje confuso y al entrar pedía iniciar sesión otra vez. Ahora el alta termina logueada en los primeros pasos del comercio, el botón no se puede apretar dos veces y la respuesta se interpreta bien aunque venga con texto de más (paxapos/paxapos#941)
- [Salón] En comercios que no son restaurante (panaderías, balnearios, municipios) el estado de la comanda, el tipo de entrega y el costo de envío no se veían en la vista de mesa aunque estuvieran configurados. Ahora se muestran para todos según la configuración, no según el rubro (paxapos/paxapos#938)
- [Mesas] El formulario de nueva mesa tenía dos campos "Ingreso" repetidos y el segundo pisaba al primero; queda uno solo junto a "Salida" (paxapos/paxapos#938)
- [Salón] Al asignar un cliente a la mesa, "Crear Cliente" abría una pantalla sin diseño fuera de la adición y "Agregar" terminaba en la pantalla de inicio. Ahora el alta se abre como ventana dentro de la adición y al guardar vuelve a la mesa; si faltan datos, el formulario queda abierto con lo cargado (paxapos/paxapos#948)
Cambios
- [Compras/Proveedores] Antes de conectarte con un proveedor tenés que marcar que leíste y aceptás el acuerdo entre comercios; el botón pasó a decir "Aceptar y enviar solicitud"
- [Pedidos de clientes] Las solicitudes de conexión ya no se aceptan desde un botón chico en la bandeja: ahora se entra a "Revisar solicitud", donde ves quién pide, su CUIT, qué se comparte y el texto del acuerdo, y aceptás con un botón grande después de marcar la conformidad. Rechazar quedó separado, abajo
- [Pedidos de clientes] El mail que avisa una solicitud nueva está escrito en el idioma del sistema, sin siglas técnicas
- [Compras] El detalle de una orden de compra a un proveedor conectado muestra el aviso "Orden vinculada" con el enlace a su ficha, y en el listado los proveedores conectados llevan la etiqueta "Conectado"
- [Planes y Módulos] El módulo se llama "Pedidos de clientes", sin siglas, y si tenés vínculos activos te pide confirmación antes de desactivarlo
- [Pedidos de clientes] La bandeja explica qué falta cuando no hay pedidos; en el detalle las cantidades son numéricas, la fecha de entrega usa el calendario del celular y cada renglón va en su propia tarjeta; botón "Volver" uniforme en todas las pantallas
- [Ficha pública] Un solo título con el nombre del comercio, datos en lista, botón "Iniciar sesión para conectar" cuando no hay sesión y la aclaración de que conectarse no comparte precios ni catálogo
- [Configuración] La sección Ficha pública muestra los dos prerrequisitos (CUIT y Pedidos de clientes) como una lista con tildes y la dirección pública de la ficha
- [Novedades] Las secciones se leen en castellano (Nuevo, Cambios, Corregido), sin notas técnicas ni referencias internas, y las versiones anteriores quedan plegadas
- [Configuración] La configuración avanzada pasa de tres columnas con setenta opciones mezcladas a paneles por tema (Salón y ventas, Remitos y comandas, Impresoras y facturación, Caja y arqueos, Compras, Apariencia, Delivery, Grilla de reservas, Notificaciones), con un índice arriba, el botón Guardar siempre a la vista y solo los paneles de los módulos que tenés activos. Son las mismas opciones con los mismos valores (paxapos/paxapos#943)
- [Salón] Las funciones ya no se muestran u ocultan según el rubro del comercio sino según sus módulos. Dos efectos visibles: la "Foto" y la "Apertura rápida" del mozo se ven solo con el módulo Salón activo, y el botón Checkout depende únicamente del permiso de ventas (paxapos/paxapos#938)
- [Hoteles] La grilla de reservas vieja se retira: la dirección anterior redirige a la grilla actual y los accesos directos instalados siguen funcionando (paxapos/paxapos#938)
- [Configuración] El campo "Valor del cubierto" se ve para cualquier tipo de comercio; dejalo en 0 si no cobrás cubierto (paxapos/paxapos#938)
Corregido
- [Novedades] En esta página, cada novedad que ocupaba más de una línea se cortaba: la primera línea quedaba como viñeta y el resto aparecía como un párrafo suelto debajo. Ahora cada novedad se lee entera
- [Compras/Proveedores] En la ficha pública, el botón "Conectar como..." para usuarios con varios comercios no abría nada al tocarlo. Ahora los comercios se muestran directamente como una lista de enlaces
Corregido
- [Compras/Proveedores] En la ficha pública de un proveedor, si tu usuario pertenece a varios comercios, el botón "Conectar" tomaba el primero de la lista y la solicitud salía a nombre de un comercio equivocado. Ahora, con más de un comercio, el botón deja elegir desde cuál conectarse y muestra el estado de cada uno
Cambios
- [Compras/Proveedores] El módulo "Pedidos de clientes (red B2B)" ahora se activa desde Planes y Módulos, como cualquier otro: ya no hace falta que soporte lo habilite por consola. Con el módulo activo, la ficha pública muestra el botón "Conectar" y el comercio recibe las solicitudes de conexión en su bandeja
- [Compras/Proveedores] Conectar con otro comercio, aceptar o rechazar una solicitud y volver a pedir la conexión ya no exigen ser administrador: alcanza con el permiso de operar la red B2B. El resto de los cambios sobre un vínculo sigue restringido como antes
- [Configuración] La sección "Ficha pública" muestra el CUIT cargado (o avisa que falta, con el enlace a Información Fiscal del Comercio) y si el módulo de red B2B está activo, que son los dos datos que hacen falta para que otro comercio pueda conectarse
- [Soporte] El alta de vínculos por consola ya no confunde dos proveedores con el mismo nombre de fantasía pero distinto CUIT: son empresas distintas y no bloquean el alta
Nuevo
- [Compras/Proveedores] Ficha pública del comercio. Cada comercio puede activar, desde Configuración, una página pública con sus datos (razón social, CUIT, dirección, rubro, descripción y foto; teléfono y horarios solo si elige mostrarlos), el mapa si tiene ubicación cargada o el aviso de que atiende online o a domicilio, y el enlace a su menú de pedidos online. Es opt-in: si no se activa, no existe. Desde Proveedores › ver, el comprador también ve la ficha pública de un proveedor conectado
- [Compras/Proveedores] Botón "Conectar". Desde la ficha pública de un proveedor que usa PaxaPos, otro comercio pide la conexión en dos pasos: el comprador elige (o crea) la ficha local del proveedor y acepta el acuerdo base; el proveedor recibe la solicitud en su bandeja y la acepta o la rechaza. Recién cuando los dos aceptan queda el vínculo activo. El botón muestra en qué punto está: Conectar, Solicitud enviada o Conectado
- [Novedades] Esta página:
/novedadesahora muestra el historial de versiones de PaxaPos tal como se publica en cada release, con un enlace fijo por versión - [Asistente/MCP] Herramientas nuevas para gestionar proveedores desde el asistente: buscar por nombre o CUIT, detectar posibles duplicados, unificar dos fichas duplicadas (con confirmación), ver los comprobantes y la deuda de un proveedor y aplicar en lote una lista de precios. Crear un proveedor ahora avisa si ya existe uno parecido antes de duplicarlo
- [Soporte] Comando
B2b.legal publicar / vigentepara publicar y consultar la versión vigente del acuerdo entre comercios
Corregido
- [Asistente/MCP] Cuando una operación del asistente fallaba con un motivo concreto —por ejemplo, unificar un proveedor que tiene un acuerdo entre comercios activo—, el mensaje que llegaba era el genérico "error 400". Ahora se ve el motivo real
Nuevo
- [Compras/Proveedores] El proveedor ahora ve, desde su propio acceso, los cobros y las retenciones que le practicó el comercio comprador, con un botón para bajar el certificado de retención sin depender de un enlace público suelto
- [Compras/Proveedores] Unificar dos fichas de proveedor duplicadas ahora también junta sus retenciones, su régimen de IIBB y sus rubros — antes esos datos quedaban huérfanos en la ficha absorbida y había que volver a cargarlos a mano. Si el proveedor que se va a absorber tiene un vínculo B2B activo con ese comercio, la unificación se frena en vez de dejarlo roto en silencio
Cambios
- [Compras/Proveedores] El estado de un documento del circuito de proveedores (una orden, una factura, un pago) puede quedar en "aplicado con advertencia" cuando se guardó bien pero hay algo para que una persona revise —por ejemplo, un CAE con un formato inusual—, en vez de mezclarse con los que salieron perfectos o con los que necesitan corregirse a mano
- [Comercios/Alta] Dar de alta un comercio nuevo (de cualquier rubro) volvía a fallar en el paso de crear los mozos por defecto. Ahora cada comercio nuevo nace con un centro de costo "General" para que ese paso no dependa de configurarlo a mano después
Corregido
- [Compras/Proveedores] Dos pantallas del circuito de proveedores daban error en vez de mostrar lo que correspondía: imprimir el remito de una entrega, y consultar el estado de un enlace público sin la extensión
.jsonen la dirección - [Compras/Proveedores] Cuando un proveedor respondía a una orden de compra por su enlace público muy rápido —antes de que el sistema terminara de procesarla del lado del comercio—, esa respuesta se perdía y la orden le quedaba mostrando "Nuevo" para siempre en su bandeja, sin que un reintento posterior la arreglara. Ahora la respuesta espera lo que haga falta y siempre queda reflejada
- [Salón/Hoteles] Un comercio hotelero no podía llegar a la lista de mozos si no tenía instalado el módulo de Salón, que no le hace falta para ese rubro
- Requiere
Risto.risto_schema update_generic(tablas nuevaspxp_vinculos,pxp_doc_linksypxp_accesosen la base genérica) yupdate_tenants(columnamesas.origen, para distinguir una mesa nacida de una venta común de una nacida de un pedido a un proveedor). - Después de migrar corresponde un backfill de
mesas.origenen los tenants con historial, para que las mesas viejas queden clasificadas igual que las nuevas.
Nuevo
- [Compras/Proveedores] Un comercio puede vincularse con otro comercio que le vende mercadería, quedando un acuerdo activo entre los dos. Con el vínculo armado:
- El proveedor recibe la orden de compra en una bandeja propia, en modo consulta (sin acceso al resto del sistema del comprador), donde puede ver el pedido y aceptarlo o rechazarlo por renglón.
- Cada orden tiene un enlace público para que el proveedor la responda aunque todavía no tenga su acceso configurado.
- El proveedor puede confirmar la entrega, y de ahí en más el comercio comprador ve la factura, registra el pago y el proveedor puede bajar su certificado de retención, todo dentro del mismo circuito — sin planillas sueltas ni ida y vuelta por WhatsApp para cada paso.
- Un panel
/admin/b2b, para el equipo de soporte de PaxaPos, con el estado de toda la red de acuerdos y sus documentos. - Un proceso interno (
B2b.sync) revisa cada 5 minutos si hay algo nuevo para emitir o aplicar de cada lado del acuerdo, así que ninguna de las dos partes tiene que hacer nada para que la información llegue.
Corregido
- [Configuración] Un comercio podía ver, durante un rato, el nombre y la configuración de otro comercio en vez de la propia — sin que la base de datos de ninguno de los dos estuviera tocada: era la config guardada en caché bajo el comercio equivocado. Pasaba al consultar la configuración de varios comercios en la misma corrida (por ejemplo, el listado de comercios de un usuario). Ahora cada lectura de configuración queda atada al comercio que corresponde antes de leerla
Corregido
- [Compras/Seguridad] El enlace público de una orden de compra —el que se le manda a un proveedor para que suba su factura o responda un pedido, sin necesidad de una cuenta— servía igual en cualquier comercio: cambiando el nombre del comercio en la misma dirección, el enlace abría la orden del mismo número de otro comercio, y ese acceso permitía subir un comprobante que quedaba cargado ahí. Ahora el enlace queda atado al comercio que lo generó. Los enlaces ya enviados antes de este cambio siguen funcionando por 30 días más, para no cortarle el acceso a nadie de un día para el otro
- [Seguridad] El listado de productos para pedidos online (usado por el menú público y por integraciones) exponía la receta completa de cada producto —qué ingredientes lleva y en qué cantidad— sin necesidad de sesión. Ahora esa información solo viaja para quien está logueado
- [Configuración] Cargar la configuración de más de un comercio en el mismo proceso —por ejemplo, al listar los comercios de un usuario— podía dejar mezclado el estado de uno con el del siguiente: la config de un comercio quedaba pegada tras consultar otro, y un comprobante fiscal podía emitirse con el certificado equivocado. Ahora cada comercio se procesa de forma aislada, sin dejar residuos para el que sigue
Corregido
- [Salón/Cajero] Cuatro callejones sin salida en el salón: desde el mapa de mesas no había forma de abrir Opciones, ni el Modo Cajero ni Impresoras, porque el mapa tapaba el encabezado y se comía los clics; el botón de menú del Modo Cajero en la pantalla de agregar pedido no abría nada; quedaban pantallas y configuración sueltas de una función de delivery ya retirada, sin uso; y el cuadro con las opciones de impresora fiscal del diálogo de cajero nunca llegaba a mostrarse. Ahora las cuatro cosas funcionan
- [Salón] El salón mostraba dos botones de menú superpuestos arriba a la izquierda, y con esa duplicación el Modo Cajero dejaba de abrir. La causa era que, al navegar dentro del salón, la pantalla volvía a traerse a sí misma entera y quedaba pegada dos veces sobre la misma ventana. Ahora el salón navega entre sus secciones sin volver a traerse a sí mismo, así que queda un solo botón de menú y el Modo Cajero vuelve a abrir
- [Producto/Buscador] El buscador con sugerencias para elegir un producto existente —al cargar un producto al menú o en Mercaderías— no devolvía ninguna sugerencia al escribir, aunque el producto ya existiera en el catálogo, sin ningún error en pantalla. Al no poder elegirlo de la lista, el alta lo interpretaba como un producto nuevo y lo creaba de nuevo en el catálogo general del comercio. Ahora el buscador vuelve a sugerir los productos existentes, evita elegir uno que ya está agregado al menú y deja de ensuciar el catálogo
- [Adición/Reservas] Un mozo que intentaba recepcionar una reserva desde el salón era mandado directo al login, sin ningún mensaje: a ese rol le faltaba el permiso para esa acción puntual, aunque el botón se mostraba igual sin fijarse si correspondía. Además, cualquier rechazo por falta de permiso en el salón se veía igual que una sesión vencida, así que el operador no tenía forma de distinguir un permiso faltante de tener que volver a loguearse. Ahora el rol de mozo puede recepcionar reservas si ya tiene permiso para abrir mesas, el botón solo se muestra a quien puede usarlo, y el aviso de permiso faltante se distingue del de sesión vencida y explica el motivo real (issue #355)
- [Formularios/Ventanas emergentes] Cualquier formulario abierto en una ventana emergente —por ejemplo, para cargar un producto o una mercadería sin salir de la pantalla— perdía sus controles especiales: el editor de texto aparecía como una caja de texto pelada, los desplegables con muchas opciones salían sin el buscador que los hace usables, y el buscador con sugerencias no sugería nada. No había ningún error visible, así que parecía que esas funciones nunca hubieran estado ahí. Ahora todos los controles se cargan bien dentro de las ventanas emergentes
- [Configuración/Terminología] Los nombres que cada comercio elige para sus secciones —por ejemplo, llamarle "Insumos" a la Mercadería— se guardaban tal cual se tipeaban y se mostraban sin filtrar en 38 pantallas distintas. Ahora ese texto se limpia al guardarlo, con una capa extra de verificación que protege todas las pantallas donde se usa
- [Formularios/Editor de texto] Con internet lenta, el editor de texto de una ventana emergente podía no llegar a aparecer y el operador se quedaba con una caja de texto pelada, sin ningún aviso: el editor se traía desde un servicio externo y, si tardaba más de cinco segundos en responder, se daba por vencido. Ahora se sirve desde el propio servidor de Paxapos, aparece siempre al instante y deja de depender de la velocidad de internet del local
- [Formularios/Fechas] Varios formularios con campos de hora tenían fallas puntuales heredadas del cambio a los selectores de fecha nativos del navegador: un campo declarado como "solo hora" en algunas pantallas de AFIP y Pagos aparecía con selector de fecha y hora juntos, y al guardar la hora sola el sistema la rechazaba. Cargar una fecha sin hora —un vencimiento, por ejemplo— podía traer pegada la hora exacta del momento en que se abrió la pantalla, así que el mismo reporte daba un resultado distinto según la hora del día en que se lo mirara. Y en unos 79 campos de fecha de toda la app, hacer clic en la etiqueta del campo no enfocaba el campo. Ahora los tres casos quedan corregidos
- [Formularios/Checkboxes] Un casillero sin una etiqueta indicada a mano quedaba completamente mudo, sin ninguna palabra al lado: pasaba, entre otras pantallas, con dos casilleros de Importar Productos —uno de ellos borra del menú todo lo que no esté en el archivo importado— y con el de habilitar un punto de venta AFIP para facturar. Por otro lado, el casillero que el sistema arma solo a partir de una columna sí/no de la base mostraba su etiqueta dos veces. Ahora cada casillero muestra su etiqueta una sola vez, y nunca queda sin ninguna
- [Formularios/Filtros con sugerencias] Al filtrar un listado por proveedor con el buscador con sugerencias, una vez aplicado el filtro no había forma de sacarlo desde la pantalla: el casillero de texto volvía vacío aunque el filtro siguiera activo, así que había que editar la dirección web a mano para quitarlo. En las pantallas donde ese buscador se repite, el campo visible y el campo oculto que en realidad filtra podían compartir el mismo identificador interno, con lo cual el buscador se enganchaba al campo invisible y nunca mostraba sugerencias. El texto de ayuda de algunos de estos buscadores además aparecía como un atributo inválido en el HTML en vez de mostrarse como corresponde, y algunas hojas de estilo se cargaban duplicadas. Ahora el filtro aplicado se puede borrar desde la pantalla, cada buscador engancha al campo correcto y el texto de ayuda se ve bien
- [Estadísticas/Materias Primas] Las columnas consumido y consumido por día salían con el número pelado, sin decir en qué unidad: la pantalla leía la unidad de stock en un lugar donde el listado nunca la dejaba. Ahora cada consumo se muestra con su unidad (kilos, litros, cajas), tanto en la pantalla como en la exportación a Excel
- [Stock/Mercaderías] En el listado de Stock de Mercaderías la columna U/M de Compra aparecía vacía en todas las filas, en cualquier comercio: el listado traía la unidad de stock pero nunca la de compra, así que la pantalla mostraba un dato que no se cargaba — justo el que aclara en qué unidad se le compra al proveedor (botella, cajón, litro). Ahora se muestra. Alcanza también al detalle de un stock y a las pantallas de movimientos
- [Producto/Ficha] En la ficha de un producto, el resumen de compras —*"Se compran 4,5 (± 1,2) Kilos cada 7 días"*— mostraba siempre un guión: la pantalla buscaba la unidad de stock en un lugar donde no estaba, así que el resumen no llegaba a armarse nunca. Ahora se muestra para los productos que tengan configurada su unidad de stock
> Esta es la primera imagen publicada desde la v3.9.6: la v3.9.7 se cortó y etiquetó pero
> nunca llegó a construirse, así que todo lo listado en 3.9.7 viaja también en esta versión.
Corregido
- [AFIP/Cierres] Exportar un cierre a Excel devolvía una pantalla de error, para cualquier cierre y cualquier comercio. El botón de descarga arma la dirección según el módulo desde el que se lo abre, y la variante del módulo AFIP nunca había tenido su plantilla de Excel. Ahora exporta con el mismo contenido que desde el módulo de cuentas (#831, issue core#259)
- [Adición/Sin conexión] Un cambio en espera se descartaba cuando el servidor contestaba con un error pasajero —un reinicio en curso, una demora, un pico de carga—, tratándolo igual que a un rechazo definitivo. Ahora solo se descarta lo que el servidor rechaza de verdad; lo pasajero se reintenta en el envío siguiente. Cobros y propinas siguen sin descartarse nunca, y además avisan en pantalla cuando llevan más de un día sin poder confirmarse, en vez de reintentar en silencio (#832, issue #803)
Cambios
- [Menú/Exportación] La exportación del menú en formato JSON ya no responde sin sesión. La importación de menú desde otro comercio sigue disponible, pero ahora pide una clave que el comercio de origen configura para sí mismo (Configuración → categoría «Product») y que se carga en el formulario de importación; si falta, el mensaje explica exactamente qué pedir. Incluir las recetas en la exportación pasa a requerir, además, el permiso de lectura de stock — el mismo que ya gobierna las recetas en el resto del sistema (#833, issue core#257)
- [Permisos] El control de escritura acepta marcar un recurso como estricto: en esos recursos, una acción que no esté declarada en el catálogo se deniega y queda registrada, en lugar de resolverse con cualquier otro permiso de escritura que la persona tenga sobre ese mismo recurso. Es opcional y ningún recurso lo usa todavía, así que no cambia nada del comportamiento actual; queda disponible para los recursos que nazcan con el mapeo completo (#834, issue core#258)
Corregido
- [Adición/Clientes] Dar de alta o editar un cliente desde el salón podía terminar sin ningún mensaje: si un dato no pasaba una validación (CUIT, mail, un campo obligatorio), el diálogo se cerraba como si hubiera guardado y el operador no tenía forma de saber qué corregir. Lo mismo ocurría en descuento, comensales, mozo, tipo de entrega, propina, cobro manual, dirección de entrega y mesas del plano de salón: varias de esas acciones contestaban “ok” y el cambio quedaba marcado como sincronizado sin haberse guardado. Ahora toda alta o edición del POS que el servidor rechaza devuelve el motivo concreto, se muestra en pantalla, queda registrado en el log y el diálogo se cierra recién cuando el guardado entró de verdad
- [Adición/Facturación] Emitir una nota de crédito electrónica o reimprimir un comprobante no daba ninguna señal: el cajero confirmaba y después no veía ni el CAE ni el motivo del rechazo. Ahora espera con indicador de carga y muestra el comprobante con su CAE al emitir, o el motivo real cuando no se pudo (#829, issue #823)
- [Adición/CRM] La nota que se escribe al cerrar la mesa podía no llegar sin avisar. Sigue sin bloquear el cierre, pero ahora: sin conexión queda encolada y se envía sola como cualquier otra escritura del POS, y si el servidor la rechaza el texto vuelve al panel con el motivo, en vez de desaparecer (#829, issue #824)
- [Comandas] Bajar la cantidad o anular un renglón podía quedar aplicado en pantalla aunque el servidor no lo aceptara. Ahora el rechazo se avisa y la baja se revierte respetando cualquier otro cambio que se haya hecho sobre ese mismo renglón mientras tanto (#829, issue #825)
- [Adición] Escrituras menores que fallaban en silencio ahora avisan: distancia de envío, mover una mesa del plano, desvincular una mesa del salón, limpiar el caché del menú y asociar un cliente a la mesa. Además se retiró un pedido al servidor que ya no existía y fallaba en cada liberación de mesa sin que nadie lo viera (#829, issues #826, #827)
- [Salón/Permisos] Los permisos granulares de mesa quedaban inertes en el camino de reintento: ese camino manda el modelo entero de la pantalla, no solo lo que cambió, y al no reconocer esos campos el control caía siempre al permiso amplio de edición. Ahora el permiso se decide sobre lo que el pedido modifica de verdad —descartando lo que no es columna guardable y lo que viene con el valor que la fila ya tenía— y nunca se exige más permiso que antes: si acotar no alcanza, se vuelve a evaluar como se hacía hasta ahora. Renumerar una mesa de
01a1sigue contando como cambio real (#828, issue #821)
- [API] Los campos de identidad que son numéricos por casualidad —CUIT, DNI, teléfono, CBU, código de barras, CAE, código postal— y cualquier valor con ceros a la izquierda viajaban en las respuestas
.jsonconvertidos a número, con lo cual el cero inicial se caía y los códigos muy largos perdían precisión. Como el tipo dependía del contenido del dato, el problema parecía intermitente: un proveedor con CUIT con guiones funcionaba y el de al lado no. Ahora la conversión solo se hace cuando el valor sobrevive intacto, y esos campos viajan como texto. Estados, booleanos e importes llegan con el mismo tipo de siempre (#830, issue #810 — primer paso de dos; la remoción completa del flag va aparte)
Cambios
- [Salón] Al guardar una mesa, los importes (
totalysubtotal) y los campos de control (identificador público, fechas de creación y modificación, marca de borrado) dejan de tomarse de lo que manda la pantalla: los calcula el servidor. La pantalla los incluía en todos los guardados, así que con un modelo desactualizado el guardado de cualquier otro campo podía arrastrar un importe viejo
- [Diagnóstico] El log de rechazos por permiso en mesa ahora nombra los campos que cambian y distingue el caso de un reintento sobre una mesa que ya está igual, que no tiene a nadie esperando del otro lado
Nuevo
- [Fichaje] Endpoint para subir fichajes de a lotes, en vez de uno por pedido: la app puede vaciar de una sola vez lo que juntó sin conexión
Corregido
- [Facturación] Cuando la emisión fiscal desde la mesa fallaba, no quedaba ningún rastro de qué había pasado. Ahora el error se registra con su motivo, que es lo que permite diagnosticar el caso después
Corregido
- [Salón/Permisos] Los permisos de cerrar y anular mesa no servían desde el diálogo de cambio de estado: ofrecía todos los estados sin mirar quién era, y al guardar exigía el permiso amplio de edición, así que a un encargado con permiso para cerrar mesas pero sin el amplio le fallaban todas las opciones. Ahora cada estado pide el permiso que le corresponde, y el diálogo muestra sólo lo que esa persona puede hacer. Reabrir una mesa o marcarla como cobrada siguen requiriendo el permiso amplio, igual que antes: nadie gana acceso a un estado nuevo (#806, issue #804)
- [Adición] Un cambio hecho sin conexión que el servidor rechaza por permisos quedaba reintentándose indefinidamente: se encontraron cambios de hace diez días todavía en la cola de tres equipos. Ahora, si un rechazo de ese tipo persiste más de un día, el cambio sale de la cola en vez de quedar dando vueltas. Los pagos y las propinas nunca se descartan, y las mesas creadas sin conexión quedan marcadas con error en pantalla en vez de desaparecer (#806, issue #803)
- [Estadísticas] El resumen podía pedirse con un rango de años y tardaba más de lo que el navegador espera, así que no devolvía nada. Ahora la ventana se limita a dos años: se conserva la parte más reciente y la respuesta avisa que se acortó (#806, issue #801)
Corregido
- [Salón/Permisos] Dos controles de la pantalla de mesa le fallaban a cualquier rol que no tuviera el permiso amplio de edición, aunque tuviera los permisos finos que la propia pantalla usa para mostrarlos. El botón de costo de envío aparecía en toda mesa de delivery sin mirar ningún permiso, y el de catálogo se mostraba con el permiso de ver el catálogo, que es de lectura, mientras que guardar exigía el de edición. En los dos casos el mozo veía el botón, lo usaba, y el cambio no se guardaba. Ahora el costo de envío queda cubierto por el permiso de tipo de entrega —es parte del mismo flujo: quien decide que la mesa es delivery es quien le pone precio al envío— y el de catálogo sólo se ofrece a quien además puede guardarlo. Ningún rol pierde acceso a algo que hoy le funcione (#805, issue #802)
Cambios
- [Diagnóstico] Cuando un cambio de mesa se rechaza por permisos, el log ahora nombra el campo concreto que lo provocó. Antes sólo decía qué permiso se había exigido, que para este endpoint es siempre el mismo, así que averiguar la causa real obligaba a reconstruir a mano qué había mandado la pantalla
Corregido
- [Compras] Corrección de lo que entró en v3.9.2: la equivalencia de unidades se aplicaba siempre, sin mirar si de verdad había habido una conversión. Una mercadería que se compra y se stockea en la MISMA unidad pero arrastra una equivalencia distinta de 1 en el catálogo —residuo típico de una limpieza a medias— quedaba leída al revés: pedir 1 kg y recibir 9 kg es un excedente de nueve veces, y se mostraba como "recepción completa". **Antes de v3.9.2 ese caso decía "excedida", que era lo correcto**, así que en esas líneas la versión anterior empeoró la lectura en vez de mejorarla. Ahora la equivalencia se aplica sólo cuando la línea se recibió en una unidad distinta de la de compra, y la conciliación informa además
factor_conversional lado destock_cant_x_um, para que se vea cuál se aplicó y cuál es la configuración. La pantalla de la OC pasa al mismo cálculo: comparaba los nombres de las unidades, así que podía volver a discrepar de la API. No corrompe datos: lo que estaba mal es el estado que se muestra (#800, issue #799)
Nuevo
- [Reportes/API] Nuevo
GET /comanda/detalle_comandas/productos_vendidos_api.json: el ranking de productos vendidos del período, con cantidad e importe por producto y los totales. Hasta ahora esto se servía por MCP apuntando a la PANTALLA del reporte, que además del ranking manda el catálogo completo de productos, categorías, tags, mozos y los productos NO vendidos, con columnas internas de la mesa pegadas a cada renglón: una consulta de 15 días en un comercio real devolvía 276 KB y reventaba el límite de tokens del cliente, y ellimitedocumentado en la tool no se aplicaba en ningún lado. El endpoint nuevo tiene tope real (default 10, máximo 200) y no hace elgetCosto()por producto que hace la pantalla. Mantiene el merge de variantes (un producto vendido como sabor de otro suma al ranking) y reordena después de ese merge, porque si no el "top 10" podía dejar afuera un producto que vende más que los que lista (#794, issue #793)
Corregido
- [Compras] La conciliación de una OC daba "sin facturar" una orden que estaba facturada entera. Son dos cruces distintos y el endpoint sólo exponía uno: el cruce por línea únicamente ve la plata que está en un renglón de factura vinculado a la línea, así que una factura cargada sin renglones queda vinculada a la OC pero aporta cero ahí. La pantalla decía "totalmente facturada" y la API decía lo contrario sobre la misma orden. Ahora el resumen trae además
por_total—el mismo veredicto que usa la pantalla y el tablero de flujo— y el contadorfacturas_sin_detalle, que explica por qué el cruce por línea no puede cerrar (#792, issue #791) - [Compras] La recepción de una OC se comparaba en cajas contra kilos. La cantidad pedida va en unidad de compra y la recibida en unidad de stock, así que cualquier mercadería con equivalencia mayor a 1 quedaba marcada como "excedida" aunque hubiera llegado exactamente lo pedido: un café que viene 1 caja = 5 kg se pidió 1 y se recibió 5. Ahora se compara contra
cantidad × stock_cant_x_um, la misma cuenta que ya hacía el desvío de recepción, y la línea informarecibido_esperadopara que el número se pueda leer sin adivinar (#796, issue #795) - [Compras/Rendimiento] El listado de mercaderías tardaba 60 segundos y el cliente cortaba la conexión: la integración del portal de proveedores expiraba siempre. La acción resolvía el precio unitario y los pendientes fila por fila, y cada precio son dos queries — con el catálogo más grande de la flota (6.009 ítems, y la extensión
.jsondesactiva la paginación) eso daba unas 18.000 consultas en una sola petición. Ahora son dos consultas para todo el listado, con la misma respuesta de antes. Verificado contra la base de un tenant real: 400 mercaderías, cero diferencias contra el cálculo viejo, 48 veces más rápido (issue #787) - [Facturas] El tipo de comprobante salía por la API con las comillas adentro del valor (
"tipo": "\"A\""). El catálogo es libre por tenant y en la flota hay varios cargados así; ahora se limpia sólo la comilla que envuelve el nombre entero, dejando intacta cualquier comilla suelta o interna, que es parte del nombre que eligió el comercio - [Migraciones]
Risto.risto_tenantinvalidaba el caché de schema con una llamada que no hacía nada: corría con el caché deshabilitado, que es la condición exacta en la que ese clear es un no-op silencioso, y además lo hacía antes de aplicar los cambios, lo que es una carrera perdida contra los contenedores que están sirviendo. Es el mismo par de bugs que ya se había corregido en el otro shell, y era la causa de que una columna nueva quedara invisible para el código durante días. El helper pasa a un lugar compartido, para que no se vuelvan a separar (issue #788) - [Tests] Un test del throttle de permisos fallaba según el estado del árbol de trabajo y no del código: limpiaba su precondición con un borrado que sólo funciona en directorios vacíos, y en cualquier entorno donde la app ya había corrido quedaba rojo para siempre (issue #790)
Relacionado (fuera de este repo)
- nestjs — la tool de conciliación de OC ahora explica cuál de los dos cruces vale y cuándo (paxapos/nestjs#120); las tres tools de productos más vendidos apuntan al endpoint nuevo en vez de a la pantalla, con el
limitefuncionando de verdad (paxapos/nestjs#121); y la suite de tests deja de colgarse sin terminar (paxapos/nestjs#119)
Nuevo
- [Finanzas/Compras] La nota de crédito / débito de proveedor ahora se puede cargar desde el form de gasto, no solo por API: al elegir un tipo NC o ND aparece el selector "Factura que ajusta esta nota" con las facturas vivas de ese proveedor (comprobante, fecha, importe y saldo pendiente), servidas por el endpoint nuevo
GET /account/gastos/facturas_proveedor_api/{proveedor_id}.json. La regla de validación es exactamente la misma que aplica la API (Gasto::motivoRechazoAjuste(): factura viva, del mismo proveedor, que no sea otra nota, que no se ajuste a sí misma), y el importe se guarda con el signo de la nota aunque el usuario lo tipee en positivo (#786, issue #773) - Cuatro correcciones encontradas revisando ese PR antes de mergearlo: (1) el endpoint nuevo caía en el gate genérico de Finanzas, así que un usuario de Compras —el perfil al que la carga de NC está habilitada a propósito— recibía 403 justo en el paso previo a cargarla; (2) el
<select>se vaciaba al ocultarse, y un select vacío igual se postea: editar una nota cuyo tipo no está en el catálogo de NC/ND (las notas sobre comprobantes no fiscales quedan sintipo_factura_ida propósito) le borraba el vínculo con su factura y cambiaba el saldo de esa factura sin que nadie lo pidiera — ahora el campo nace deshabilitado y el JS lo habilita recién cuando cargó las facturas; (3) el signo solo se forzaba si había factura vinculada, así que una NC "suelta" en positivo se rechazaba con el mensaje inverso al problema real; (4) el''del select vacío MySQL 5.7 lo guarda como0, y un0no matchea elIS NULLcon el que se decide qué facturas se pueden ajustar
Corregido
- [Finanzas/API] El listado de gastos de la API (
/account/gastos/listar.json, que es lo que lee la toolinvoice_get_gastos) devolvía **número, importe y saldo ennullpara todos los gastos**: leíaGasto.numero,Gasto.importeyGasto.saldo, tres columnas que no existen. Ahora arma el comprobante conpunto_de_venta-factura_nro, el importe conimporte_totaly el saldo contra lo realmente imputado enaccount_egresos_gastos(una sola query para todo el listado). Además el filtropendientestiraba SQL error por la misma razón y ahora usa el mismo criterio de "en deuda" que el resto del sistema - [Compras] El alta y la edición de mercaderías tenían **dos implementaciones paralelas** —una en el form y otra en la API— que se habían ido separando: la del form creaba el producto y seguía adelante aunque ese save fallara, y guardaba cualquier campo de
Productoque viniera posteado. Ahora las dos pasan porMercaderia::normalizarAlta()+crearConProducto(), con whitelist de campos y en una transacción (#783, issue #777) - Corrección encontrada en la revisión: los dos pasos lanzan
InvalidArgumentExceptionpero significan cosas opuestas (falta un dato del form vs. regla de negocio), y compartían el mismocatch: un alta sin nombre sobre un producto existente caía en la rama de "salir" y le borraba al usuario todo lo que había tipeado - [Permisos/Monitoreo] La denegación a un usuario autenticado se logueaba **siempre como Error**, incluido el caso en que el rol simplemente no tiene esa acción porque así se lo configuró. Ese ruido tapa señales reales: durante el monitoreo del deploy de v3.9.0, los 4 "errores" de estable en 2 h eran todos el mismo mozo sin
Ventas.salon:editar_mesa. Ahora se distingue por si la acción la tiene **algún otro rol del tenant**: si la tiene, es configuración deliberada y va awarning; si no la tiene nadie, sigue siendoerror— ese es el caso accionable, el síntoma de una acción nueva que quedó sin backfillear y está denegada en silencio para todos. El motivo va en el propio mensaje de log. El write-gate no se toca - [Finanzas/Compras] Cuando el snapshot de schema cacheado de un tenant quedaba viejo (pasó en
taco_time,uejn_nautico_villa_gesellyuejn_congresodespués de v3.9.0),save()descartabagasto_ajustado_iden silencio y el guard de #781 abortaba: en esos tenants no se podía cargar ninguna nota de crédito hasta que alguien entrara al container a correrRisto.auditoria_cache_schema --fix. Ahora, detectado el drift, la nota se repara en la misma transacción (la columna existe en la base; lo viejo es la descripción cacheada) y queda unwarningcon el tenant y el comando de remediación, para que el problema de fondo se vea en el monitoreo en vez de aparecer como una nota que "no anda"
Relacionado (fuera de este repo)
- nestjs — tool MCP
invoice_get_facturas_proveedorsobre el endpoint nuevo, que es el paso previo aregistrar_nota_credito_proveedor: permite elegir a qué factura vincular la nota en vez de adivinar elgasto_id(paxapos/nestjs#118)
- Requiere
Risto.risto_schema update_genericyupdate_tenantspor la columna nuevaaccount_gastos.gasto_ajustado_id+ índice. Es unADD COLUMNONLINE/ INPLACE en InnoDB — medido contra el tamaño real de la flota: 116.588 filas y 68 MB en total, segundos por tenant. Se migran los 97 tenants (73 activos + 24 dormidos). - ⚠️ Responder
yminúscula al prompt del shell: conYmayúscula el shell dice "Tenants actualizados OK" sin aplicar ningún ALTER. - ⚠️ Invalidar el caché
_cake_model_(Redis) después de migrar — si no,save()descartagasto_ajustado_iden silencio y la nota de crédito queda sin guardar el vínculo a la factura.
Después de migrar, correr estos backfills por tenant y con --dry-run primero:
Account.gasto_items_backfill— copia aaccount_gasto_itemssolo las líneas que el OCR había escrito desde la factura antes de esta entrega.Users.backfill_cambiar_medio_pago(PR #739) — backfillearoles_permissionspara la acción nuevaVentas.cajero:cambiar_medio_pago; sin esto queda denegada en silencio en los tenants con overrides de rol.Users.backfill_editar_receta— ídem para la acción nuevaOperaciones.stock:editar_receta.
Nuevo
- [Payments/Medios de Cobro] Nueva acción para corregir el medio de un cobro manual cargado a mano por error (ej. "Efectivo" cuando el cliente pagó con Visa), reetiquetable mientras la caja siga abierta desde el detalle del cobro, el listado y la tabla de cobros del arqueo. Regla pura
Payments.CambioMedioDeCobro(aplicada enPaymentsTransaction::beforeSave, mismo patrón queCash.ReasignacionArqueo#642): solo cobros manuales sin id externo (un cobro conciliado por MercadoPago/Payway no se toca), solo con el arqueo abierto, el destino también tiene que ser un medio manual y activo del catálogo (si no, se repite el cobro selladoMPAGO/QRque arregló #614), y en moneda extranjera solo efectivo. Nunca cambia monto, moneda, mesa ni estado — reetiqueta, no reescribe. Permiso nuevoVentas.cajero:cambiar_medio_pago. Cada corrección queda auditada (quién, cuándo, de qué medio a qué medio) enaudits, visible en el detalle del cobro. Bug encontrado en el camino:Form->createinfería el controller comopayments_transactiones(inflector en español) y daba 404 — afectaba también aasignar_arqueode #642, que tampoco podía guardar; corregido con el controller explícito en ambas vistas - ⚠️ Requiere backfill de permisos:
Users.backfill_cambiar_medio_pago— sumar una acción nueva a un recurso existente no alcanza, porque donde el rol ya tiene filas enroles_permissionslos defaults del PHP dejan de aplicar y la acción queda denegada en silencio. Idempotente - [Compras/API] Nuevo
GET /compras/pedidos/conciliacion_api/{id}.json: 3-way match pedido/recibido/facturado por línea de la OC (estadossin_facturar/sub_facturado/ok/sobre_facturado), más resumen, facturas vinculadas y renglones de factura sin conciliar. Es el mismo cruce dePedido::conciliarLineas()que ya usaPedidos/view.ctp, expuesto como dato para el bot. PermisoOperaciones.compras:read+ filtro por centro de costo, igual queview(). Bug encontrado en el review:PedidoMercaderia::facturadoPorLinea()solo cruzaba líneas vivas de la OC, así que la plata facturada contra una línea ya soft-deleteada desaparecía del resumen sin dejar rastro —no sumaba atotal_facturado_conciliadoni aparecía enitems_sin_conciliar—. Se extraen dos funciones puras (Pedido::indiceLineasDeOc(),Pedido::clasificarItemsFueraDeLinea()) y el resumen suma ahora el bucketitems_linea_borrada/total_items_linea_borrada, para marcarlo como pendiente de revisión humana en vez de perderlo (paxapos#778, issue #775) - [Account/API] Pagos a proveedor por API JSON: crear (
POST /account/egresos/save.json), listar pendientes, aprobar, rechazar, ejecutar — reusanEgreso::totalesDesdeGastos()yafterSave(), sin duplicar la lógica del form. PermisoFinanzas.pagos+ chequeo de caja iniciada (Site.nuevo_arqueo), igual que las pantallas. Tres hallazgos del review, el primero de la misma familia que el pago fantasma de sep-2026: (a)__esApi()solo mirabaRequestHandler->prefers('json')(Accept/.json), pese a que el docblock decía que también cubría Content-Type — unPOSTconContent-Type: application/jsonsin extensión ni Accept caía al camino del form, dondeModel::set()reinterpretaba el JSON plano como campos sueltos deEgresoy podía guardar un pago PAGADO, con monto real, sin ningúnEgresoGastovinculado; fix en dos capas:__esApi()ahora también resuelve por Content-Type ysave()rechaza con 400 cualquier payload sin la forma mínima del form; (b)aprobar()/rechazar()/ejecutarPago()y el alta eran check-then-act sin candado — dos requests simultáneas podían ejecutar la misma transición dos veces o pagar el mismo saldo dos veces; se descartóupdateAll()porque saltea los callbacks de los que dependeejecutarPago()para estampar el arqueo abierto, así que toman el mismoGET_LOCK(named lock de MySQL) que ya usaPaymentApprovalService; (c)totalno numérico se casteaba en silencio con(float)— ahora 400 explícito. 35 tests nuevos sin DB (paxapos#779, issue #774) - [Stock/API] Crear/editar la receta de un producto por API JSON (
GET /stock/recetas/ver_api/{id}.json,POST .../guardar_api/{id}.json), misma lógica que el form (Receta::guardarConIngredientes()). PermisoOperaciones.stock:readpara la lectura. Dos hallazgos bloqueantes del review: (a) los ingredientes sacados del payload se borraban condeleteAll()sincallbacks, salteandoRistoSoftDeleteBehaviorcon unDELETE FROMcrudo —mismo patrón que #713/#714/#715, arreglado igual que en #761—; ahora borra concallbacks=truey verifica contra el estado real confind('count'), porquedeleteAll()con callbacks devuelvefalseaunque el soft-delete haya salido bien; (b) la escritura estaba gateada con el permiso de lectura de stock — cualquier usuario con acceso de solo lectura podía borrar ingredientes de cualquier receta. Se agrega la acciónOperaciones.stock:editar_receta(default: encargado para arriba) + shell de backfillUsers.backfill_editar_receta. El guardado quedó transaccional ycant <= 0se rechaza con 400 (paxapos#780, issue #776) - [Account/Compras] Nota de crédito / débito de proveedor contra una factura: no existía la NC de compra, la única era la de venta por ARCA. Ahora es un
Gastocon la columna nuevaaccount_gastos.gasto_ajustado_idapuntando a la factura que ajusta, con sus propios renglones (GastoItem, #595) y guardado con importes negativos (la ND, positivos) — misma convención que ya usan_cambiarSignoSegunTipoFacturayEgreso::afterSave(), así que la deuda del proveedor y la imputación de pagos netean sin código nuevo.POST /account/gastos/nota_credito_api.jsonhereda proveedor, clasificación, centro de costo, OC, moneda y punto de venta de la factura; permisoOperaciones.compras:cargar_facturaoFinanzas.contabilidad:cargar_gasto. Cuatro hallazgos del review: (a) el tope se calculaba contra el total original de la factura en vez del saldo restante — dos NC parciales consecutivas podían sumar más del 100% (ej. NC de 700 + NC de 600 sobre una factura de 1000) y dejar la deuda del proveedor negativa sin justificación; ahora el tope esfactura - NC previas(la ND no resta saldo); (b) el tipo de NC se resolvía parseando el nombre del tipo de factura y caía en silencio al primero del catálogo si no encontraba la letra —guardando la letra fiscal incorrecta, dato que alimenta CITI/AFIP—; ahora resuelve porcodigo_afip(TipoFactura::listNCConCodigo()) y tira si no hay match; (c) la nota no generaba desglose de IVA (Account.Impuesto), así que nunca entraba al reporte CITI Compras (RG 3685) — se deriva proporcionalmente del desglose de la factura ajustada; (d) guardrail contra el caché_cake_model_desactualizado, que descartaríagasto_ajustado_iden silencio. Dos bugs más, encontrados corriendo contra datos reales y no por los unit tests: tirar excepción cuando no se podía inferir la letra fiscal bloqueaba el 63,8% de los gastos de producción (medido en 6 tenants, 83.488 gastos: 16,4% sintipo_factura_id, 47,4% con "Otros"/"X"/"TIQUE"/"Vale") — ahora una nota contra un comprobante NO fiscal queda sintipo_factura_id(la nota tampoco es fiscal), y solo falla si la factura SÍ tiene letra pero al comercio le falta esa NC en el catálogo; ysignoValidoSegunTipoFactura()identificaba la nota solo por el tipo, así que con tipo NULL rechazaba el importe negativo — ahora la reconoce porgasto_ajustado_id(paxapos#781, issue #773) - [MCP/nestjs] El servidor MCP pasa a exponer las escrituras como tools con nombre propio (antes genéricas), con
annotations(readOnlyHint/destructiveHint) y un parámetroconfirm: trueobligatorio en las irreversibles — mismo criterio de "pedir confirmación antes de ejecutar algo que no se puede deshacer" que ya rige en la UI (paxapos/nestjs#116). Se suman 10 tools nuevas que consumen los cuatro endpoints de arriba: conciliación de OC, pagos a proveedor (crear/pendientes/aprobar/rechazar/ejecutar), recetas (ver/guardar) y nota de crédito/débito de proveedor (crear/consultar) (paxapos/nestjs#117)
Cambios
- [Mesas/Performance]
/mesas/indextardaba hasta 31 s en producción. No es volumen ni índice faltante: MySQL 5.7 (la versión de prod) elige Block Nested Loop para cualquier tabla de referencia muy chica del join —acádescuentos, con 5 filas— y basta que UNA tabla use BNL para que el optimizer abandone el índice que satisfacía elORDER BY Mesa.created LIMIT 20y pase aUsing temporary; Using filesort, ordenando las ~174.000 filas condeleted=0enteras antes de aplicar el LIMIT. Sacar la tabla que hoy dispara BNL no alcanza —el rol lo toma otra tabla chica (medido: 3,95 s → 3,30 s → 3,03 s)—, así que se cambia la FORMA de la query: paginar sobremesassola (sin contain) y traer las asociaciones con un segundo find acotado a los ids de la página. Medido contra el dump de producción de delicias_del_cerro (358.322 mesas): la query del listado baja de 2.487 ms a 11 ms + 1 ms de re-fetch, elCOUNT(*)de 909 ms a 55 ms, HTML idéntico byte a byte. El export (sin LIMIT) y las búsquedas por campos de un modelo asociado (mozo_numero,tiene_mesa_salon) siguen por el camino viejo a propósito (core#156, #757) - [Cobros/Performance]
PaymentsTransactionsController::index()containeabaCreator(resuelto fila por fila, cruzando datasources) yCreatorGeneric(joinea, pero dispara el mismo Block Nested Loop de MySQL 5.7 por sergeneric_usersuna tabla chica), y sin índice que empiece pordeletedel plan degradaba a full scan con filesort. Medido en delicias_del_cerro (344.985 filas): 8,33 s → 0,051 s (×163); en producción este listado llegó a 71 s, con 19 de 53 requests por encima de 10 s. Se agregaRistoAuditableBehavior::hidratarCreadoresGenericos(), gemelo del existente paraCreator, que resuelve los genéricos en una sola query dejando la fila con la misma forma que dejaba Containable. No arregla/mesas/index(el otro endpoint de core#156): ahí el disparador de BNL esdescuentos, noCreatorGeneric, y necesita el cambio de forma de la query de arriba (core#156, #754)
Corregido
- [Account/API]
/account/gastos/ver/{id}.jsondevolvía 500 y nadie lo sabía: elcontainpedíaMedia(id,type,model,file_path)yfile_pathno existe en la tabla de medias — "1054 Unknown column 'Media.file_path'". Es el endpoint que consume la toolinvoice_get_gasto_detalledel MCP, así que el detalle de un gasto por MCP estaba roto. Los otros cuatrocontainde Media del mismo controller piden solo(id, type, model), que es lo que hay; la URL del comprobante se arma con el id. De paso, ese detalle ahora incluye los renglones de la factura (GastoItem): sin eso, lo que devolvía era la lista de la OC vinculada —lo que se pidió, no lo que se facturó— y con 1 OC y N facturas las N contestaban lo mismo - [Account/Auth] La API volvía a leer el catálogo de tipos de impuesto:
TipoImpuestosController::isAuthorized()gateaba todo el controller —lectura incluida— detrás deuserIsAdmin(), y el JWT de la API es del usuario final del tenant, no un admin. 24 × 403 enGET /<tenant>/account/tipo_impuestos/index.jsonen 4 días de canary;InvoiceApiService::getTiposImpuestos()devuelve[]en el catch, así que el agente de facturas quedaba sin alícuotas para mapear el IVA del OCR. Ahora lectura (index/view) exigeFinanzas.contabilidad:read(mismo shape queUnidadDeMedidasController, el otro catálogo global) y las mutaciones (add/edit/delete) siguen admin-only, gateadas antes dePermissionManagerpara que el bypass de dueño de tenant no alcance a un catálogo global - [Compras] El circuito de compras se nombra por documento, y la OC dejó de crear catálogo sin confirmación. Había cinco pantallas distintas con "pendiente" en el nombre —ítems solicitados sin OC, OCs esperando aprobación, facturas sin digitalizar, renglones de factura sin vincular y pagos no ejecutados— y ninguna decía de qué documento hablaba
- El menú de Compras se agrupa en Lo que pido · Seguimiento · Catálogo, con cada entrada nombrando su documento ("Crear orden de compra", "Mercaderías por pedir", "Órdenes a aprobar", "Ítems de factura por vincular"). En Finanzas, "Pendientes OCR" pasa a "Facturas por digitalizar"
- Elemento nuevo
Risto.encabezado_documento: una línea bajo el título que dice qué es la pantalla —pedido interno, orden de compra, factura o catálogo— con la explicación en un solo lugar para que no se desincronice entre vistas - La OC ya no da de alta mercaderías en silencio. Escribir un nombre que no existía creaba mercadería + producto al guardar (
getOrSave); el JS avisaba en un panelito, pero nada bloqueaba. Es por donde entraron el TV, los tornillos y el abono de mantenimiento al catálogo de Cangas, y era la última puerta abierta después de #686. Ahora un modal lista exactamente qué se va a crear y el gate real vive en el servidor (ComprasAppController::_resolverMercaderiasDeRenglones): sin confirmación no se crea nada, en los tres formularios que cargan renglones (OC, recepción y lista interna) - Ese modal es además donde se responde "¿y si quiero comprar un horno?": un equipo, un servicio o un gasto no van por la orden de compra: se cargan como factura, con el renglón marcado Activo o Servicio (ver #741)
- [Compras/Account] Épica #698 — la factura y la orden de compra pasan a ser documentos distintos, y el catálogo de compras deja de ensuciarse solo. El OCR de facturas, cuando no encontraba match para un renglón, creaba producto + mercadería en el catálogo y además escribía sobre las líneas de la OC. En Paxapoga Cangas eso dejó 62 "mercaderías" que eran cargos de Edesur, IIBB, un TV, tornillos y abonos de mantenimiento; 29 OCs sintéticas; y 209 mercaderías sin rubro. Mientras ese camino siguiera abierto, cualquier limpieza del catálogo se volvía a ensuciar sola
- El detalle de una factura ahora vive en
account_gasto_items, con renglones tipados (mercaderia·servicio·activo·impuesto_cargo·otro). Antes no existía: con 1 OC y N facturas, las N mostraban la misma lista —la de la OC— y no había forma de saber qué trajo cada una - El OCR no crea catálogo por ningún camino.
__buscarOCrearMercaderia()pasó a ser__buscarMercaderia(): solo busca. Un renglón sin match queda comoGastoItempendiente de vincular y lo resuelve una persona.__actualizarPedidoDesdeOcrItems()desapareció: con OC previa el OCR concilia porpedido_mercaderia_idy no escribe ni una fila decompras_pedido_mercaderias, ni siquiera en un renglón que el usuario editó a mano (el gatetocadode #581 dejó de ser necesario).ACCION_ELIMINARde la grilla omite el renglón de la factura y deja la OC intacta - La OC generada desde una factura lleva SOLO los ítems de mercadería vinculados, y si no queda ninguno no se crea ninguna OC — una factura de servicios ya no genera la OC sintética que ensuciaba todo. El tipo de cada renglón sale de
GastoItem::tipoSugerido()y se loguea con[GASTO_ITEM TIPO]junto al contexto que lo decidió, para que el criterio sea observable - Dónde se ve: panel "Ítems de esta factura" en la ficha (con Σ ítems contra el neto y aviso de diferencia), columna Facturado por línea en la OC (3-way match pedido / recibido / facturado, sumando todas las facturas de esa OC), tab Facturas desplegable a los renglones de cada una, grilla de ítems en el form —también en el alta— y vista Pendientes de vincular con badge en el sidebar de Compras
- Backfill acotado (
Account.gasto_items_backfill, #597): copia solo las líneas que el OCR había escrito desde la factura. Una OC real sin marcas de OCR no se toca: sus líneas son lo pedido, no lo facturado, y copiarlas fabricaría evidencia de que nunca hubo diferencias. Dry-run por defecto, idempotente y reversible - Contrato de API:
gastos/add.jsonyedit.jsondevuelvenpedido_ideitems_pendientes. El invoice-agent de nestjs manda el detalle comoOcrItems[], dejó de llamarcrearMercaderiay su cliente ya ni siquiera tiene ese método (paxapos/nestjs#107) - ⚠️ Deploy: el lado nestjs necesita este release en producción — el detalle viaja en el mismo payload del gasto y la OC del flujo normal ahora la crea CakePHP
- [Compras/Stock] Unificar productos duplicados borraba el inventario de las fichas absorbidas. Los dos caminos de fusión cerraban los stocks abiertos con criterios opuestos:
Mercaderia::absorberMercaderia()conservaba la existencia (cant_actual_calculada) yProducto::unificarProducto()cerraba en 0. Con 17 nombres duplicados esperando fusión solo en Cangas, la diferencia es material. Ahora los dos usanStock::cerrarConservandoExistencia(), único lugar donde vive la regla: la mercadería sigue estando en el depósito, lo único que cambia es bajo qué ficha se la registra. ⚠️ Afecta valorización: a partir de ahora una fusión de productos ya no baja el inventario a cero - [Compras/Stock]
Mercaderia::fusionarEnProducto()re-parentaba la mercadería (Mercaderia.producto_id) pero no tocabastock_stocks.producto_id, que seguía apuntando al producto de origen — yabsorberMercaderia()tampoco lo cubre, porque migra porcompras_mercaderia_id, no porproducto_id. La partida quedaba con las dos columnas contradichas:compras_mercaderia_iddel producto destino yproducto_iddel producto viejo, y las dos siempre van acopladas donde hayproducto_idseteado (450 partidas abiertas así solo en los tres tenants Paxapoga), así queStock::cantPorProducto()yStock::valorarStock('producto')seguían reportando contra el producto equivocado. Se alinean las dos columnas dentro de la transacción de fusión, antes de la absorción, yCompras.salud_catalogosuma el chequeostock_producto_desalineadocomo señal continua en vez de algo que dependa de acordarse de correrlo - [Compras]
Mercaderia.stock_cant_x_umvalidado en el modelo. Es la equivalencia entre unidad de compra y unidad de stock (1 Caja = 15 Kilo), el número que convierte una compra en consumo, y el modelo aceptabanull,0y negativos; cuando queda vacíocreateCosto()cae a 1 y el bulto entra al stock como una sola unidad — el origen de las diferencias de 5× a 24× medidas en Cangas. #684 lo validaba solo en el camino de API. El alta que no lo manda no falla: le queda 1 explícito, nonull - [Compras] Una línea de OC con
precio = 0.00ganaba como "última compra" y dejaba el costo de la mercadería en cero.ultimaCompra()filtrabaprecio IS NOT NULLy un 0.00 no es NULL. El costo 0 no explota: se propaga en silencio al costeo de recetas, a la rentabilidad por producto y al reporte de materias primas. Además elorderera solo porcreated, sin desempate, así que con dos líneas del mismo día el resultado era no determinista — la mercadería 2152 de Cangas (Vino Durigutti) mostraba costo 0 teniendo la factura de ese mismo día cargada ($150.743,80 vs $0,00). Ahora filtraprecio > 0y desempata porid DESC. Un 0 significa "no se cargó el precio", no "salió gratis": el costeo lo ignora y el reporte de salud lo lista para corregirlo. ⚠️ Corregir la query no repuebla nada: las fichas afectadas pasan a mostrar el último precio real, que en algunos casos es de hace años - [Compras] Recepción: aviso cuando la cantidad recibida no cierra con la equivalencia.
recibida_cantidadse guarda en unidades de stock y se tipea a mano; comparando lo pedido contra lo recibido en 944 líneas de OC de Cangas, 66 mercaderías tenían factor inconsistente entre órdenes — 416 líneas, el 44 %, con "Queso Portsalud" en 1, 4,25 y 381. Ahora la recepción compara contracantidad × stock_cant_x_umy avisa por Flash (form) y poravisos[](recepcionar_api) si el desvío pasa el umbral. No bloquea: una entrega parcial o un faltante son legítimos, y recibir 0 no dispara nada. Umbral configurable conCompras.recepcionDesvioMaximo, default 20 % — deja pasar el peso variable (±3 % es normal en peceto, mondongo y quesos) - [Compras] Reporte de salud del catálogo:
Compras.salud_catalogo, READ-ONLY, por tenant. Los hallazgos de la auditoría de Cangas salieron de un análisis a mano y nadie los vio degradarse durante meses porque no había nada que los mirara. Ocho chequeos —sin unidad de stock, equivalencia sospechosa, duplicados por nombre normalizado, sin rubro, sin proveedor, sin precio, precio 0 y recepciones incoherentes— con los ids afectados en--detalle. Reusa la normalización canónica y el criterio de desvío, así que el reporte y los avisos no pueden medir distinto - [Compras] Una sola normalización de nombre de mercadería por propósito. Convivían tres implementaciones distintas en dos lenguajes y ninguna era la fuente de verdad; cuando divergen, el OCR matchea algo que el typeahead no —o al revés— y nace un duplicado (
Cafe Premium Expresso×3 en Cangas). Quedan dos, las dos en el modelo y documentadas:normalizarNombre()para persistir y buscar con=(el collation_ciya resuelve mayúsculas y acentos) ynormalizarNombreParaComparar()para comparar en PHP, que además pliega mayúsculas, acentos y la coma decimal. El invoice-agent de nestjs la espeja y los dos lados testean el mismo set de casos - [Compras]
Pedido::borrarMercaderiaDelPedido()borraba la línea de una OC EN DURO: llamabadeleteAll($conditions, false)sin$callbacks, y con!$cascade && !$callbacksModel::deleteAll()cortocircuita a unDELETE FROMcrudo, salteando el soft-delete deRistoSoftDeleteBehavior::beforeDelete()— la línea desaparecía decompras_pedido_mercaderiassin rastro ni auditoría. Mismo patrón que #713 (MercaderiasController::index(), ya arreglado en #714). AhoradeleteAllva concallbacks = true(conditions prefijadas por alias, para no chocar con el join que agrega el behavior) yMercaderia hasMany PedidoMercaderiapasa a filtrardeleted— mismo agujero de #598 pero del lado de la mercadería, latente hasta ahora porque el borrado físico no dejaba filasdeleted=1que filtrar - [Product/Recetas] Tres bugs con el mismo síntoma: secciones de receta vacías o en blanco.
listado_recetasde subproductos siempre vacío porque el segundopaginatereutilizaba las conditions del primero (id IN (vendibles) AND id NOT IN (vendibles), conjunto vacío por construcción). En la ficha de producto los dos paneles de receta no se renderizaban nunca:$tieneRecetamiraba$producto['Ingrediente']—esa clave son los usos del producto COMO ingrediente de otros, no su receta, yview()ni la trae— en vez de$producto['Receta']['id']; el gate deseccion_ingredienteleía una clave de$productoque en realidad es una viewVar suelta del controller. Se saca además un conteo de ingredientes que mostraba un número inventado y se corrigecosto_real, columna que no existe (la viewVar es$costo) - [Mail/Seguridad] El fallback de reply-to armaba
'noreply@' . env('HTTP_HOST')sin sanitizar.HTTP_HOSTlo manda el cliente: con puerto (dev2.paxapos.com:8443) el resultado no valida como email yCakeEmailtiraSocketException, tumbando el request con 500 —en/login, o sea el tenant entero sin poder entrar— y con un headerHostarbitrario el dominio del remitente lo elige quien manda el request (noreply@evil.com). Solo se llega a este fallback cuandoRestaurante.mailyApp.defaultEmailestán vacíos, que es justo lo que lo hacía peligroso. Ahora se recorta el puerto y se valida el host contra[a-zA-Z0-9.-]+, con un dominio fijo si no pasa - [Compras/API]
vincular_gastopisaba en silencio la OC que un gasto ya tenía: hacíasaveField('pedido_id', $id)a secas, así que re-vincular un gasto reemplazaba sin aviso la orden de compra anterior. El caso real: una factura del portal público nace con supedido_idcorrecto, y el worker de digitalización (que no reconocía esa OC por un desacople de contrato, arreglado en paxapos/nestjs#114) creaba otra y la enganchaba por acá, perdiendo el vínculo original. Ahora re-vincular a la MISMA OC responde 200 no-op (para que un reintento del worker no falle) y vincular a una OC DISTINTA cuando el gasto ya tiene una responde 409 con elpedido_idactual — mover un gasto de OC es una decisión explícita, para eso existedesvincular_gasto()(paxapos/nestjs#108, #748) - [Permisos/Billing] El reporte de comisiones daba 403 al superadmin con un token de API válido. No era
userIsAdmin()—chequea el SuperRol globalsuperadmin, el gate correcto acá— sino DÓNDE se evaluaba: el gate vivía enbeforeFilter()(Controller.initialize), que corre antes deController.startup, que es donde Auth resuelve a los autenticadores stateless (JWT de la API, devices de fichaje); para esos clientesAuthComponent::user()está vacío por definición en ese punto. Mismo defecto que #577, que ya dejó el patrón sancionado (isAuthorized($user)) y un trinquete con los controllers pendientes —CommissionsReportControllersale de esa lista. No se migra aPermissionManagera propósito: el reporte leebilling_usage_recordsde la DB master (comisiones de TODOS los comercios), y un dueño de tenant no debe verlo ni acotado al suyo — para eso está/billing - [Docker/Build] El target
productiondel Dockerfile terminaba suRUNconcomposer install ... || true && mkdir ... && chown ...: como&&/||asocian a izquierda, un fallo decomposer installquedaba neutralizado por el|| truede una instrucción posterior (elfindde limpieza), y el paso reportabaDONEcon una imagen degradada —sirviendo elVendor/del disco de quien buildea— sin que nadie se enterara. Fix mínimo: agrupar elfindcon{ ...; }para que su|| truecubra sólo a él.composer.lockya no está excluido del contexto de build (resuelto antes, en main)
Quitado
- [Limpieza/Vistas] Se borran 4 vistas/elements sin callers (
sidebar_comandero.ctp,modulo_list_element.ctp,fiscalberry_btn.ctp,payway_search_ajax.ctp) y el CSS que solo ellos cargaban, verificado contra los dos includes dinámicos deLayouts/base.ctp(11 literales posibles, ninguno de estos archivos) - [Limpieza/CSS] Se borran dos hojas de estilo huérfanas (
Fidelization/simple_add.css,Risto/user-home-modern.css, cero referencias en todo el repo) y se ajusta el baseline delint-design-tokens— 9 mejoras que ya estaban en el árbol, 0 entradas nuevas
_Detalle archivado en [docs/CHANGELOG_2026_H2.md](docs/CHANGELOG_2026_H2.md)._
Nueve releases de producción seguidas (v3.5.0, v3.5.1, v3.5.2, v3.6.0, v3.6.1, v3.7.0,
v3.8.0, v3.8.1, v3.8.2) que vivieron mezcladas en [Unreleased] hasta esta
reorganización. Lo más importante:
- Caja/Arqueos rehecho: doble partida en traspasos entre cajas, ventana de fechas de un arqueo saneada — dejó de arrastrar toda la historia de la caja — y se mata la pantalla vieja de arqueos, unificando todo en
nindex/nadd/ncerrar/nedit/nview. - Medios de cobro rediseñados: catálogo cerrado de medios manuales, un cobro integrado (QR/Payway/Mercado Pago) deja de heredar un tipo de pago manual, e ícono único por medio en POS/arqueos/backoffice (imagen propia > SVG > genérico).
/mesas/indexdejó de tirar 500 por memoria agotada en tenants con historial grande — tresfind('list')sinrecursivetraían la tablamesasentera.- Cuenta Comercial (ex
GroupSite) se vuelve gestionable: panel de staff, responsables por rol, self-service en/billing_accounts. - Datasources cruzados: CakePHP no joinea entre datasources, y desde la Gran Normalización varios catálogos globales (unidades de medida, tipos de impuesto) quedaron rotos o mintiendo datos viejos hasta que se corrigió con JOIN explícito calificado.
- Compras — primera entrega de ítems de factura (schema de #698, sin activar):
account_gasto_items,compras_rubros.naturaleza, UM por ingrediente de receta; y recepción que sólo pide cantidad para mercadería bajo control real de stock. update_tenantsmentía "OK" sin aplicar ningún ALTER si el operador confirmaba conYmayúscula en vez dey.
🎯 Beta Release (beta.paxapos.com)
Commit base: ca579494e58f295c8713d4c8ead0e2c88f46ffe1
Nuevo
- Segunda versión de producción
- Branch de release:
release/v2.x - Configuración específica para beta.paxapos.com
Cambios
- Mejoras y actualizaciones desde v1.0.0
🏛️ Legacy Release (paxapos.com)
Commit base: f6574ee03ecab3e8b188e01d4728c089fdcbe6d0
Nuevo
- Primera versión estable de producción
- Branch de release:
release/v1.x - Configuración base para paxapos.com
Features
- Refactor de RistoSecurityComponent.php para mejorar redirección de URL para paxapos
- Sistema base de punto de venta
- Gestión de mesas y comandas
- Sistema de pagos
- Reportes y arqueos
Notes
- Esta versión está en modo mantenimiento
- Solo se aplicarán hotfixes críticos
- No se agregarán nuevas features