@@ -53,9 +53,7 @@ import {
5353 updateGoodsReceipt ,
5454 deleteGoodsReceiptLinesByReceipt ,
5555 createGoodsReceiptLine ,
56- updateGoodsReceiptLine ,
5756 confirmReceipt ,
58- adjustGoodsReceiptLine ,
5957 voidReceipt ,
6058 listOrderLineReceived ,
6159 qtyInBaseFromFormat ,
@@ -98,23 +96,12 @@ export interface ReceiptPrefill {
9896 // REVISAR BORRADOR EN SITIO: si la fuente es un borrador, la oficina lo revisa
9997 // y confirma la MISMA recepción (no crea otra ni anula). Trae la foto del albarán.
10098 isDraft ?: boolean
101- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — la fuente es
102- // una recepción 'recibido' (el asistente ya metió el stock): oficina VERIFICA,
103- // no re-cuenta. A diferencia de isDraft, persist() NO borra y recrea las
104- // líneas — las ajusta en sitio (adjustGoodsReceiptLine) para no romper la
105- // referencia (source_id) de lo que ya está posteado en el ledger.
106- isReceived ?: boolean
10799 code ?: string | null
108100 rawDocumentUrl ?: string | null
109101 // ENCARGO CODE (13/08) fix/recepcion-p2-oficina, §3 — total del albarán leído
110102 // por la IA (goods_receipt_ai_session.parsed_result.document.grand_total, YA
111103 // EXISTE — se lee vía getReceiptDocTotal). null si no hubo OCR o no lo leyó.
112104 docTotal ?: number | null
113- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — quién recibió y
114- // cuándo (goods_receipt.created_by_name / received_at), para la cabecera
115- // "Recibido por X · HH:MM · ya en stock". Solo se usa con isReceived.
116- receivedByName ?: string | null
117- receivedAt ?: string | null
118105}
119106export interface ReceiptPrefillLine {
120107 // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — id real de
@@ -146,10 +133,6 @@ export interface ReceiptPrefillLine {
146133 docQty : number | null
147134 docAmount : number | null
148135 discrepancyReason : string | null
149- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — la ⚑ que puso
150- // el asistente ("que lo mire la oficina"). Ordena la revisión (⚑ primero) y
151- // no cuenta como fallo de cocina en la racha.
152- flaggedForOffice ?: boolean
153136}
154137
155138// Propuesta OCR (C2.2.a-2): cabecera resuelta + líneas leídas del albarán.
@@ -224,7 +207,6 @@ interface DraftLine {
224207 // (prefill). Ancla adjustGoodsReceiptLine/updateGoodsReceiptLine a LA MISMA
225208 // fila en vez de borrar y recrear.
226209 id ?: string
227- flaggedForOffice ?: boolean
228210 recipeItemId : string | null
229211 productName : string
230212 purchaseFormatId : string | null
@@ -679,12 +661,6 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
679661 const fromOcr = ! ! ocrPrefill
680662 // ¿Revisión de un BORRADOR en sitio? La oficina confirma la MISMA recepción.
681663 const reviewingDraft = correcting && ! ! prefill ?. isDraft
682- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — ¿revisión de un
683- // 'recibido' del asistente? El stock YA entró; oficina verifica, no re-cuenta.
684- const reviewingReceived = correcting && ! ! prefill ?. isReceived
685- console . log ( '[DEBUG-wizard-routing] GoodsReceiptForm montado' , {
686- againstOrder, correcting, fromOcr, reviewingDraft, reviewingReceived,
687- } )
688664 const fixedHeader = againstOrder || correcting // en OCR la cabecera es editable (propuesta)
689665 // ENCARGO CODE (12/08) fix/recepcion-fromocr-borrador. `fromOcr` significa
690666 // "esta sesión trae ocrPrefill" (escaneo en vivo) — NO "esta línea necesita
@@ -1580,14 +1556,12 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
15801556 lines = prefill . lines . map ( ( l , i ) => {
15811557 const cat = resolveFmt ( l . purchaseFormatId , l . recipeItemId )
15821558 const ref = refFor ( l . purchaseOrderLineId )
1583- // Revisión de BORRADOR o de un 'recibido' (Tramo C) → precarga lo que
1584- // se contó (para verlo y ajustar). Corrección de una CONFIRMADA →
1585- // vacío (se re-cuenta).
1586- const startQty = ( prefill . isDraft || prefill . isReceived ) && l . qtyReceived != null ? numToInputStr ( l . qtyReceived ) : ''
1559+ // Revisión de BORRADOR → precarga lo que se contó (para verlo y
1560+ // ajustar). Corrección de una CONFIRMADA → vacío (se re-cuenta).
1561+ const startQty = prefill . isDraft && l . qtyReceived != null ? numToInputStr ( l . qtyReceived ) : ''
15871562 return {
15881563 key : `pf-${ i } ` ,
15891564 id : l . id ,
1590- flaggedForOffice : l . flaggedForOffice ?? false ,
15911565 recipeItemId : l . recipeItemId ,
15921566 productName : l . productName ,
15931567 purchaseFormatId : l . purchaseFormatId ?? cat ?. purchaseFormatId ?? null ,
@@ -1614,7 +1588,7 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
16141588 // el picker de motivo al editar en oficina, solo en BORRADOR).
16151589 lineAmount : l . docAmount ,
16161590 albaranQty : l . docQty ,
1617- originalQty : ( prefill . isDraft || prefill . isReceived ) ? startQty : null ,
1591+ originalQty : prefill . isDraft ? startQty : null ,
16181592 discrepancyReason : l . discrepancyReason ,
16191593 }
16201594 } )
@@ -1693,30 +1667,8 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
16931667
16941668 const visible = useMemo ( ( ) => {
16951669 const q = search . trim ( ) . toLowerCase ( )
1696- const base = q === '' ? draft : draft . filter ( l => l . productName . toLowerCase ( ) . includes ( q ) )
1697- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C, §4 — "las
1698- // líneas marcadas ⚑ primero, con el editor completo": la oficina entra
1699- // directa a lo que cocina pidió revisar.
1700- if ( ! reviewingReceived ) return base
1701- return [ ...base ] . sort ( ( a , b ) => Number ( ! ! b . flaggedForOffice ) - Number ( ! ! a . flaggedForOffice ) )
1702- } , [ draft , search , reviewingReceived ] )
1703-
1704- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C, §4 — "todo lo
1705- // demás colapsado a una frase por línea": al revisar un 'recibido', las
1706- // líneas SIN ⚑ arrancan colapsadas (un vistazo, no un formulario); las ⚑
1707- // arrancan abiertas con el editor completo (PR #63). Un toque expande
1708- // cualquiera si la oficina quiere tocarla igualmente — nada queda bloqueado.
1709- const [ expandedKeys , setExpandedKeys ] = useState < Set < string > > ( new Set ( ) )
1710- function toggleExpanded ( key : string ) {
1711- setExpandedKeys ( prev => {
1712- const next = new Set ( prev )
1713- if ( next . has ( key ) ) next . delete ( key ) ; else next . add ( key )
1714- return next
1715- } )
1716- }
1717- function isCompactLine ( l : DraftLine ) : boolean {
1718- return reviewingReceived && ! l . flaggedForOffice && ! expandedKeys . has ( l . key )
1719- }
1670+ return q === '' ? draft : draft . filter ( l => l . productName . toLowerCase ( ) . includes ( q ) )
1671+ } , [ draft , search ] )
17201672
17211673 const filled = useMemo (
17221674 ( ) => draft . filter ( l => { const n = parseNum ( l . qty ) ; return n !== null && n > 0 } ) ,
@@ -1976,8 +1928,6 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
19761928 // Tramo A/C — crea el NODO de formato si la línea trae equivalencia pero aún
19771929 // no tiene id (propuesto por la IA o montado por el wizard T1). Si ya tiene
19781930 // purchaseFormatId (heredado del proveedor o del catálogo), se reutiliza.
1979- // Extraído para no duplicar esta lógica entre persist() (recepción nueva) y
1980- // persistReviewedReceived() (Tramo C, ajuste de una línea ya recibida).
19811931 async function resolvePurchaseFormatId ( l : DraftLine ) : Promise < string | null > {
19821932 if ( l . purchaseFormatId ) return l . purchaseFormatId
19831933 if ( ! l . recipeItemId || l . formatQtyInBase == null || l . formatQtyInBase <= 0 ) return null
@@ -2014,73 +1964,10 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
20141964 }
20151965 }
20161966
2017- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C — persistencia de
2018- // la revisión de oficina de un 'recibido'. NUNCA borra y recrea las líneas
2019- // (a diferencia de reviewingDraft): su id es la referencia (source_id) del
2020- // movimiento YA posteado en el ledger por el asistente. Solo las líneas con
2021- // algo que afecta a stock/valoración cambiado de verdad llaman a
2022- // adjustGoodsReceiptLine (reversa + repostea, ledger append-only); un cambio
2023- // de solo el motivo usa updateGoodsReceiptLine (no toca stock); sin cambios,
2024- // no se escribe nada. Cierra con confirmReceipt, que sobre un 'recibido' NO
2025- // vuelve a postear — solo pasa a 'confirmado'.
2026- async function persistReviewedReceived ( ) {
2027- if ( ! prefill ) return
2028- await updateGoodsReceipt ( prefill . sourceReceiptId , {
2029- supplierId : supplierId || null ,
2030- purchaseOrderId : linkedOrderId ,
2031- supplierDocNumber : supplierDoc . trim ( ) || null ,
2032- receiptDate,
2033- } )
2034-
2035- for ( const l of draft ) {
2036- if ( ! l . id ) continue
2037- const orig = prefill . lines . find ( pl => pl . id === l . id )
2038- if ( ! orig ) continue
2039- const newQty = parseNum ( l . qty ) ?? 0
2040- const newCost = parseNum ( l . unitCost )
2041- const reason = discrepancyReasons [ l . key ] ?? null
2042- const purchaseFormatId = await resolvePurchaseFormatId ( l )
2043- const stockChanged =
2044- orig . recipeItemId !== l . recipeItemId ||
2045- ( orig . purchaseFormatId ?? null ) !== purchaseFormatId ||
2046- orig . qtyReceived !== newQty ||
2047- ( orig . unitCost ?? null ) !== newCost
2048- if ( stockChanged ) {
2049- await adjustGoodsReceiptLine ( l . id , {
2050- recipeItemId : l . recipeItemId ,
2051- purchaseFormatId,
2052- qtyReceived : newQty ,
2053- unitCost : newCost ,
2054- discrepancyReason : reason ,
2055- } )
2056- } else if ( reason !== ( orig . discrepancyReason ?? null ) ) {
2057- await updateGoodsReceiptLine ( l . id , { discrepancyReason : reason } )
2058- }
2059- }
2060-
2061- const res = await confirmReceipt ( prefill . sourceReceiptId )
2062-
2063- let learnNote = ''
2064- try {
2065- const learned = await learnFromReceipt ( prefill . sourceReceiptId )
2066- if ( learned > 0 ) learnNote = ` · memoria del proveedor actualizada (${ learned } )`
2067- } catch ( e ) {
2068- console . error ( 'persistReviewedReceived: confirmada OK pero el aprendizaje falló' , e )
2069- }
2070-
2071- const parts = [ `verificada` ]
2072- if ( res . skippedLines > 0 ) parts . push ( `${ res . skippedLines } sin postear (revisar)` )
2073- onSaved ( `Recepción ${ prefill . code ?? '' } confirmada: ${ parts . join ( ' · ' ) } ${ learnNote } .` )
2074- }
2075-
20761967 async function persist ( confirm : boolean ) {
20771968 if ( ! locationId ) { setError ( 'No hay un local operativo definido. Revisa el aviso de local arriba.' ) ; return }
20781969 setSaving ( true ) ; setError ( null )
20791970 try {
2080- if ( reviewingReceived && prefill ) {
2081- await persistReviewedReceived ( )
2082- return
2083- }
20841971 let receipt : { id : string ; code : string | null }
20851972 if ( reviewingDraft && prefill ) {
20861973 // En sitio: actualiza cabecera + limpia líneas para reescribirlas. NO crea
@@ -2202,20 +2089,10 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
22022089 }
22032090 }
22042091
2205- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C, §4 — "cabecera
2206- // honesta": el stock YA está dentro, esto no es una revisión a ciegas.
2207- const receivedTimeLabel = prefill ?. receivedAt
2208- ? new Intl . DateTimeFormat ( 'es-ES' , { hour : '2-digit' , minute : '2-digit' } ) . format ( new Date ( prefill . receivedAt ) )
2209- : null
2210-
2211- const title = reviewingReceived
2212- ? `Recibido por ${ prefill ?. receivedByName ?? '—' } ${ receivedTimeLabel ? ` · ${ receivedTimeLabel } ` : '' } · ya en stock`
2213- : againstOrder
2092+ const title = againstOrder
22142093 ? `Recibir pedido ${ order ?. code ?? '' } ${ fromOcr ? ' · albarán escaneado' : '' } `
22152094 : reviewingDraft ? `Revisar recepción ${ prefill ?. code ?? '' } ` : correcting ? 'Corregir recepción' : fromOcr ? 'Revisar recepción escaneada' : 'Nueva recepción'
2216- const subtitle = reviewingReceived
2217- ? 'El almacén ya tiene esta mercancía. Verifica: las líneas marcadas ⚑ primero. Cambiar la cantidad pide motivo y genera un ajuste de stock.'
2218- : againstOrder
2095+ const subtitle = againstOrder
22192096 ? 'Cuenta lo que ha llegado y escríbelo. Lo pedido y lo pendiente están a la derecha como referencia.'
22202097 : reviewingDraft
22212098 ? 'Esto contó quien recibió. Revisa la foto del albarán y las líneas; ajusta lo que falte y confirma — entra al almacén con su coste.'
@@ -2242,8 +2119,8 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
22422119
22432120 { ! fixedHeader && < OperativeLocationBanner op = { op } locations = { locations } /> }
22442121
2245- { /* Foto del albarán al revisar un borrador o un recibido (la oficina ve lo que firmó el muelle). */ }
2246- { ( reviewingDraft || reviewingReceived ) && prefill ?. rawDocumentUrl && (
2122+ { /* Foto del albarán al revisar un borrador (la oficina ve lo que firmó el muelle). */ }
2123+ { reviewingDraft && prefill ?. rawDocumentUrl && (
22472124 < div className = "rounded-lg border border-border-default bg-card p-3" >
22482125 < p className = "text-xs text-text-secondary mb-2" > Albarán recibido</ p >
22492126 < ReceiptPhotoViewer path = { prefill . rawDocumentUrl } />
@@ -2563,30 +2440,6 @@ export default function GoodsReceiptForm({ accountId, order, prefill, ocrPrefill
25632440 const albaranPackTotalBase = albaranPack ? albaranPack . n * albaranPack . m * fu . factor : null
25642441 const formatMismatchAlbaran = albaranPackTotalBase != null && albaranPackTotalBase > 0 && l . formatQtyInBase != null && Math . abs ( l . formatQtyInBase - albaranPackTotalBase ) / albaranPackTotalBase > 0.02
25652442
2566- // ENCARGO CODE (13/08) feat/recepcion-v2-asistente, Tramo C, §4 —
2567- // línea SIN ⚑ al revisar un 'recibido': una frase, no un formulario.
2568- // Un toque ("Revisar") la abre al editor completo de abajo — nada
2569- // queda inaccesible, solo declutter por defecto.
2570- if ( isCompactLine ( l ) ) {
2571- const perBaseCompact = costN != null && l . formatQtyInBase ? costN / l . formatQtyInBase : null
2572- return (
2573- < div key = { l . key } className = "rounded-lg border border-border-default bg-card px-3 py-2 flex items-center justify-between gap-3" >
2574- < div className = "min-w-0 flex items-center gap-2" >
2575- < Check size = { 14 } className = "text-success shrink-0" />
2576- < span className = "text-sm text-text-primary truncate" >
2577- { l . matchedName ?? l . productName }
2578- { hasQty && < span className = "text-text-secondary" > · { l . qty } { l . formatLabel ? ` ${ l . formatLabel } ` : '' } </ span > }
2579- { perBaseCompact != null && < span className = "text-text-secondary" > · { fmtHumanPrice ( perBaseCompact ) } €/{ l . baseUnit ?. abbr ?? 'ud' } </ span > }
2580- </ span >
2581- </ div >
2582- < button type = "button" onClick = { ( ) => toggleExpanded ( l . key ) }
2583- className = "shrink-0 text-xs font-medium text-accent hover:underline" >
2584- Revisar
2585- </ button >
2586- </ div >
2587- )
2588- }
2589-
25902443 return (
25912444 < div key = { l . key }
25922445 className = { `rounded-lg border p-3 ${ albaranDiff ? 'border-danger bg-danger-bg' : 'border-border-default bg-card' } ${ complete && ! hasQty ? 'opacity-60' : '' } ` } >
0 commit comments