Presupuestador_web/doc/capitulos-cotizaciones.md

47 KiB

Capítulos en cotizaciones: auditoría y plan

Fecha: 19 de octubre de 2026. Estado: fases 1 a 5 implementadas y probadas sin conexión a base de datos, incluida generación real de PDF con Chromium. La aplicación y validación del esquema en MariaDB siguen pendientes de un entorno autorizado; todavía no se considera completada la funcionalidad de extremo a extremo.

Requisitos confirmados

  • Las cotizaciones nuevas comienzan sin capítulos. Las antiguas siguen pudiéndose consultar, editar, duplicar y exportar sin convertirlas.
  • Una cotización puede tener cero capítulos o uno o más capítulos. Si tiene capítulos, todos sus detalles pertenecen a uno de ellos, incluidos los bloques de texto.
  • El capítulo tiene una cabecera donde solo se edita su descripción y un pie con su subtotal automático, sin campos editables.
  • Un botón en la barra de herramientas de detalles permite añadir capítulos.
  • Al mover un capítulo se desplazan todos sus detalles, conservando el orden interior.
  • El PDF soporta ambos modos. Con capítulos se puede mostrar el desglose económico de los detalles o mostrar únicamente sus totales de capítulo, conservando las descripciones.

Fase 1: decisiones funcionales

Las reglas de esta sección y de los apartados «Comportamiento del editor» e «Informe PDF» constituyen la especificación funcional de partida para las fases 2 a 6. Se concretan los valores propuestos en el plan de actuación; cualquier cambio posterior debe actualizar sus casos de aceptación.

Modos, títulos e importes

  • La cotización empieza en modo libre, sin capítulos. Leer, editar o exportar una cotización antigua no activa la agrupación.
  • Crear el primer capítulo cambia toda la cotización al modo agrupado en una única operación: todos sus detalles, incluidos los textos, pasan al capítulo. No existen estados intermedios guardables con detalles huérfanos.
  • Se permiten capítulos vacíos y capítulos que solo contienen texto. Ambos muestran subtotal de cero en la moneda de la cotización.
  • La descripción del capítulo es obligatoria: se rechaza un valor vacío o compuesto solo por espacios al crear o guardar. Puede coincidir con la de otro capítulo; la identidad no depende del título. Cancelar el alta no modifica los detalles ni el modo.
  • Los capítulos se numeran automáticamente desde 1 según su orden. La cabecera muestra «1. Descripción» y el pie «Subtotal capítulo 1. Descripción», con sus etiquetas traducidas. La numeración no es editable ni forma parte de la descripción guardada.
  • La cabecera solo permite editar la descripción. Sus botones de selección, movimiento y acciones no representan campos de datos editables. El subtotal se muestra en el pie y no se repite como campo monetario editable en la cabecera.
  • No se incluyen capítulos anidados. El plegado visual del editor se incorpora por petición posterior: los capítulos empiezan desplegados; el botón de flecha oculta sus detalles y acciones, conservando título y subtotal visibles. Este estado local se mantiene al reordenar el capítulo y no se guarda ni se envía al PDF. Los capítulos duplicados empiezan desplegados.
  • El subtotal suma los totales de sus líneas tras sus descuentos individuales y antes del descuento general e impuestos. No hay descuentos, impuestos ni moneda propios del capítulo. Se conservan las reglas actuales de precisión y redondeo.
  • Agrupar, desagrupar o mover contenido conserva los valores de las líneas y el total general. La visibilidad del PDF tampoco modifica importes ni los oculta en el editor.

Capítulo activo y destinos

  • Crear un capítulo, activar su cabecera o interactuar con uno de sus detalles lo convierte en el capítulo activo. La selección múltiple no cambia por sí sola ese destino.
  • Añadir filas, artículos o bloques los inserta al final del capítulo activo; si no hay activo, al final del último capítulo. El selector de catálogo o bloques conserva el destino al abrirse, también si permite varias inserciones.
  • Insertar encima o debajo de un detalle mantiene su capítulo, incluso si se trata de la primera o última línea. El pie no es un destino de inserción independiente.
  • Eliminar el capítulo activo deja activo el capítulo anterior; si no existe, el siguiente. Al quitar todos los capítulos deja de haber capítulo activo.

Selección, movimiento y acciones en grupo

  • La cabecera selecciona el capítulo como unidad completa. El pie no se selecciona, edita, duplica, elimina ni arrastra por separado.
  • Arrastrar un capítulo seleccionado entre varios capítulos seleccionados mueve esas unidades completas, manteniendo su orden relativo y el de sus detalles. El destino es un límite entre capítulos, nunca el interior de otro.
  • Arrastrar detalles seleccionados los inserta consecutivamente en el destino y conserva su orden visual relativo. Si cambia el capítulo, cambia también su pertenencia. Se puede dejar vacío el capítulo de origen.
  • Los destinos de detalle son posiciones dentro de un capítulo, incluido un capítulo vacío; no se permite soltar detalles antes de la primera cabecera ni fuera del último capítulo.
  • Si la selección mezcla capítulos con detalles de otros capítulos, se deshabilita su arrastre conjunto y se explica el motivo. Los detalles pertenecientes a capítulos seleccionados ya están incluidos en esas unidades y no cuentan como selección mixta.
  • Duplicar una selección coloca las copias de capítulos después del último capítulo seleccionado y las copias de detalles después del último detalle seleccionado de su respectivo capítulo. Se conserva el orden relativo. Los hijos de un capítulo seleccionado se procesan una sola vez.
  • Duplicar capítulos copia títulos y contenido; la numeración se recalcula y las identidades son nuevas. Duplicar detalles mantiene su capítulo y los valores actuales del formulario, incluidos cambios aún no guardados.
  • Borrar una selección con contenido requiere confirmación e indica los capítulos y detalles afectados, sin contar hijos dos veces. Borrar solo detalles conserva los capítulos, aunque queden vacíos.
  • Las mismas restricciones de pertenencia y movimiento se aplican con ratón, teclado y pantalla táctil.

