fix: corregir ventas y mejorar catalogos

This commit is contained in:
mono
2026-10-25 18:40:00 -05:00
parent 6da4996cec
commit 9900235250
13 changed files with 274 additions and 9 deletions

View File

@@ -1,7 +1,22 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { ExportImportService } from '@toldo/services'; import { ExportImportService } from '@toldo/services';
import { db } from '@toldo/data';
import { JornadaService } from '@toldo/services';
describe('contratos de operación y respaldo', () => { 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', () => { it('expone la versión actual en las exportaciones', () => {
const result = new ExportImportService().exportAll({ productores: [], productos: [], movimientosInventario: [], ventas: [], ventaLineas: [], gastos: [], ajustes: [], cuadres: [] }); const result = new ExportImportService().exportAll({ productores: [], productos: [], movimientosInventario: [], ventas: [], ventaLineas: [], gastos: [], ajustes: [], cuadres: [] });
expect(result.appVersion).toBe('0.0.2'); expect(result.appVersion).toBe('0.0.2');

View File

@@ -1,4 +1,5 @@
import 'vuetify/styles' import 'vuetify/styles'
import '@mdi/font/css/materialdesignicons.css'
import { createVuetify } from 'vuetify' import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components' import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives' import * as directives from 'vuetify/directives'

View File

@@ -20,8 +20,9 @@
<v-btn @click="crearProductor" color="primary" block>Agregar</v-btn> <v-btn @click="crearProductor" color="primary" block>Agregar</v-btn>
</v-col> </v-col>
</v-row> </v-row>
<v-text-field v-model="busquedaProductores" label="Buscar productor" prepend-inner-icon="mdi-magnify" clearable density="compact" />
<v-list> <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-list>
</v-card-text> </v-card-text>
</v-card> </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="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-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-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 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-list>
</v-card-text> </v-card-text>
</v-card> </v-card>
@@ -72,7 +74,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { db } from '@toldo/data'; import { db } from '@toldo/data';
import { InventarioService, ImportInventarioCsvService, EdicionService } from '@toldo/services'; import { InventarioService, ImportInventarioCsvService, EdicionService } from '@toldo/services';
@@ -101,6 +103,10 @@ const edicionProducto = ref<any>({});
const originalEdicion = ref<any>({}); const originalEdicion = ref<any>({});
const actor = ref(''); const actor = ref('');
const motivo = 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 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 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; } function editarProducto(producto: any) { entidadEdicion.value = 'PRODUCTO'; originalEdicion.value = { ...producto }; edicionProducto.value = { ...producto }; actor.value = ''; motivo.value = ''; dialogoEdicion.value = true; }

View File

@@ -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> <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-row>
<v-col cols="12" md="8"> <v-col cols="12" md="8">
<v-card> <v-card>
<v-card-title>Productos disponibles</v-card-title> <v-card-title>Productos disponibles</v-card-title>
<v-card-text> <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-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 @click="agregarProducto(prod)" class="cursor-pointer" hover>
<v-card-text> <v-card-text>
<div class="text-subtitle-1 font-weight-bold">{{ prod.nombrePresentacion }}</div> <div class="text-subtitle-1 font-weight-bold">{{ prod.nombrePresentacion }}</div>
@@ -31,14 +32,14 @@
<v-list-item-subtitle> <v-list-item-subtitle>
<v-row align="center" class="mt-1"> <v-row align="center" class="mt-1">
<v-col cols="4"> <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>
<v-col cols="4">${{ l.precioUnitario }}</v-col> <v-col cols="4">${{ l.precioUnitario }}</v-col>
<v-col cols="4">${{ l.subtotal }}</v-col> <v-col cols="4">${{ l.subtotal }}</v-col>
</v-row> </v-row>
</v-list-item-subtitle> </v-list-item-subtitle>
<template v-slot:append> <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> </template>
</v-list-item> </v-list-item>
</v-list> </v-list>
@@ -68,6 +69,12 @@ const productos = ref<any[]>([]);
const lineas = ref<any[]>([]); const lineas = ref<any[]>([]);
const formaPago = ref('EFECTIVO'); const formaPago = ref('EFECTIVO');
const jornadaAbierta = ref<any>(null); 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 error = ref('');
const exito = ref(''); const exito = ref('');
const ventas = ref<any[]>([]); const ventas = ref<any[]>([]);
@@ -80,7 +87,8 @@ const motivoVenta = ref('');
const edicionService = new EdicionService(); const edicionService = new EdicionService();
async function cargar() { 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(); jornadaAbierta.value = await jornadaService.getJornadaAbierta();
const ventasDelDia = await ventasService.getVentasDelDia(new Date()); const ventasDelDia = await ventasService.getVentasDelDia(new Date());
ventas.value = await Promise.all(ventasDelDia.map(async (venta) => ({ ...venta, lineas: await ventasService.getLineasPorVenta(venta.id) }))); 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; 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; 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) { function eliminarLinea(i: number) {
lineas.value.splice(i, 1); lineas.value.splice(i, 1);

19
docs/PRUEBAS-MANUALES.md Normal file
View 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
View 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
1 Nombre de producto Nombre Productor precio unitario cantidad
2 Cafe molido 250g Santa Ana 12000 8
3 Cafe molido 500g Santa Ana 22000 6
4 Miel 250g Santa Ana 14000 5
5 Miel 500g Santa Ana 25000 4
6 Galletas de avena Santa Ana 7000 10
7 Granola artesanal Santa Ana 11000 7
8 Chocolate de mesa Santa Ana 9000 8
9 Infusion de hierbas Santa Ana 6500 6
10 Panela organica Santa Ana 5000 12
11 Manjar artesanal Santa Ana 13000 3
12 Cafe tostado 250g La Familia Gonzalez 12500 8
13 Cafe tostado 500g La Familia Gonzalez 23000 6
14 Mermelada de mora La Familia Gonzalez 10000 5
15 Mermelada de guayaba La Familia Gonzalez 10000 4
16 Galletas de coco La Familia Gonzalez 7500 10
17 Brownie artesanal La Familia Gonzalez 8500 7
18 Arequipe casero La Familia Gonzalez 12000 8
19 Salsa de tomate La Familia Gonzalez 9000 6
20 Salsa picante La Familia Gonzalez 8500 12
21 Vinagreta artesanal La Familia Gonzalez 9500 3
22 Queso campesino 250g El Paraiso 11000 8
23 Queso campesino 500g El Paraiso 20000 6
24 Yogur natural 250ml El Paraiso 6000 5
25 Yogur natural 500ml El Paraiso 10000 4
26 Kumis artesanal El Paraiso 9000 10
27 Cuajada fresca El Paraiso 13000 7
28 Mantequilla artesanal El Paraiso 12000 8
29 Suero costeño El Paraiso 10000 6
30 Queso crema El Paraiso 14000 12
31 Ricotta fresca El Paraiso 15000 3
32 Jabon de avena Manos de la Montaña 8000 8
33 Jabon de cafe Manos de la Montaña 8500 6
34 Jabon de calendula Manos de la Montaña 9000 5
35 Shampoo solido Manos de la Montaña 14000 4
36 Balsamo labial Manos de la Montaña 6500 10
37 Crema corporal Manos de la Montaña 18000 7
38 Aceite corporal Manos de la Montaña 16000 8
39 Sales de baño Manos de la Montaña 12000 6
40 Vela aromatica Manos de la Montaña 15000 12
41 Desodorante natural Manos de la Montaña 11000 3
42 Aretes tejidos Tejidos CESTA 18000 8
43 Pulsera artesanal Tejidos CESTA 10000 6
44 Bolso de fique Tejidos CESTA 45000 5
45 Canasto pequeño Tejidos CESTA 28000 4
46 Monedero tejido Tejidos CESTA 16000 10
47 Camino de mesa Tejidos CESTA 38000 7
48 Individual tejido Tejidos CESTA 12000 8
49 Sombrero artesanal Tejidos CESTA 55000 6
50 Llaveros tejidos Tejidos CESTA 7000 12
51 Muñeca de trapo Tejidos CESTA 22000 3
52 Tomate chonto Huerta La Esperanza 4500 8
53 Zanahoria organica Huerta La Esperanza 4000 6
54 Lechuga crespa Huerta La Esperanza 3500 5
55 Espinaca fresca Huerta La Esperanza 4000 4
56 Cilantro Huerta La Esperanza 2000 10
57 Papa criolla Huerta La Esperanza 5000 7
58 Yuca fresca Huerta La Esperanza 4000 8
59 Platano harton Huerta La Esperanza 3500 6
60 Remolacha Huerta La Esperanza 4500 12
61 Pepino cohombro Huerta La Esperanza 3500 3
62 Jugo de mora Frutos del Valle 7000 8
63 Jugo de mango Frutos del Valle 7000 6
64 Pulpa de guanabana Frutos del Valle 9000 5
65 Pulpa de lulo Frutos del Valle 9000 4
66 Pulpa de maracuya Frutos del Valle 8500 10
67 Nectar de guayaba Frutos del Valle 7500 7
68 Conserva de durazno Frutos del Valle 12000 8
69 Conserva de piña Frutos del Valle 12000 6
70 Fruta deshidratada Frutos del Valle 14000 12
71 Chips de platano Frutos del Valle 8000 3
72 Salsa de ajo Sabores de la Abuela 9500 8
73 Salsa BBQ Sabores de la Abuela 11000 6
74 Salsa de mango Sabores de la Abuela 10000 5
75 Ají casero Sabores de la Abuela 7500 4
76 Mayonesa artesanal Sabores de la Abuela 9000 10
77 Pesto de albahaca Sabores de la Abuela 13000 7
78 Hummus Sabores de la Abuela 12000 8
79 Pasta de aceitunas Sabores de la Abuela 14000 6
80 Aderezo de mostaza Sabores de la Abuela 9000 12
81 Chutney de mango Sabores de la Abuela 12500 3
82 Miel de abejas 250g Apiario El Roble 15000 8
83 Miel de abejas 500g Apiario El Roble 28000 6
84 Polen 100g Apiario El Roble 12000 5
85 Propoleo 30ml Apiario El Roble 13000 4
86 Jalea real Apiario El Roble 18000 10
87 Velas de cera Apiario El Roble 9000 7
88 Jabon de miel Apiario El Roble 8500 8
89 Caramelos de miel Apiario El Roble 7000 6
90 Turron de miel Apiario El Roble 11000 12
91 Hidromiel Apiario El Roble 30000 3
92 Harina de maiz Cooperativa La Cosecha 6000 8
93 Harina de platano Cooperativa La Cosecha 7000 6
94 Frijol cargamanto Cooperativa La Cosecha 9000 5
95 Frijol rojo Cooperativa La Cosecha 8500 4
96 Lenteja Cooperativa La Cosecha 7500 10
97 Arveja seca Cooperativa La Cosecha 8000 7
98 Maiz peto Cooperativa La Cosecha 6500 8
99 Arroz integral Cooperativa La Cosecha 8500 6
100 Quinua Cooperativa La Cosecha 15000 12
101 Amaranto Cooperativa La Cosecha 14000 3
102 Pan de yuca Panaderia El Trigal 4000 8
103 Pan integral Panaderia El Trigal 5000 6
104 Pan de bono Panaderia El Trigal 3500 5
105 Croissant Panaderia El Trigal 4500 4
106 Mogolla integral Panaderia El Trigal 4000 10
107 Torta de zanahoria Panaderia El Trigal 18000 7
108 Torta de chocolate Panaderia El Trigal 20000 8
109 Galleta integral Panaderia El Trigal 3000 6
110 Almojabana Panaderia El Trigal 3500 12
111 Roscon de bocadillo Panaderia El Trigal 7000 3

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-25

View 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.

View 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.

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -7,7 +7,7 @@ export class VentasService {
throw new Error('La venta debe tener cantidades mayores que cero'); throw new Error('La venta debe tener cantidades mayores que cero');
} }
// Validar stock mínimo conceptual - se puede expandir // 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) { for (const linea of lineas) {
const movimientos = await db.movimientosInventario.where('productoId').equals(linea.productoId).toArray(); const movimientos = await db.movimientosInventario.where('productoId').equals(linea.productoId).toArray();
const stock = movimientos.reduce((total, movimiento) => total + movimiento.cantidad, 0); const stock = movimientos.reduce((total, movimiento) => total + movimiento.cantidad, 0);