fix: corregir ventas y mejorar catalogos
This commit is contained in:
@@ -1,7 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ExportImportService } from '@toldo/services';
|
||||
import { db } from '@toldo/data';
|
||||
import { JornadaService } from '@toldo/services';
|
||||
|
||||
describe('contratos de operación y respaldo', () => {
|
||||
it('persiste una venta y su movimiento de inventario dentro de la misma transacción', async () => {
|
||||
await db.delete();
|
||||
await db.open();
|
||||
await db.productos.add({ id: 'product-sale', productorId: 'producer-sale', nombrePresentacion: 'Producto de prueba', precioUnitario: 1000, activo: true, createdAt: new Date() } as any);
|
||||
await db.movimientosInventario.add({ id: 'stock-sale', fecha: new Date(), tipo: 'INGRESO_INICIAL', productoId: 'product-sale', cantidad: 3, createdAt: new Date() } as any);
|
||||
const jornadaService = new JornadaService();
|
||||
const jornadaId = await jornadaService.abrirJornada(['Prueba'], 0);
|
||||
const ventaId = crypto.randomUUID();
|
||||
await jornadaService.registrarVentaEnJornada(jornadaId, { id: ventaId, fecha: new Date(), subtotal: 1000, total: 1000, createdAt: new Date() } as any, [{ id: crypto.randomUUID(), ventaId, productoId: 'product-sale', cantidad: 1, precioUnitario: 1000, subtotal: 1000, formaPago: 'EFECTIVO' } as any]);
|
||||
expect(await db.ventas.get(ventaId)).toBeTruthy();
|
||||
expect((await db.movimientosInventario.toArray()).filter((movimiento) => movimiento.referenciaId === ventaId)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('expone la versión actual en las exportaciones', () => {
|
||||
const result = new ExportImportService().exportAll({ productores: [], productos: [], movimientosInventario: [], ventas: [], ventaLineas: [], gastos: [], ajustes: [], cuadres: [] });
|
||||
expect(result.appVersion).toBe('0.0.2');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'vuetify/styles'
|
||||
import '@mdi/font/css/materialdesignicons.css'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
<v-btn @click="crearProductor" color="primary" block>Agregar</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-text-field v-model="busquedaProductores" label="Buscar productor" prepend-inner-icon="mdi-magnify" clearable density="compact" />
|
||||
<v-list>
|
||||
<v-list-item v-for="p in productores" :key="p.id" :title="p.nombre"><template #append><v-btn icon="mdi-pencil" variant="text" size="small" :aria-label="`Editar ${p.nombre}`" @click="editarProductor(p)" /></template></v-list-item>
|
||||
<v-list-item v-for="p in productoresFiltrados" :key="p.id" :title="p.nombre"><template #append><v-btn icon="mdi-pencil-outline" color="primary" variant="tonal" size="small" :aria-label="`Editar ${p.nombre}`" @click="editarProductor(p)" /></template></v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -35,8 +36,9 @@
|
||||
<v-text-field v-model="nombreProd" label="Nombre/Presentación (ej: Café molido 250gr)" density="compact" class="mb-2"></v-text-field>
|
||||
<v-text-field v-model.number="precioProd" label="Precio unitario" type="number" density="compact" class="mb-2"></v-text-field>
|
||||
<v-btn @click="crearProducto" color="primary" block>Agregar Producto</v-btn>
|
||||
<v-text-field v-model="busquedaProductos" label="Buscar producto" prepend-inner-icon="mdi-magnify" clearable density="compact" class="mt-3" />
|
||||
<v-list class="mt-3">
|
||||
<v-list-item v-for="prod in productos" :key="prod.id" :title="prod.nombrePresentacion" :subtitle="'$' + prod.precioUnitario"><template #append><v-btn icon="mdi-pencil" variant="text" size="small" :aria-label="`Editar ${prod.nombrePresentacion}`" @click="editarProducto(prod)" /></template></v-list-item>
|
||||
<v-list-item v-for="prod in productosFiltrados" :key="prod.id" :title="prod.nombrePresentacion" :subtitle="'$' + prod.precioUnitario"><template #append><v-btn icon="mdi-pencil-outline" color="primary" variant="tonal" size="small" :aria-label="`Editar ${prod.nombrePresentacion}`" @click="editarProducto(prod)" /></template></v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -72,7 +74,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { db } from '@toldo/data';
|
||||
import { InventarioService, ImportInventarioCsvService, EdicionService } from '@toldo/services';
|
||||
|
||||
@@ -101,6 +103,10 @@ const edicionProducto = ref<any>({});
|
||||
const originalEdicion = ref<any>({});
|
||||
const actor = ref('');
|
||||
const motivo = ref('');
|
||||
const busquedaProductores = ref('');
|
||||
const busquedaProductos = ref('');
|
||||
const productoresFiltrados = computed(() => productores.value.filter((productor) => !busquedaProductores.value.trim() || productor.nombre.toLocaleLowerCase().includes(busquedaProductores.value.trim().toLocaleLowerCase())));
|
||||
const productosFiltrados = computed(() => productos.value.filter((producto) => !busquedaProductos.value.trim() || producto.nombrePresentacion.toLocaleLowerCase().includes(busquedaProductos.value.trim().toLocaleLowerCase())));
|
||||
function avisar(texto: string, tipo: 'success' | 'error' = 'error') { mensaje.value = texto; tipoMensaje.value = tipo; }
|
||||
function editarProductor(productor: any) { entidadEdicion.value = 'PRODUCTOR'; originalEdicion.value = { ...productor }; edicionProductor.value = { ...productor }; actor.value = ''; motivo.value = ''; dialogoEdicion.value = true; }
|
||||
function editarProducto(producto: any) { entidadEdicion.value = 'PRODUCTO'; originalEdicion.value = { ...producto }; edicionProducto.value = { ...producto }; actor.value = ''; motivo.value = ''; dialogoEdicion.value = true; }
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
<div class="d-flex flex-wrap align-center justify-space-between mb-4 ga-2"><div><h1 class="text-h4 mb-1">Punto de venta</h1><div class="text-body-2 text-medium-emphasis">Toca un producto para agregarlo al carrito</div></div><v-chip :color="jornadaAbierta ? 'success' : 'warning'" variant="tonal" :prepend-icon="jornadaAbierta ? 'mdi-check-circle' : 'mdi-alert'">{{ jornadaAbierta ? 'Jornada abierta' : 'Abre una jornada' }}</v-chip></div>
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<v-card>
|
||||
<v-card>
|
||||
<v-card-title>Productos disponibles</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="busqueda" label="Buscar por producto o productor" prepend-inner-icon="mdi-magnify" clearable density="compact" class="mb-3" />
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-for="prod in productos" :key="prod.id">
|
||||
<v-col cols="12" sm="6" md="4" v-for="prod in productosFiltrados" :key="prod.id">
|
||||
<v-card @click="agregarProducto(prod)" class="cursor-pointer" hover>
|
||||
<v-card-text>
|
||||
<div class="text-subtitle-1 font-weight-bold">{{ prod.nombrePresentacion }}</div>
|
||||
@@ -31,14 +32,14 @@
|
||||
<v-list-item-subtitle>
|
||||
<v-row align="center" class="mt-1">
|
||||
<v-col cols="4">
|
||||
<v-text-field v-model.number="l.cantidad" type="number" density="compact" hide-details @update:model-value="actualizarLinea(i)"></v-text-field>
|
||||
<div class="d-flex align-center justify-center ga-1"><v-btn icon="mdi-minus" size="small" variant="tonal" aria-label="Disminuir cantidad" @click="cambiarCantidad(i, -1)" /><span class="quantity-value">{{ l.cantidad }}</span><v-btn icon="mdi-plus" size="small" variant="tonal" aria-label="Aumentar cantidad" @click="cambiarCantidad(i, 1)" /></div>
|
||||
</v-col>
|
||||
<v-col cols="4">${{ l.precioUnitario }}</v-col>
|
||||
<v-col cols="4">${{ l.subtotal }}</v-col>
|
||||
</v-row>
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-btn icon="mdi-delete" size="small" @click="eliminarLinea(i)"></v-btn>
|
||||
<v-btn icon="mdi-delete-outline" color="error" variant="tonal" size="small" aria-label="Eliminar producto" @click="eliminarLinea(i)"></v-btn>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
@@ -68,6 +69,12 @@ const productos = ref<any[]>([]);
|
||||
const lineas = ref<any[]>([]);
|
||||
const formaPago = ref('EFECTIVO');
|
||||
const jornadaAbierta = ref<any>(null);
|
||||
const busqueda = ref('');
|
||||
const productosFiltrados = computed(() => {
|
||||
const termino = busqueda.value.trim().toLocaleLowerCase();
|
||||
if (!termino) return productos.value;
|
||||
return productos.value.filter((producto) => `${producto.nombrePresentacion} ${producto.productorNombre ?? ''}`.toLocaleLowerCase().includes(termino));
|
||||
});
|
||||
const error = ref('');
|
||||
const exito = ref('');
|
||||
const ventas = ref<any[]>([]);
|
||||
@@ -80,7 +87,8 @@ const motivoVenta = ref('');
|
||||
const edicionService = new EdicionService();
|
||||
|
||||
async function cargar() {
|
||||
productos.value = await db.productos.toArray();
|
||||
const productores = await db.productores.toArray();
|
||||
productos.value = (await db.productos.toArray()).map((producto) => ({ ...producto, productorNombre: productores.find((productor) => productor.id === producto.productorId)?.nombre ?? '' }));
|
||||
jornadaAbierta.value = await jornadaService.getJornadaAbierta();
|
||||
const ventasDelDia = await ventasService.getVentasDelDia(new Date());
|
||||
ventas.value = await Promise.all(ventasDelDia.map(async (venta) => ({ ...venta, lineas: await ventasService.getLineasPorVenta(venta.id) })));
|
||||
@@ -112,6 +120,7 @@ function actualizarLinea(i: number) {
|
||||
if (!Number.isFinite(lineas.value[i].cantidad) || lineas.value[i].cantidad < 1) lineas.value[i].cantidad = 1;
|
||||
lineas.value[i].subtotal = lineas.value[i].cantidad * lineas.value[i].precioUnitario;
|
||||
}
|
||||
function cambiarCantidad(i: number, cambio: number) { lineas.value[i].cantidad = Math.max(1, lineas.value[i].cantidad + cambio); actualizarLinea(i); }
|
||||
|
||||
function eliminarLinea(i: number) {
|
||||
lineas.value.splice(i, 1);
|
||||
|
||||
19
docs/PRUEBAS-MANUALES.md
Normal file
19
docs/PRUEBAS-MANUALES.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Pruebas manuales
|
||||
|
||||
## Importar inventario de prueba
|
||||
|
||||
El archivo `docs/inventario-prueba.csv` contiene 11 productores y 110 productos, con cantidades iniciales para probar el catálogo grande.
|
||||
|
||||
1. Abrir `Inventario`.
|
||||
2. Seleccionar `docs/inventario-prueba.csv`.
|
||||
3. Revisar la previsualización.
|
||||
4. Seleccionar `Confirmar importación`.
|
||||
5. Usar el campo de búsqueda de productores y productos para comprobar que el catálogo se puede filtrar.
|
||||
6. Ir a `Ventas POS` y buscar productos por nombre.
|
||||
7. Agregar un producto, usar los botones `+` y `-`, eliminarlo y confirmar una venta.
|
||||
|
||||
El CSV admite coma o punto y coma como separador. La comparación de nombres ignora mayúsculas, tildes y espacios redundantes.
|
||||
|
||||
## Verificar actualización Android
|
||||
|
||||
La APK se identifica con `applicationId com.ilusion.toldoconjunto`. Cada nueva APK debe aumentar `versionCode` y conservar la misma firma para actualizar la instalación existente.
|
||||
111
docs/inventario-prueba.csv
Normal file
111
docs/inventario-prueba.csv
Normal file
@@ -0,0 +1,111 @@
|
||||
Nombre de producto,Nombre Productor,precio unitario,cantidad
|
||||
Cafe molido 250g,Santa Ana,12000,8
|
||||
Cafe molido 500g,Santa Ana,22000,6
|
||||
Miel 250g,Santa Ana,14000,5
|
||||
Miel 500g,Santa Ana,25000,4
|
||||
Galletas de avena,Santa Ana,7000,10
|
||||
Granola artesanal,Santa Ana,11000,7
|
||||
Chocolate de mesa,Santa Ana,9000,8
|
||||
Infusion de hierbas,Santa Ana,6500,6
|
||||
Panela organica,Santa Ana,5000,12
|
||||
Manjar artesanal,Santa Ana,13000,3
|
||||
Cafe tostado 250g,La Familia Gonzalez,12500,8
|
||||
Cafe tostado 500g,La Familia Gonzalez,23000,6
|
||||
Mermelada de mora,La Familia Gonzalez,10000,5
|
||||
Mermelada de guayaba,La Familia Gonzalez,10000,4
|
||||
Galletas de coco,La Familia Gonzalez,7500,10
|
||||
Brownie artesanal,La Familia Gonzalez,8500,7
|
||||
Arequipe casero,La Familia Gonzalez,12000,8
|
||||
Salsa de tomate,La Familia Gonzalez,9000,6
|
||||
Salsa picante,La Familia Gonzalez,8500,12
|
||||
Vinagreta artesanal,La Familia Gonzalez,9500,3
|
||||
Queso campesino 250g,El Paraiso,11000,8
|
||||
Queso campesino 500g,El Paraiso,20000,6
|
||||
Yogur natural 250ml,El Paraiso,6000,5
|
||||
Yogur natural 500ml,El Paraiso,10000,4
|
||||
Kumis artesanal,El Paraiso,9000,10
|
||||
Cuajada fresca,El Paraiso,13000,7
|
||||
Mantequilla artesanal,El Paraiso,12000,8
|
||||
Suero costeño,El Paraiso,10000,6
|
||||
Queso crema,El Paraiso,14000,12
|
||||
Ricotta fresca,El Paraiso,15000,3
|
||||
Jabon de avena,Manos de la Montaña,8000,8
|
||||
Jabon de cafe,Manos de la Montaña,8500,6
|
||||
Jabon de calendula,Manos de la Montaña,9000,5
|
||||
Shampoo solido,Manos de la Montaña,14000,4
|
||||
Balsamo labial,Manos de la Montaña,6500,10
|
||||
Crema corporal,Manos de la Montaña,18000,7
|
||||
Aceite corporal,Manos de la Montaña,16000,8
|
||||
Sales de baño,Manos de la Montaña,12000,6
|
||||
Vela aromatica,Manos de la Montaña,15000,12
|
||||
Desodorante natural,Manos de la Montaña,11000,3
|
||||
Aretes tejidos,Tejidos CESTA,18000,8
|
||||
Pulsera artesanal,Tejidos CESTA,10000,6
|
||||
Bolso de fique,Tejidos CESTA,45000,5
|
||||
Canasto pequeño,Tejidos CESTA,28000,4
|
||||
Monedero tejido,Tejidos CESTA,16000,10
|
||||
Camino de mesa,Tejidos CESTA,38000,7
|
||||
Individual tejido,Tejidos CESTA,12000,8
|
||||
Sombrero artesanal,Tejidos CESTA,55000,6
|
||||
Llaveros tejidos,Tejidos CESTA,7000,12
|
||||
Muñeca de trapo,Tejidos CESTA,22000,3
|
||||
Tomate chonto,Huerta La Esperanza,4500,8
|
||||
Zanahoria organica,Huerta La Esperanza,4000,6
|
||||
Lechuga crespa,Huerta La Esperanza,3500,5
|
||||
Espinaca fresca,Huerta La Esperanza,4000,4
|
||||
Cilantro,Huerta La Esperanza,2000,10
|
||||
Papa criolla,Huerta La Esperanza,5000,7
|
||||
Yuca fresca,Huerta La Esperanza,4000,8
|
||||
Platano harton,Huerta La Esperanza,3500,6
|
||||
Remolacha,Huerta La Esperanza,4500,12
|
||||
Pepino cohombro,Huerta La Esperanza,3500,3
|
||||
Jugo de mora,Frutos del Valle,7000,8
|
||||
Jugo de mango,Frutos del Valle,7000,6
|
||||
Pulpa de guanabana,Frutos del Valle,9000,5
|
||||
Pulpa de lulo,Frutos del Valle,9000,4
|
||||
Pulpa de maracuya,Frutos del Valle,8500,10
|
||||
Nectar de guayaba,Frutos del Valle,7500,7
|
||||
Conserva de durazno,Frutos del Valle,12000,8
|
||||
Conserva de piña,Frutos del Valle,12000,6
|
||||
Fruta deshidratada,Frutos del Valle,14000,12
|
||||
Chips de platano,Frutos del Valle,8000,3
|
||||
Salsa de ajo,Sabores de la Abuela,9500,8
|
||||
Salsa BBQ,Sabores de la Abuela,11000,6
|
||||
Salsa de mango,Sabores de la Abuela,10000,5
|
||||
Ají casero,Sabores de la Abuela,7500,4
|
||||
Mayonesa artesanal,Sabores de la Abuela,9000,10
|
||||
Pesto de albahaca,Sabores de la Abuela,13000,7
|
||||
Hummus,Sabores de la Abuela,12000,8
|
||||
Pasta de aceitunas,Sabores de la Abuela,14000,6
|
||||
Aderezo de mostaza,Sabores de la Abuela,9000,12
|
||||
Chutney de mango,Sabores de la Abuela,12500,3
|
||||
Miel de abejas 250g,Apiario El Roble,15000,8
|
||||
Miel de abejas 500g,Apiario El Roble,28000,6
|
||||
Polen 100g,Apiario El Roble,12000,5
|
||||
Propoleo 30ml,Apiario El Roble,13000,4
|
||||
Jalea real,Apiario El Roble,18000,10
|
||||
Velas de cera,Apiario El Roble,9000,7
|
||||
Jabon de miel,Apiario El Roble,8500,8
|
||||
Caramelos de miel,Apiario El Roble,7000,6
|
||||
Turron de miel,Apiario El Roble,11000,12
|
||||
Hidromiel,Apiario El Roble,30000,3
|
||||
Harina de maiz,Cooperativa La Cosecha,6000,8
|
||||
Harina de platano,Cooperativa La Cosecha,7000,6
|
||||
Frijol cargamanto,Cooperativa La Cosecha,9000,5
|
||||
Frijol rojo,Cooperativa La Cosecha,8500,4
|
||||
Lenteja,Cooperativa La Cosecha,7500,10
|
||||
Arveja seca,Cooperativa La Cosecha,8000,7
|
||||
Maiz peto,Cooperativa La Cosecha,6500,8
|
||||
Arroz integral,Cooperativa La Cosecha,8500,6
|
||||
Quinua,Cooperativa La Cosecha,15000,12
|
||||
Amaranto,Cooperativa La Cosecha,14000,3
|
||||
Pan de yuca,Panaderia El Trigal,4000,8
|
||||
Pan integral,Panaderia El Trigal,5000,6
|
||||
Pan de bono,Panaderia El Trigal,3500,5
|
||||
Croissant,Panaderia El Trigal,4500,4
|
||||
Mogolla integral,Panaderia El Trigal,4000,10
|
||||
Torta de zanahoria,Panaderia El Trigal,18000,7
|
||||
Torta de chocolate,Panaderia El Trigal,20000,8
|
||||
Galleta integral,Panaderia El Trigal,3000,6
|
||||
Almojabana,Panaderia El Trigal,3500,12
|
||||
Roscon de bocadillo,Panaderia El Trigal,7000,3
|
||||
|
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-25
|
||||
23
openspec/changes/fix-sale-transaction-catalog-ux/design.md
Normal file
23
openspec/changes/fix-sale-transaction-catalog-ux/design.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
La venta escribe ventas, líneas y movimientos. La transacción existente sólo declaraba las dos primeras tablas. La interfaz usa nombres MDI sin cargar la fuente y muestra listas completas sin filtro.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** garantizar atomicidad, visibilidad de acciones y búsqueda rápida sin backend.
|
||||
|
||||
**Non-Goals:** paginación remota o rediseño completo del modelo de inventario.
|
||||
|
||||
## Decisions
|
||||
|
||||
- Incluir `movimientosInventario` en el `db.transaction` de ventas.
|
||||
- Cargar la fuente CSS MDI y usar botones Vuetify explícitos para aumentar, disminuir y eliminar.
|
||||
- Filtrar listas localmente por texto normalizado, manteniendo el almacenamiento offline.
|
||||
- Mantener un CSV versionado en `docs` para reproducir pruebas manuales.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Filtrar en memoria es suficiente para la POC y evita dependencias; si el catálogo crece mucho deberá migrarse a índices/paginación.
|
||||
- El CSV de prueba aumenta el repositorio, pero permite repetir validaciones de UX y carga.
|
||||
30
openspec/changes/fix-sale-transaction-catalog-ux/proposal.md
Normal file
30
openspec/changes/fix-sale-transaction-catalog-ux/proposal.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
Registrar una venta falla cuando el movimiento de inventario se escribe fuera del alcance de la transacción Dexie. Además, catálogos grandes requieren búsqueda, controles de cantidad visibles e iconos funcionales para operar con rapidez.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Incluir inventario en la transacción de venta.
|
||||
- Activar los iconos MDI correctamente.
|
||||
- Sustituir edición numérica incómoda por controles `+` y `-` y borrado visible.
|
||||
- Añadir búsqueda de productos/productores en POS e Inventario.
|
||||
- Documentar un CSV de prueba grande.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `sale-transaction-integrity`: venta e inventario persisten atómicamente.
|
||||
- `catalog-large-list-ux`: búsqueda y controles adecuados para catálogos grandes.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `toldo-poc`: mejora la experiencia de venta e inventario.
|
||||
|
||||
## Impact
|
||||
|
||||
- `packages/services/src/ventas/VentasService.ts`.
|
||||
- Vistas POS e Inventario y configuración Vuetify.
|
||||
- Tests, documentación de pruebas y APK.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Permite operar catálogos con muchos productos sin perder orientación ni precisión al modificar cantidades o eliminar líneas.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Controles visibles del carrito
|
||||
The system SHALL provide visible plus, minus, and delete controls for each sale line.
|
||||
|
||||
#### Scenario: Ajustar cantidad
|
||||
- **WHEN** user selects plus or minus
|
||||
- **THEN** quantity changes by one and never falls below one
|
||||
|
||||
### Requirement: Búsqueda de catálogo
|
||||
The system SHALL filter products and producers by a visible search field without requiring network access.
|
||||
|
||||
#### Scenario: Buscar producto
|
||||
- **WHEN** user enters a product or producer fragment
|
||||
- **THEN** only matching catalog entries are shown
|
||||
@@ -0,0 +1,16 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Garantiza que confirmar una venta actualice ventas e inventario como una sola operación local, evitando datos parciales y errores de IndexedDB.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Persistencia atómica de venta
|
||||
The system SHALL persist the sale, sale lines, and inventory movements in one local transaction.
|
||||
|
||||
#### Scenario: Venta confirmada
|
||||
- **GIVEN** an open day and sufficient stock
|
||||
- **WHEN** user confirms a sale
|
||||
- **THEN** sale, lines, and negative inventory movement are stored
|
||||
- **AND** no object store transaction error is raised
|
||||
12
openspec/changes/fix-sale-transaction-catalog-ux/tasks.md
Normal file
12
openspec/changes/fix-sale-transaction-catalog-ux/tasks.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Integridad y UX
|
||||
|
||||
- [x] 1.1 Incluir movimientos de inventario en la transacción de ventas y verificar con test de jornada real
|
||||
- [x] 1.2 Activar MDI y controles visibles `+`, `-` y eliminar en POS; verificar compilación
|
||||
- [x] 1.3 Añadir búsqueda local en POS e Inventario y documentar CSV grande
|
||||
|
||||
## 2. Verificación
|
||||
|
||||
- [x] 2.1 Ejecutar tests unitarios, `vue-tsc` y Playwright
|
||||
- [x] 2.2 Generar APK actualizado y verificar instalación Android
|
||||
@@ -7,7 +7,7 @@ export class VentasService {
|
||||
throw new Error('La venta debe tener cantidades mayores que cero');
|
||||
}
|
||||
// Validar stock mínimo conceptual - se puede expandir
|
||||
return db.transaction('rw', db.ventas, db.ventaLineas, async () => {
|
||||
return db.transaction('rw', db.ventas, db.ventaLineas, db.movimientosInventario, async () => {
|
||||
for (const linea of lineas) {
|
||||
const movimientos = await db.movimientosInventario.where('productoId').equals(linea.productoId).toArray();
|
||||
const stock = movimientos.reduce((total, movimiento) => total + movimiento.cantidad, 0);
|
||||
|
||||
Reference in New Issue
Block a user