Borrado y vuelta al modo libre

  • Eliminar un capítulo vacío no requiere un diálogo adicional. Si era el último, la cotización vuelve al modo libre.
  • Para eliminar un capítulo con contenido, el diálogo permite cancelar, eliminar capítulo y contenido o conservar el contenido trasladándolo a un capítulo no seleccionado para borrar. El traslado añade las líneas al final del destino, conservando el orden visual de origen.
  • Si no queda otro capítulo de destino, la opción de conservación será «Quitar capítulos y conservar detalles»: elimina la agrupación completa y mantiene todas las líneas en su orden visual. Así se puede quitar el último capítulo sin perder contenido.
  • La acción «Quitar todos los capítulos» está disponible siempre que haya capítulos y pide confirmación. Conserva todos los detalles y sus valores, elimina todas las pertenencias y vuelve al modo libre. Cancelar no produce cambios.
  • Las operaciones afectan al formulario y marcan cambios sin guardar; solo el guardado las persiste. Cancelar un diálogo no modifica datos. Se mantiene el aviso existente al abandonar con cambios pendientes.

Criterios de cierre de la fase

La fase 1 entrega reglas y escenarios para implementación, sin incorporar todavía código de dominio, interfaz ni persistencia. Los siguientes resultados quedan fijados:

Caso Acción Resultado exigido
F1-01 Añadir el primer capítulo a tres detalles libres Los tres pertenecen al nuevo capítulo en el mismo orden; no cambia el total
F1-02 Cancelar el primer capítulo o introducir solo espacios Cancelar conserva el modo libre; el título inválido impide completar el alta
F1-03 Añadir un segundo capítulo y un artículo El segundo se crea vacío y activo; el artículo se incorpora a él
F1-04 Mover un capítulo con artículos y textos Se mueve la unidad completa; la numeración se actualiza y sus detalles mantienen el orden
F1-05 Mover el último detalle de un capítulo a otro vacío El origen queda vacío con subtotal cero; el destino recalcula su subtotal; el total general no cambia
F1-06 Borrar el último capítulo conservando contenido Desaparece la agrupación y quedan todas las líneas libres con sus valores y orden
F1-07 Duplicar o borrar un capítulo y alguno de sus hijos seleccionados El hijo se procesa una sola vez; nunca queda una referencia huérfana
F1-08 Ocultar importes individuales y guardar Editor con precios visibles; PDF con cantidades, descripciones y subtotales de capítulo, sin precios ni descuentos individuales
F1-09 Quitar capítulos, guardar y volver a agrupar La preferencia PDF se conserva, se ignora en modo libre y vuelve a aplicarse en modo agrupado
F1-10 Editar una cotización antigua sin añadir capítulos Se mantiene el flujo libre y la plantilla de exportación sin capítulos

Estos escenarios serán verificados mediante pruebas en las fases de implementación; su definición no implica que la aplicación ya los soporte.

Auditoría inicial del código

Las rutas citadas son relativas a la raíz del repositorio. Esta tabla describe el estado previo a las fases de implementación; sus entregas se documentan al final.

Área Código actual Consecuencia para los capítulos
Especificación inicial doc/readme.adoc exige conceptos libres y orden de los detalles; no define capítulos La agrupación constituye una ampliación del modelo actual
Dominio server/src/contexts/sales/domain/entities/Quotes/Quote.ts, QuoteItem.ts Quote contiene una colección plana; QuoteItem contiene artículo, descripción, cantidad, precio y descuento. No hay pertenencia ni entidad capítulo
Cálculos QuoteItem.subtotalPrice/totalPrice, Quote._calculateTotalPriceItems; client/src/lib/calc.ts El subtotal de cotización suma los totales de línea después del descuento de línea; luego aplica descuento general e impuesto
DTOs shared/lib/contexts/sales/application/dto/Quote/ Crear, obtener, actualizar y duplicar tienen contratos propios que habrá que extender de forma coherente
Validación DTOs con Joi y Quote.create Joi admite campos desconocidos; Quote.create tiene pendientes las reglas de negocio. Añadir campos al formulario no garantiza la integridad en servidor
Casos de uso server/src/contexts/sales/application/Quote/{CreateQuote,UpdateQuote,DuplicateQuote}.useCase.ts Crear y actualizar reconstruyen las líneas; duplicar crea nuevas entidades de detalle. Hay que transportar capítulos y reasignar sus referencias al duplicar
Persistencia server/src/contexts/sales/infrastructure/sequelize/{quote,quoteItem}.model.ts quote_items tiene clave compuesta quote_id/item_id y orden mediante position; no tiene capítulo
Repositorio y mapeo Quote.repository.ts, mappers/{quote,quoteItem}.mapper.ts bajo la misma infraestructura Obtener ordena por position; actualizar elimina y reinserta los detalles en una transacción. Los capítulos necesitan formar parte de esa misma escritura atómica
Presentación API Presentadores de createQuote, getQuote, updateQuote, duplicateQuote Cada uno mapea sus propias líneas. Es necesario cubrir todos; no basta con modificar GET
Alta SPA client/src/app/quotes/create.tsx Recoge datos generales y redirige al editor; no requiere introducir capítulos en el alta
Editor client/src/app/quotes/edit.tsx, components/editors/QuoteDetailsCardEditor.tsx React Hook Form usa items como lista plana mediante useFieldArray; recalcula importes al cambiar campos o estructura
Tabla y acciones QuoteItemsSortableDataTable.tsx, QuoteItemsSortableDataTableToolbar.tsx, QuoteItemsInlineRowActions.tsx TanStack Table y dnd-kit mueven filas e incluyen selección múltiple, inserción, duplicado y borrado. Todas esas operaciones necesitan respetar pertenencia
Artículos y textos Selectores de catálogo y bloques en QuoteDetailsCardEditor.tsx Actualmente añaden al final de la lista; en modo capítulos deben tener un capítulo de destino
Proveedor de datos client/src/app/quotes/hooks/useQuotes.tsx Las operaciones actuales pueden seguir usando useDataSource y las claves existentes; no hacen falta llamadas HTTP desde componentes
PDF ReportQuote.controller.ts, reporter/ReportQuote.reporter.ts, reporter/templates/quote/template.hbs La ruta GET /quotes/:quoteId/report carga la cotización guardada; Handlebars recorre items y Puppeteer genera el PDF
Preferencias de informe show_line_discount_column atraviesa dominio, DTOs, modelo, editor y plantilla Existe un patrón cercano para persistir la nueva preferencia de visibilidad de importes
Traducciones GetQuote.presenter.ts, resolveLocalizedQuoteDescriptions.ts y reporter Las descripciones de catálogo tienen proyección por idioma; no debe alterarse esa lógica al agrupar ni traducirse el título libre como si fuera un artículo

