|
| 1 | +# ENCARGO «Verificar un albarán a ciegas» — resultado |
| 2 | + |
| 3 | +Fecha: 2026-08-20 · Rama: `main` y `claude/redesign-pricing-modal-amxuzy` |
| 4 | + |
| 5 | +> **Hay una migración SIN APLICAR**: `supabase/migrations/20260820T1700_recepcion_verificacion_a_ciegas.sql`. |
| 6 | +> El frontend está blindado para funcionar antes y después de aplicarla (ver §Orden de despliegue). |
| 7 | +
|
| 8 | +--- |
| 9 | + |
| 10 | +## 0 · Qué pasó en ALB-00119, con los datos delante |
| 11 | + |
| 12 | +Sesión de IA `9bae61b1-fdba-4fd0-a16e-c8450aec72d1`, leída de producción: |
| 13 | + |
| 14 | +```json |
| 15 | +"document": { "handwritten": true, "supplier_name": null, "doc_number": null, |
| 16 | + "bill_to_name": "LLORENTE", "tax_base_total": 424.27, |
| 17 | + "tax_total": 42.27, "grand_total": 466.54 } |
| 18 | +"lines": [ { "raw_text": "Milanesas pollo", "quantity": 56.57, "unit": "kg", |
| 19 | + "unit_price_net": 7.5, "line_amount": null } ] |
| 20 | +"validation": { "needs_review": true, |
| 21 | + "reasons": ["No se pudo validar por base imponible (faltan importes)", |
| 22 | + "Documento manuscrito", "Confianza de lectura baja"] } |
| 23 | +``` |
| 24 | + |
| 25 | +**La IA lo leyó bien.** Los tres números distintos salen de tres fallos encadenados: |
| 26 | + |
| 27 | +1. **`unit_price_net` sí estaba, `line_amount` no.** El asistente solo prerrellenaba |
| 28 | + el importe desde `line_amount`, así que la casilla salió vacía → `unit_cost = NULL`. |
| 29 | + 424 € de pollo a coste cero. |
| 30 | + |
| 31 | +2. **El artículo se mide en unidades, no en kilos.** `Milanesa de Pollo Rebozado` |
| 32 | + tiene unidad base `ud` (250 g cada una) y cuatro formatos, ninguno en kg. El papel |
| 33 | + dice 56,57 kg; el asistente lo convirtió a Paquetes (6 uds = 1,5 kg): |
| 34 | + 56,57 ÷ 1,5 = 37,71 → **38 paquetes = 228 uds = 57 kg**. Sobran 1,72 uds (0,43 kg). |
| 35 | + La pantalla enseña «56,57 uds» porque pinta `doc_qty` con la etiqueta de la unidad |
| 36 | + base del artículo, sin decir que el papel hablaba de kilos. |
| 37 | + |
| 38 | +3. **La bandera de la IA no llegaba a la recepción**, y por dos sitios a la vez: |
| 39 | + `createGoodsReceipt` se llamaba sin `needsReview`, y aunque se hubiera pasado, |
| 40 | + `receive_goods_receipt` la PISABA con `needs_review = (v_skipped > 0)` — un albarán |
| 41 | + con todas las líneas casadas sale `false` por definición, diga lo que diga la IA. |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## 1 · El tamaño real, con el filtro que faltaba |
| 46 | + |
| 47 | +Las 410 del encargo incluyen líneas de albaranes **anulados o archivados**. Sin ellos: |
| 48 | + |
| 49 | +| | Del encargo | Medido hoy (albarán vivo, no anulado) | |
| 50 | +|---|---:|---:| |
| 51 | +| Líneas de recepción | 831 (dos cuentas) | 744 en Foodint | |
| 52 | +| Marcadas sin confirmar | 410 | **341** | |
| 53 | +| Importe en juego | — | **16.108,21 €** | |
| 54 | +| Cantidad distinta a la del papel | 53 | 53 | |
| 55 | +| Sin coste unitario | 8 | 5 en Foodint | |
| 56 | + |
| 57 | +**Y el aviso amarillo describe mal el problema.** Decía siempre *«Lo emparejó el sistema |
| 58 | +por parecido de nombre, no por código»*. De las 408 marcadas en Foodint, **400 son |
| 59 | +`map_source='unmapped'`** — el sistema no las casó en absoluto — y en toda la base solo |
| 60 | +hay **11 líneas `fuzzy`**. El aviso mandaba a mirar donde no era. Ahora cada línea dice |
| 61 | +lo que le pasa a ella (`unverifiedReason`, con 8 pruebas). |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## 2 · Lo que se ha hecho |
| 66 | + |
| 67 | +### 2.1 · Ver el albarán — `ReceiptOfficeReview.tsx` |
| 68 | + |
| 69 | +El componente ya existía (`ReceiptPhotoViewer`, con zoom, lightbox y visor de PDF) y |
| 70 | +esta pantalla no lo usaba. Ahora: |
| 71 | + |
| 72 | +- **Pantalla ancha**: dos columnas, el papel a la derecha y `sticky` — sigue visible al |
| 73 | + bajar por las líneas. |
| 74 | +- **Móvil**: botón grande *Ver el albarán* (no un icono de 9 px). |
| 75 | + |
| 76 | +Sirve para los dos formatos porque el visor ya los distinguía: ALB-00119 es `00-image.jpg`, |
| 77 | +ALB-00117 es un PDF de Bidfood. |
| 78 | + |
| 79 | +### 2.2 · Corregir cantidad, formato y coste |
| 80 | + |
| 81 | +`adjust_goods_receipt_line` estaba desplegada desde el 13/08 y en 24 h se llamó 2 veces |
| 82 | +frente a 357 peticiones de la pantalla. **No faltaba construirla: no se llegaba a ella.** |
| 83 | +Dos huecos, los dos tapados: |
| 84 | + |
| 85 | +- El editor **solo dejaba cambiar cantidad y coste, no el formato**. Un albarán en kilos |
| 86 | + casado a un formato de 6 unidades no tenía arreglo desde la pantalla — el caso ALB-00119. |
| 87 | + Ahora hay desplegable de formato con la medida en la etiqueta («Paquete (1,5 kg)»). |
| 88 | +- El botón **solo estaba en las líneas resueltas**. Las 341 marcadas son *dudosas*, y una |
| 89 | + dudosa solo ofrecía *Sí, es esta* / *No, es otro artículo*: si el artículo era el |
| 90 | + correcto pero la cantidad estaba mal, no había por dónde. Ahora hay un tercer botón, |
| 91 | + **Corregir cantidad, formato o precio**. |
| 92 | + |
| 93 | +El editor enseña, mientras escribes, **lo que va a entrar al almacén** y el total. |
| 94 | +Exige motivo (`discrepancy_reason`) siempre que se mueva cantidad o formato — corregir |
| 95 | +solo el coste no mueve stock. Escribe **siempre** por `adjust_goods_receipt_line` |
| 96 | +(reverso + reposteo, quién y por qué), nunca por un `UPDATE` (§9.8). |
| 97 | + |
| 98 | +### 2.3 · Proveedor y nº de albarán |
| 99 | + |
| 100 | +Editables en la cabecera, y **obligatorios para cerrar** — en la pantalla y también en |
| 101 | +el servidor (`confirm_goods_receipt`), para que no dependa de que la pantalla se acuerde. |
| 102 | +La exigencia solo aplica al cierre de OFICINA (`recibido` → `confirmado`); un borrador |
| 103 | +manual sigue igual, para no levantar un muro sin puerta donde no se puede arreglar. |
| 104 | + |
| 105 | +Impacto medido: de 114 recepciones confirmadas, **0 sin proveedor y 4 sin nº**. |
| 106 | + |
| 107 | +### 3 · Que no se repita — la migración |
| 108 | + |
| 109 | +`20260820T1700_recepcion_verificacion_a_ciegas.sql`, tres piezas: |
| 110 | + |
| 111 | +| | Qué | |
| 112 | +|---|---| |
| 113 | +| **A** | `_post_goods_receipt_lines(p_receipt_id, p_only_unposted default false)` — postear solo las líneas que nunca han posteado. Sin el parámetro, idéntica a hoy. | |
| 114 | +| **B** | `receive_goods_receipt(p_receipt_id, p_hold default false)` — `p_hold` deja el albarán en `'recibido'` (para que la oficina lo pueda abrir) pero **no postea nada** y marca `needs_review`. Y deja de PISAR una bandera que venga puesta. | |
| 115 | +| **C** | `confirm_goods_receipt` — al cerrar un `'recibido'`, entra lo que aún no había entrado. Para un albarán normal no cambia nada: sus líneas posteables ya tienen movimiento. | |
| 116 | + |
| 117 | +**Por qué se retiene el posteo y no el estado**: un borrador va a `GoodsReceiptForm`, no |
| 118 | +a la pantalla de oficina, y `adjust_goods_receipt_line` exige `status='recibido'`. |
| 119 | +Retener en borrador dejaría el albarán fuera del único sitio donde se puede corregir. |
| 120 | + |
| 121 | +Verificado que nada más en la base llama a esas funciones: solo se mencionan en |
| 122 | +comentarios de `adjust_goods_receipt_line` y `post_pending_receipt_line`. |
| 123 | + |
| 124 | +En el asistente, un albarán retenido: |
| 125 | +- enseña al trabajador los motivos de la IA antes de guardar, |
| 126 | +- cambia el botón a **«Guardar y mandar a oficina»**, |
| 127 | +- y no aprende nada (`learnFromReceipt`, `last_price`, cascada de coste): aprender de un |
| 128 | + albarán que la IA marcó dudoso es exactamente cómo se contamina el catálogo. |
| 129 | + |
| 130 | +### 6 · La lista de las líneas sin confirmar |
| 131 | + |
| 132 | +`UnverifiedLinesPage.tsx`, nueva. Todas las líneas marcadas de la cuenta, **ordenadas por |
| 133 | +importe**, cada una con su albarán detrás a un clic. Filtro por local. Distingue las que |
| 134 | +están en el almacén de las que no entraron. Entrada desde el aviso de la lista de |
| 135 | +recepciones, que ahora es un botón, no un cartel. |
| 136 | + |
| 137 | +--- |
| 138 | + |
| 139 | +## Orden de despliegue |
| 140 | + |
| 141 | +El frontend **no depende** de que la migración esté puesta: |
| 142 | + |
| 143 | +- `hold=false` (todo el tráfico de hoy) llama a `receive_goods_receipt` con **un** |
| 144 | + argumento → funciona antes y después. |
| 145 | +- `hold=true` solo puede ocurrir con la migración puesta; si no lo está, el error se |
| 146 | + explica en castellano y **la recepción se queda en borrador**: con todo lo que escribió |
| 147 | + el trabajador guardado y sin nada posteado. Recepcionar es lo que estuvo 6 días roto; |
| 148 | + no se vuelve a romper por un orden de despliegue. |
| 149 | + |
| 150 | +Después de aplicar la migración, `needs_review` de la IA empieza a retener de verdad. |
| 151 | + |
| 152 | +--- |
| 153 | + |
| 154 | +## Criterio 1 · La corrección de ALB-00119 |
| 155 | + |
| 156 | +**No la he ejecutado yo, y es deliberado.** `adjust_goods_receipt_line` escribe |
| 157 | +`created_by = auth.uid()`; llamándola desde MCP (rol de servicio) el movimiento de 424 € |
| 158 | +quedaría firmado por nadie, que es justo lo contrario de lo que pide el criterio (§9.8, |
| 159 | +rastro). Se hace desde la pantalla, con el botón **Corregir** nuevo — y así el criterio 3 |
| 160 | +se verifica con el mismo gesto. |
| 161 | + |
| 162 | +**Los números, sacados del papel:** |
| 163 | + |
| 164 | +| Campo | Valor | De dónde sale | |
| 165 | +|---|---|---| |
| 166 | +| Proveedor | **NOBLEZA VACUNA SL** (a confirmar con el papel) | es el proveedor **preferido** de este artículo y su ficha está en formato *Paquete*, el mismo de la línea. El albarán es manuscrito: la IA no leyó emisor. | |
| 167 | +| Nº de albarán | el que venga en el papel | la IA lo leyó como `null` | |
| 168 | +| Formato | **Paquete** (6 uds = 1,5 kg) | el que ya tiene; no hay ningún formato en kg | |
| 169 | +| Cantidad | **37,713333** | 56,57 kg ÷ 1,5 kg/paquete | |
| 170 | +| Coste de cada uno | **11,25 €** | 7,50 €/kg × 1,5 kg/paquete | |
| 171 | +| Motivo | *lo dice el albarán* | | |
| 172 | + |
| 173 | +Comprobación: 37,713333 × 11,25 = **424,27 €** ✓ (base imponible del papel) |
| 174 | +y 37,713333 × 6 = **226,28 uds** al almacén, frente a las 228 que hay ahora. |
| 175 | + |
| 176 | +**Aviso de precio**: `article_supplier.last_price` de este artículo con NOBLEZA está en |
| 177 | +1,25 €/ud = 5,00 €/kg. El papel dice 7,50 €/kg, **un 50 % más**. Puede ser real o puede |
| 178 | +ser un 5 leído como 7 en letra manuscrita. Con el papel delante se resuelve en un |
| 179 | +segundo; ése es exactamente el punto del encargo. |
| 180 | + |
| 181 | +--- |
| 182 | + |
| 183 | +## Criterios |
| 184 | + |
| 185 | +| # | Criterio | Estado | |
| 186 | +|---|---|---| |
| 187 | +| 1 | ALB-00119 corregido con la función y motivo | Números listos arriba · **lo ejecuta Julio desde el botón Corregir** (razón explicada) | |
| 188 | +| 2 | El albarán se ve sin salir de la pantalla (foto y PDF) | ✅ implementado · falta tu captura | |
| 189 | +| 3 | Cambiar cantidad, formato y coste con rastro | ✅ implementado · falta tu captura | |
| 190 | +| 4 | Proveedor y nº se pueden poner y sin ellos no se cierra | ✅ pantalla + servidor (tras la migración) | |
| 191 | +| 5 | Una recepción con `needs_review` no postea stock | ✅ código · **efectivo al aplicar la migración** | |
| 192 | +| 6 | Lista de las líneas sin confirmar por importe | ✅ `UnverifiedLinesPage` · **341 líneas, 16.108,21 €** | |
| 193 | + |
| 194 | +## Comprobaciones |
| 195 | + |
| 196 | +`tsc -b` limpio · `npm run build` limpio · **31 pruebas, 3 ficheros, todas pasan** |
| 197 | +(8 nuevas de `unverifiedReason`). |
| 198 | +`eslint`: los mismos errores que ya había antes del encargo, verificado con `git stash` |
| 199 | +(2 en `GoodsReceiptsPage`, 2 en `ReceiptScanPanel`/`ReceiptWizard`, todos |
| 200 | +`set-state-in-effect` preexistentes). `UnverifiedLinesPage.tsx` sale limpia. |
0 commit comments