Las rutas de cotizaciones ya usan checkUser, getDealerMiddleware y handleRequest. Se extenderán los contratos existentes manteniendo estos controles, sin crear endpoints independientes para editar capítulos.

Modelo propuesto

Mantener items como lista de detalles y añadir una colección chapters al agregado Quote. Cada capítulo tendrá identidad propia y descripción. Cada detalle incorporará chapter_id, nullable en el modo sin capítulos. La pertenencia será explícita, no inferida por encontrar una cabecera anterior.

Contrato orientativo, adicional a los campos actuales:

chapters: Array<{ id: string; description: string }>;
items: Array<ExistingQuoteItem & { chapter_id: string | null }>;
show_chapter_line_amounts: boolean;
  • El orden de chapters define el orden exterior. El orden de items define el orden de los detalles dentro de cada capítulo; serializar la lista plana en orden de capítulos y después de detalles.
  • El servidor devuelve además el subtotal calculado por capítulo en el DTO de respuesta. El request no acepta ese subtotal como fuente de verdad.
  • No guardar el pie como QuoteItem: produciría una línea monetaria adicional y permitiría duplicar, mover o editar un subtotal como un concepto normal.
  • No añadir un booleano has_chapters: se deriva de la colección y evita estados contradictorios.
  • El identificador del capítulo debe ser estable entre cargas y guardados. Diferenciarlo de los identificadores internos que genera useFieldArray; no usar su id interno como identidad persistente.
  • No es necesario cambiar la identidad pública de los detalles para esta funcionalidad; hoy los DTOs no exponen item_id y actualizar reconstruye las entidades.

Persistencia propuesta

  • Nueva tabla quote_chapters: quote_id, chapter_id, position, description. Clave compuesta por cotización y capítulo, con borrado en cascada desde la cotización.
  • quote_items.chapter_id nullable e indexado junto con quote_id. La referencia debe apuntar al capítulo de la misma cotización; considerar FK compuesta además de la validación de dominio.
  • quotes.show_chapter_line_amounts: booleano con valor por defecto true.
  • Persistir posiciones a partir de los arrays, como ya hace el mapper de detalles; no pedirlas también en el request.
  • Cargar capítulos y detalles con orden explícito por posición. Mantener la consulta de listado sin cargar asociaciones innecesarias.
  • Crear y actualizar cabecera, capítulos y detalles en la misma transacción. Con el reemplazo actual, eliminar primero detalles, después capítulos, insertar capítulos y finalmente detalles; no ejecutar en paralelo operaciones dependientes de las FK.
  • Los registros históricos conservan chapter_id = null y ningún capítulo. No crear capítulos automáticamente al leerlos.

La preparación y aplicación del cambio de esquema será una tarea de implementación/despliegue expresamente autorizada. Esta auditoría no ejecuta migraciones ni arranca el backend. El arranque actual usa sync({ alter: true }): antes de desplegar hay que revisar su efecto sobre la nueva tabla, columnas, asociaciones y FK en una base aislada. No dar por validado el esquema solo porque TypeScript compile.

Reglas de dominio y compatibilidad

  1. Con chapters.length === 0, todos los detalles tienen chapter_id = null.
  2. Con uno o más capítulos, cada detalle tiene exactamente un chapter_id existente en esa misma cotización. Rechazar referencias desconocidas, capítulos duplicados y pertenencias a otra cotización.
  3. No hay capítulos anidados en esta primera versión. La descripción es el único dato editable de la cabecera; la numeración, si se muestra, se deriva de la posición.
  4. El subtotal de capítulo suma el totalPrice de sus detalles, con descuento de línea aplicado, antes del descuento general y los impuestos. Usar MoneyValue y las mismas escalas y reglas de redondeo que en los cálculos actuales.
  5. Los textos sin importe no contribuyen al subtotal. Los títulos y pies nunca contribuyen al total de cotización.
  6. Agrupar, desagrupar, cambiar el orden o alternar la visibilidad del informe no cambia el total general.
  7. Validar estructura en DTO/Joi y pertenencia en el agregado antes de persistir. Devolver errores con el patrón actual, sin corregir silenciosamente una estructura inválida enviada por API.
  8. En creación y lectura antigua, campos nuevos ausentes equivalen a chapters: [], chapter_id: null y visibilidad true.
  9. Para PUT antiguos sobre cotizaciones sin capítulos, aceptar que falten los nuevos campos. Para una cotización ya agrupada, rechazar un PUT que omita la estructura de capítulos: el cliente antiguo no conoce la agrupación y podría perderla. La eliminación de agrupación debe enviar explícitamente chapters: [] y todos los detalles sin pertenencia. La preferencia de informe omitida en actualización conserva el valor guardado.
  10. Duplicar una cotización o un capítulo crea nuevos identificadores de capítulo y remapea los detalles; conserva orden, descripciones y preferencia PDF.

La compatibilidad se refiere a conservar datos y flujos antiguos; no a permitir que un cliente antiguo elimine accidentalmente capítulos que no puede representar.

Comportamiento del editor

Estas decisiones quedan fijadas en la fase 1 y se complementan con las reglas de selección, destinos y borrado anteriores:

Operación Comportamiento definido
Añadir el primer capítulo Solicitar descripción y agrupar en él todos los detalles existentes, conservando su orden. En una cotización vacía, crear un capítulo vacío
Añadir otro capítulo Crear un capítulo vacío después del activo, o al final si no hay capítulo activo; llevar el foco a su descripción
Añadir fila, artículo o bloque Insertar en el capítulo activo; si no hay activo, usar el último. En modo sin capítulos mantener el comportamiento actual
Insertar encima o debajo de un detalle Mantener el capítulo del detalle de referencia, también en sus extremos
Mover capítulo Moverlo como unidad con todos sus detalles. Solo aceptar destinos entre capítulos; conservar el orden interior
Mover detalles Reordenar dentro de su capítulo o trasladar a otro, actualizando pertenencia y orden en una operación. Incluir destino en capítulos vacíos; no permitir soltarlos fuera de capítulo
Seleccionar capítulo Seleccionar su unidad completa para acciones de grupo. Si también se seleccionaron hijos, procesarlos una sola vez
Selección múltiple de detalles Mantener la operación actual; en un traslado entre capítulos, todos los seleccionados pasan al destino conservando orden relativo
Selección mixta de capítulos y detalles sueltos Deshabilitar el arrastre mixto inicialmente con explicación traducida; permite evitar movimientos ambiguos sin quitar la selección múltiple de detalles
Duplicar detalle Duplicarlo en el mismo capítulo usando los valores actuales del formulario
Duplicar capítulo Duplicar cabecera y contenido completo con nueva identidad y pertenencias remapeadas
Eliminar capítulo vacío Eliminarlo; si era el último, volver al modo sin capítulos
Eliminar capítulo con contenido Confirmar eliminación de contenido o trasladarlo a otro capítulo; sin destino disponible, permitir quitar la agrupación conservando detalles. Nunca dejar detalles huérfanos
Quitar todos los capítulos Acción explícita que conserva los detalles y su orden visual, elimina sus referencias y vuelve al modo libre; también disponible con un único capítulo

La tabla mostrará una cabecera diferenciada, detalles y pie por capítulo, siguiendo la referencia visual. Solo la descripción de la cabecera tendrá control de edición; el pie será texto monetario calculado y no tendrá selección, arrastre ni menú de acciones.

Reutilizar las celdas actuales para detalles, los diálogos y botones existentes y dnd-kit. Generar una proyección visual de cabeceras/detalles/pies a partir de los datos; esa proyección no se envía como items ni se incorpora al cálculo general. Mantener correspondencia explícita entre índices visuales y los índices de items del formulario.

Centralizar las operaciones de agrupación en el feature de cotizaciones, reutilizando el formulario y los hooks actuales. Los subtotales deben reaccionar a cantidades, precios, descuentos, altas, bajas y traslados; leer valores observados del formulario, no solo fields, que no refleja por sí solo cada edición de celda. El guardado enviará capítulos y pertenencias junto al resto de la cotización. Mantener aviso de cambios sin guardar.

Informe PDF

Decisión de fase 1: la visibilidad de importes es una preferencia global por cotización, no por capítulo ni por descarga. Se guardará como show_chapter_line_amounts, siguiendo el patrón de show_line_discount_column. Un control en el editor permitirá «Mostrar importes de los detalles en el PDF» cuando haya capítulos. Valor inicial true, independiente de la visibilidad de descuentos y sin ocultar los importes en el editor. Quitar capítulos conserva esta preferencia para una agrupación posterior; duplicar la cotización también la conserva.

  • Sin capítulos: conservar el informe actual e ignorar esta preferencia.
  • Con capítulos y desglose: mostrar cabecera, detalles con sus importes y pie con subtotal.
  • Con capítulos sin desglose: mostrar cantidades y descripciones de detalle, ocultando precio unitario, subtotal, porcentaje de descuento e importe total de cada detalle; mostrar el subtotal del capítulo. Evitar columnas monetarias vacías y ajustar colspan y cabeceras. El descuento general, los impuestos y el total de la cotización siguen en el resumen final.
  • El resumen de descuentos de línea existente se mantiene como dato global según show_line_discount_column; nunca debe revelar precios ni porcentajes de detalles individuales cuando se oculte el desglose.
  • Usar el mismo cálculo de capítulo en API y reporter, sin sumar cadenas formateadas ni confiar en subtotales enviados desde el navegador.
  • Adaptar ReportQuote.reporter.ts y la plantilla única template.hbs para ambas estructuras. No duplicar plantillas completas.
  • Añadir etiquetas de capítulo y subtotal a los idiomas documentales reales (es, en, fr). Los títulos libres se imprimen como texto escapado; conservar la resolución localizada de artículos existente.
  • La preferencia persistida llega tanto a previsualización como a descarga mediante la ruta actual; no es necesario añadir parámetros de URL. La previsualización actual usa datos guardados, por lo que el cambio requiere guardar para reflejarse en el informe.
  • Verificar PDF de varias páginas: cabecera junto al primer detalle cuando quepa, pie junto al último, cabeceras de tabla repetidas y capítulos largos capaces de partirse. No aplicar break-inside: avoid al capítulo completo.

Las cantidades y descripciones permanecen visibles en ambos modos del PDF. Con desglose, la columna de descuentos depende de show_line_discount_column; sin desglose, no se muestran descuentos individuales aunque esa preferencia esté activada. La cabecera del capítulo muestra número y descripción y su pie muestra el subtotal, tanto con desglose como sin él.

Secuencia de implementación

  1. Reglas funcionales — completada. Modos libre/agrupado, títulos, subtotales, destinos, selección, movimientos, duplicado, borrado y preferencia PDF definidos en este documento, con escenarios F1-01 a F1-10.
  2. Dominio y contratos compartidos — completada. QuoteChapter, pertenencia y preferencia incorporados al dominio; DTOs y validadores ampliados. Presentadores de las cuatro respuestas adaptados al contrato. Pruebas de invariantes, subtotales, compatibilidad antigua y respuestas API.
  3. Backend y persistencia — implementación completada. Modelos, asociaciones, mappers, repositorio transaccional y casos de uso adaptados. Lectura/escritura, rechazo de estructura inválida y duplicado con remapeo probados con dobles. Cambio de esquema preparado en doc/capitulos-esquema.md, sin aplicación a una base existente ni certificación de integración MariaDB.
  4. Editor — implementación completada. Botón de alta, cabecera editable, pie calculado, destinos de inserción y movimiento de capítulos/detalles. Selección, duplicado, borrado, traslado y vuelta al modo libre cubiertos mediante pruebas del formulario, con sensores de arrastre simulados. Pendiente la inspección en navegador y la integración con una API/base autorizadas.
  5. PDF — completada. Preferencia guardada, proyección del reporter, plantilla de ambos modos y traducciones. Pruebas HTML de contenido y ausencia de importes individuales, más generación e inspección de PDF multipágina con recursos locales. La validación integrada con datos guardados en MariaDB sigue en fase 6.
  6. Regresión y entrega. Tests acotados, lint y compilación de paquetes afectados. Con entorno aislado autorizado, verificar esquema y ampliar E2E para crear, guardar, recargar, reordenar, duplicar y exportar ambos modos. Desplegar esquema/backend compatibles antes del cliente y evitar revertir a un backend que ignore capítulos después de empezar a guardarlos.

Cada etapa tendrá pruebas de su comportamiento; la funcionalidad no se considera completa hasta cubrir editor, API, persistencia y exportación conjuntamente.

Casos de aceptación prioritarios

  • Cotización antigua: abrir, editar, guardar, duplicar y exportar conserva líneas, orden y totales, sin capítulos creados automáticamente.
  • Cotización nueva: comienza sin capítulos y permite seguir trabajando de ese modo.
  • Crear primer capítulo con detalles existentes no pierde ninguna línea ni deja conceptos fuera.
  • Dos capítulos con artículos y bloques de texto: guardado y recarga conservan agrupación, descripciones y orden.
  • Editar cantidad, precio o descuento actualiza pie y resumen; mover detalles recalcula ambos capítulos sin cambiar el total general.
  • Ejemplo: líneas con total 90 € y 50 € dan capítulo de 140 €; otra línea de 60 € da capítulo de 60 €. Subtotal general de 200 €; descuento general del 10 % e impuesto del 21 % producen 217,80 €. Reordenar u ocultar precios mantiene estos resultados.
  • Capítulo vacío: pie de 0 en la moneda de la cotización; capítulo solo con textos: subtotal de 0, sin inventar importes para los textos.
  • Arrastre por ratón, táctil y teclado mueve el capítulo completo; arrastre de detalles entre capítulos y hacia capítulo vacío mantiene pertenencia válida.
  • Duplicado y borrado individual o múltiple no procesan hijos dos veces, no reutilizan identidades de capítulo y no dejan huérfanos.
  • La API rechaza pertenencia desconocida, de otra cotización, capítulos duplicados y combinación de detalles libres con capítulos; una actualización inválida no cambia datos persistidos.
  • Un fallo al escribir capítulos o detalles revierte toda la transacción.
  • PDF sin capítulos y PDF con capítulos en ambos modos; combinar con descuentos visibles/ocultos, moneda adicional e idiomas documentales. Probar líneas con precios distintivos para verificar su ausencia real en el HTML sin desglose.
  • PDF largo: ningún contenido desaparece; pies y encabezados quedan legibles tras los saltos de página.

Validación realizada y hallazgos previos

Entrega de fase 5

  • ReportQuote.reporter.ts: proyección ordenada de cabeceras, detalles y pies. Los subtotales proceden de Quote.getChapterSubtotal, después del descuento de línea y antes del descuento general e impuestos. Sin capítulos se ignora show_chapter_line_amounts. Con capítulos y preferencia desactivada, los importes individuales tampoco se incorporan a la proyección del informe.
  • template.hbs: una sola tabla para ambos modos. Cabeceras numeradas, pies con subtotal y texto escapado; modo sin desglose con únicamente cantidad y descripción, sin columnas económicas vacías. El resumen conserva descuentos globales, impuestos y moneda adicional. Se mantienen las traducciones de artículos y sus descripciones de respaldo.
  • Etiqueta de subtotal de capítulo en los idiomas documentales es, en y fr, incluida en la validación de etiquetas requeridas. No se amplía el selector de idiomas documentales.
  • Impresión: cabecera de tabla repetida, títulos unidos al primer detalle y pies al último cuando caben; capítulos largos y bloques de texto pueden continuar en otras páginas. Colores de capítulo preservados al imprimir, importes sin partir y resumen económico sin división entre páginas cuando cabe.
  • Correcciones necesarias en el reporter: eliminar la espera de navegación tras setContent y cerrar Chromium en finally; excluir líneas de texto del acumulador de descuentos para evitar mezclar monedas nulas; imprimir el porcentaje de descuento general como cadena formateada y mostrar la moneda base real en la leyenda de cambio, que antes decía siempre 1 EUR.
  • Pruebas automatizadas: compatibilidad sin capítulos, visibilidad de importes y descuentos, orden y subtotales, capítulos vacíos y de texto, EUR/USD y moneda adicional, idiomas, escape de títulos/descripciones, traducción de artículos y cierre del navegador tanto en éxito como en fallo.
  • Validación: cd server && yarn test --runInBand src/contexts/sales (10 suites, 72 pruebas), yarn tsc --noEmit --pretty false, ESLint acotado al reporter y sus pruebas y git diff --check. ESLint usa rutas de parser corregidas por argumento para producción y project: null para tests, excluidos por el tsconfig heredado; los tests sí comprueban tipos mediante ts-jest. No se cambia la configuración de lint en esta fase.
  • PDF reales generados mediante ReportQuotePresenter.toPDF: sin capítulos (1 página), con desglose (1), sin desglose (1), capítulo de 90 líneas en ambos modos (9 páginas por modo), doce capítulos en límites de página (7) y bloque de texto de 150 líneas (5). Comprobación de texto con pdftotext: todas las líneas una sola vez, cabeceras repetidas en páginas con detalles, doce títulos junto al primer detalle y doce pies junto al último. Inspección visual de la muestra con desglose y de la última página con varios capítulos.
  • Muestras temporales: /tmp/uecko-chapter-pdf/, generadas sin iniciar la API ni conectar a MariaDB. Para una verificación reproducible sin red se sustituyeron las peticiones al CSS externo por el CSS Tailwind ya compilado en dist/client y la imagen del pie por su copia local; no se modificó dist. La plantilla de producción conserva su dependencia previa del CDN de Tailwind 2 y del pie remoto; estas muestras no certifican la disponibilidad de esos recursos ni equivalencia visual exacta con ese CDN.
  • No se ejecutan E2E contra la aplicación, operaciones de base de datos/Docker, instalaciones ni builds de distribución. El guardado real y la integración completa se reservan para fase 6 tras aplicar y comprobar el esquema en un entorno autorizado. Persiste una incoherencia previa ajena a capítulos: el bloque de condiciones usa quote.default_legal_terms, aunque el reporter expone dealer.default_legal_terms; no se modifica en esta fase.

Entrega de fase 4

  • Nuevo client/src/app/quotes/chapterEditor.ts: operaciones sobre capítulos y detalles, orden, duplicado sin doble procesamiento de hijos, traslado, borrado, desagrupación y cálculo de pies con MoneyValue. El guardado elimina de los capítulos sus subtotales de respuesta y de las líneas su descripción localizada de solo lectura.
  • Nuevo hook useChapterEditor.ts: lectura de valores actuales de React Hook Form, capítulo activo, destino capturado al abrir selectores, aplicación de cambios con aviso de formulario modificado y foco del título al crear un capítulo. Los identificadores persistidos del capítulo no se confunden con los internos de useFieldArray.
  • Nuevos QuoteChapterCreateDialog.tsx y QuoteChaptersDataTable.tsx: alta con título obligatorio, cabeceras numeradas, detalles con las celdas existentes, pies no editables, arrastre de unidades completas y detalles hacia capítulos vacíos o con contenido, selección, duplicado y diálogos de borrado/traslado. La selección mixta deshabilita el arrastre conjunto y evita procesar dos veces los hijos de un capítulo seleccionado.
  • QuoteDetailsCardEditor.tsx integra ambos modos y conserva la tabla anterior para cotizaciones sin capítulos. El botón «Añadir capítulo» aparece en su barra de herramientas. Filas nuevas, artículos y bloques respetan el capítulo activo o el destino capturado al abrir el selector.
  • La barra de herramientas, acciones de fila, filas arrastrables y useDetailColumns.tsx reutilizan los componentes existentes. Se ha corregido el evento de selección de Radix Checkbox; las columnas conservan sus celdas durante la escritura y se actualizan cuando cambia la moneda o el idioma.
  • edit.tsx conserva defaults compatibles, serializa la estructura y guarda la preferencia PDF. El watcher se suscribe desde el inicio para recalcular también la primera modificación; subtotal y descuento se comparan con sus campos correspondientes. Al cargar se recupera la moneda/idioma reales y al cambiar la moneda se mantienen los valores numéricos de las líneas bajo la nueva moneda, sin añadir conversión de divisas.
  • client/src/lib/calc.ts conserva la moneda de las líneas y de la cotización y excluye importes nulos, manteniendo el criterio del backend para líneas incompletas y textos. Los pies no entran en el cálculo general.
  • Nuevos textos en es, en, ca y fr: títulos, acciones, confirmaciones, preferencia PDF y mensajes de accesibilidad del arrastre.
  • La preferencia «Mostrar importes de los detalles en el PDF» queda guardada, pero su efecto en la plantilla pertenece a la fase 5. No se ha modificado todavía el reporter ni la plantilla.

Validaciones de fase 4:

  • Seis suites del editor y operaciones: 27 pruebas correctas, incluidas 12 pruebas de integración React Hook Form con componentes reales de capítulos. Cubren creación/cancelación, título obligatorio, subtotales en vivo y mantenimiento del foco, movimiento de bloques, traslado a capítulo vacío, duplicado de selección, inserción en extremos, borrado/traslado, regreso al modo libre, preferencia y payload compatible con el validador compartido. El arrastre, sus destinos de teclado y sus colisiones se simulan; no son E2E en navegador.
  • Traducciones: seis comprobaciones correctas de claves e interpolaciones, incluidas las de capítulos en catalán. La suite completa de recursos se ejecutó y conserva un fallo previo de bloques franceses: falta CAP_SEP_AMBIENTES y aparecen CAP_INSTALACION/CAP_SUMINISTRO respecto a español e inglés. Esos ficheros de bloques no se modifican en esta fase. La ejecución acotada de claves/textos excluye las dos comprobaciones de bloques.
  • cd client && yarn tsc --noEmit --pretty false correcto. ESLint acotado sin errores; permanecen advertencias del código anterior por any y contexto exportado junto a componentes. La configuración tenía una severidad inválida "none": se cambia por "off" en client/.eslintrc.cjs para poder ejecutar ESLint.
  • Se sustituyó el import de FormLabel desde el barrel global en la barra por Label de UI, con su asociación al switch. Esto evita cargar indirectamente react-pdf en su prueba y resuelve el fallo previo de Jest con pdfjs-dist en esa suite.
  • No se instalaron dependencias: los tests de formulario usan React DOM, porque falta @testing-library/dom en el entorno. No se ejecutaron E2E ni inspección visual: el navegador Chromium de Playwright no está instalado. No se arrancó el backend ni se aplicó el esquema; el guardado/recarga se comprueba mediante payload, validador y reset del formulario, junto a las pruebas de persistencia de fase 3.
  • No se ejecutó el build de distribución para evitar recrear dist/client; se comprobó TypeScript sin emitir. Tampoco se ejecutó lint global con --max-warnings 0, dado el historial de advertencias fuera de estos cambios.

Entrega de fase 3

  • Modelos quote.model.ts, quoteItem.model.ts y nuevo quoteChapter.model.ts: capítulos asociados a cotización, posiciones, pertenencia nullable e índice de detalles, preferencia PDF con default true. Barrel de modelos actualizado.
  • Nuevo quoteChapter.mapper.ts y ampliación de mappers de cotización y detalle: lectura de capítulos ordenados, validación del propietario, pertenencias y preferencia. La escritura genera posiciones consecutivas y orden canónico por capítulos, conservando el orden interior de detalles y textos.
  • Quote.repository.ts: creación y reemplazo usan una transacción obligatoria; guardan cabecera, eliminan detalles antes que capítulos e insertan capítulos antes que detalles. La lectura carga ambas asociaciones por separado y con orden explícito; la búsqueda de última cotización conserva también capítulos y transacción.
  • Crear y actualizar reconstruyen entidades de capítulo mediante createQuoteChapters.ts, validan la estructura incluso al invocarse sin controlador y asignan pertenencias a las líneas. Los casos antiguos sin capítulos siguen siendo válidos.
  • La actualización carga la cotización con bloqueo FOR UPDATE dentro de la transacción de escritura; rechaza omitir capítulos si el registro ya está agrupado. La desagrupación exige una estructura explícita sin pertenencias. Omitir la preferencia PDF mantiene el valor persistido.
  • Duplicar genera nuevos capítulos y detalles, reasigna pertenencias a la cotización nueva y conserva títulos, orden, importes y preferencia PDF. Se comprueba el fallo del resultado de dominio antes de persistir la copia.
  • Pruebas de fase 3: casos de uso de creación, consulta, actualización y duplicado; mapeo en ambos sentidos; reemplazo y creación transaccionales; propagación de fallos en capítulos y detalles; metadatos y asociaciones Sequelize. El rollback se verifica con un doble transaccional y la clase de transacción existente, sin conexión a MariaDB.
  • Validación: cd server && yarn test --runInBand src/contexts/sales (9 suites, 56 pruebas); cd shared && yarn test --runInBand --runTestsByPath lib/contexts/sales/application/dto/Quote/QuoteChapter.dto.test.ts (27 pruebas). TypeScript del servidor con yarn tsc --noEmit --pretty false; ESLint acotado a los archivos ejecutables de fase 3 usando las rutas de configuración corregidas por argumento, sin cambiar la configuración del repositorio.
  • No se ejecutan backend, Docker, DDL, migraciones, instalaciones, builds de distribución ni E2E. La comprobación de esquema, FK compuesta, bloqueo y rollback real permanece documentada en doc/capitulos-esquema.md para integración autorizada. No hay cambios nuevos en la interfaz que requieran repetir pruebas del cliente en esta fase.
  • Diferencia relevante de infraestructura: Sequelize no instala la FK compuesta de pertenencia desde los modelos definidos; la validación de aplicación sí está implementada. La restricción SQL y su convivencia con sync({ alter: true }) quedan preparadas para revisión explícita antes de aplicar el esquema.

Entrega de fase 2

  • Dominio: nueva entidad QuoteChapter, con identidad, cotización propietaria y descripción obligatoria; QuoteItem.chapterId; Quote.chapters, showChapterLineAmounts y getChapterSubtotal. Se rechazan duplicados, capítulos de otra cotización y pertenencias inválidas. Sin capítulos se conservan los valores por defecto antiguos.
  • Contratos: QuoteChapter.dto.ts, DTOs de crear, consultar, actualizar y duplicar y sus barrel exports. Los requests admiten omitir los campos para compatibilidad y no introducen defaults que confundan omisión con desagrupación explícita. Las respuestas incluyen capítulos con subtotal calculado, pertenencia nullable y preferencia PDF.
  • Validadores de crear/actualizar: verifican estructura, UUID v4, títulos, unicidad, pertenencia y preferencia booleana. Rechazan datos de subtotal o anidación dentro de capítulos enviados como entrada. La comprobación de PUT antiguo frente a una cotización ya agrupada requiere cargar el registro y corresponde a la fase 3.
  • Los cuatro presentadores API se adaptan ahora para satisfacer los contratos de respuesta requeridos. No hay cambios de esquema, repositorio ni casos de uso: estos todavía no reconstruyen ni guardan capítulos. Esta fase no constituye una versión desplegable de la funcionalidad completa.
  • Los textos sin importe se excluyen de la suma general, al igual que de la suma de capítulos. Esto evita intentar sumar importes nulos en la moneda por defecto a una cotización de otra moneda.
  • Tests de contratos en shared: 27 pruebas correctas. Tests de dominio y regresión en servidor: 24 pruebas correctas; tests de los cuatro presentadores: 8 pruebas correctas.
  • TypeScript de servidor y cliente: yarn tsc --noEmit --pretty false correcto en ambos paquetes.
  • ESLint acotado a los archivos ejecutables modificados del servidor: sin errores y con tres advertencias previas por parámetros context sin usar en presentadores. Para ejecutarlo se usaron --ignore-path ../.gitignore y un tsconfigRootDir absoluto mediante --parser-options: la configuración habitual intenta leer rutas relativas inexistentes desde server/. No se modifica esa configuración fuera del alcance.
  • El test nuevo de shared importa las funciones de @jest/globals explícitamente: en este entorno shared/node_modules/@types no existe y el typeRoots del paquete impide resolver los globales Jest automáticamente. No se instalaron dependencias ni se cambió la configuración global de pruebas.
  • git diff --check correcto. No se ejecutan builds de distribución, lint global, E2E ni operaciones de infraestructura: se comprueban tipos sin emitir y pruebas acotadas; la validación de esquema y flujos completos corresponde a fases posteriores.

Auditoría y fase 1

Validación del cierre de fase 1: revisión de coherencia entre decisiones funcionales, tabla del editor, comportamiento PDF y escenarios F1-01 a F1-10. git diff --no-index --check /dev/null doc/capitulos-cotizaciones.md correcto. No se repiten tests, lint, builds ni E2E porque esta fase solo modifica la especificación; los resultados siguientes corresponden a la auditoría inicial y no verifican capítulos implementados.

Estado inicial: git status --short limpio. Auditoría estática de especificación, rutas, dominio, DTOs, persistencia, editor, cálculos, traducciones y exportación.

Comandos ejecutados desde cada paquete:

# server
yarn test --runInBand --runTestsByPath src/contexts/sales/application/Quote/UpdateQuote.useCase.test.ts src/contexts/sales/infrastructure/mappers/quote.mapper.test.ts src/contexts/sales/infrastructure/express/controllers/quotes/reportQuote/reporter/ReportQuote.reporter.test.ts

# client
yarn test --runInBand --runTestsByPath src/app/quotes/components/QuoteItemsSortableDataTable.test.tsx src/app/quotes/components/QuoteItemsSortableDataTableToolbar.test.tsx src/app/quotes/components/QuoteItemsInlineRowActions.test.tsx
  • Servidor: 3 suites y 12 pruebas correctas. Son pruebas con dobles de repositorio/mappers y renderizado HTML; no validan una base real ni generan PDF.
  • Cliente: 2 suites correctas, 4 pruebas correctas; la suite de la barra de herramientas falla antes de ejecutar sus pruebas porque Jest no transforma el ESM de pdfjs-dist, importado indirectamente desde el barrel de componentes y react-pdf. Fallo previo a esta propuesta; no se ha modificado su configuración.
  • No se ejecutaron builds ni lint: la entrega solo incorpora documentación y no cambia código ejecutable. No se ejecutó la batería completa.
  • No se ejecutaron E2E, backend, Docker ni operaciones de base de datos: requieren infraestructura fuera del alcance de esta auditoría. No se instaló ninguna dependencia.

Inconsistencias observadas, sin cambios fuera del alcance:

  • AGENTS.md indica que no se localizaron tests de servidor ni E2E; el repositorio actual sí contiene ambos y scripts Playwright en el paquete raíz.
  • La interfaz carga es/en/ca/fr, mientras los selectores de idioma documental y el reporter admiten es/en/fr. Añadir textos de interfaz en los cuatro recursos; no asumir PDF catalán implementado.
  • Los DTOs de línea tienen diferencias previas: crear declara price, actualizar/obtener usan subtotal_price, y el validador de actualización menciona article_id mientras el contrato y caso de uso usan id_article. Ampliar las pruebas de contrato sin normalizar campos ajenos a esta funcionalidad.
  • El watcher de edit.tsx compara en varios puntos el subtotal o descuento calculado con formData.total_price en lugar del campo correspondiente. Revisar esa ruta al incorporar el recálculo de capítulos para no añadir escrituras redundantes; no se corrige en esta auditoría.

Ampliación: plegado visual del editor

  • QuoteChaptersDataTable.tsx: botón accesible con aria-expanded y etiquetas traducidas en es/en/ca/fr. Estado local por capítulo, vinculado a su identificador; no forma parte del formulario ni de los DTO. Conserva el título, subtotal y acciones del capítulo visibles.
  • QuoteItemsSortableTableRow.tsx: soporte para filas ocultas. Los campos permanecen montados para conservar valores y validación; los detalles y destinos vacíos plegados se excluyen del movimiento de filas. Los capítulos completos siguen pudiendo desplazarse.
  • QuoteChapterEditor.test.tsx: plegar/desplegar sin alterar datos ni payload, conservar campos editados y subtotal, ocultar acciones de fila, mantener plegado al reordenar y desplegar copias de forma independiente.
  • Validación: 22 pruebas de editor y operaciones de capítulos, cinco comprobaciones de traducciones, TypeScript sin emisión y ESLint acotado (dos advertencias previas de contexto/any en la fila reutilizable). La suite de editor emite un aviso de actualización asíncrona de paginación fuera de act; las dos pruebas nuevas de plegado se ejecutan también aisladas y pasan sin ese aviso. No se modifican ni repiten las pruebas de servidor/PDF, ni se ejecutan E2E o builds de distribución: el cambio se limita al estado visual del cliente.