From cb0cabf2d294f204f9de6d63adc23d0380255fc3 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:18:48 -0600 Subject: [PATCH 01/11] docs(#32): Add detailed implementation plan for automatic sample generation --- documents/plans/ISSUE32_PLAN.md | 191 ++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 documents/plans/ISSUE32_PLAN.md diff --git a/documents/plans/ISSUE32_PLAN.md b/documents/plans/ISSUE32_PLAN.md new file mode 100644 index 0000000..acf276e --- /dev/null +++ b/documents/plans/ISSUE32_PLAN.md @@ -0,0 +1,191 @@ +# Plan de Implementación - Issue #32: Generación Automática de Muestras + +## Objetivo +Automatizar la generación de muestras cuando se confirman órdenes de laboratorio, basándose en las relaciones test-muestra establecidas en Issue #44. + +## Análisis de Requisitos + +### Funcionalidad Esperada +1. Al confirmar una orden de laboratorio (`sale.order` con `is_lab_request=True`): + - Analizar todos los análisis incluidos en las líneas de orden + - Agrupar análisis por tipo de muestra requerida + - Generar automáticamente registros `stock.lot` (muestras) para cada grupo + - Asignar códigos de barras únicos a cada muestra + - Establecer el estado inicial como 'pending_collection' + +### Reglas de Negocio +1. **Agrupación de Análisis**: Múltiples análisis que requieran el mismo tipo de muestra deben compartir un único contenedor +2. **Volumen de Muestra**: Sumar los volúmenes requeridos de todos los análisis del grupo +3. **Identificación**: Cada muestra debe tener un código de barras único generado automáticamente +4. **Trazabilidad**: Las muestras deben estar vinculadas a la orden de laboratorio original +5. **Manejo de Errores**: Si un análisis no tiene tipo de muestra definido, generar advertencia pero continuar con los demás + +## Tareas de Implementación + +### 1. Extender el modelo sale.order +**Archivo:** `lims_management/models/sale_order.py` +- [ ] Agregar campo Many2many para referenciar las muestras generadas: + ```python + generated_sample_ids = fields.Many2many( + 'stock.lot', + 'sale_order_stock_lot_rel', + 'order_id', + 'lot_id', + string='Muestras Generadas', + domain="[('is_lab_sample', '=', True)]", + readonly=True + ) + ``` +- [ ] Override del método `action_confirm()` para interceptar la confirmación +- [ ] Implementar método `_generate_lab_samples()` con la lógica principal +- [ ] Agregar método `_group_analyses_by_sample_type()` para agrupar análisis + +### 2. Lógica de generación de muestras +**Archivo:** `lims_management/models/sale_order.py` +- [ ] Implementar algoritmo de agrupación: + ```python + def _group_analyses_by_sample_type(self): + """Agrupa las líneas de orden por tipo de muestra requerida""" + groups = {} + for line in self.order_line: + if line.product_id.is_analysis: + sample_type = line.product_id.required_sample_type_id + if sample_type: + if sample_type.id not in groups: + groups[sample_type.id] = { + 'sample_type': sample_type, + 'lines': [], + 'total_volume': 0.0 + } + groups[sample_type.id]['lines'].append(line) + groups[sample_type.id]['total_volume'] += line.product_id.sample_volume_ml or 0.0 + return groups + ``` +- [ ] Crear método para generar muestras por grupo +- [ ] Implementar logging para trazabilidad + +### 3. Generación de códigos de barras +**Archivo:** `lims_management/models/stock_lot.py` +- [ ] Mejorar el método `_compute_barcode()` para asegurar unicidad +- [ ] Agregar validación de duplicados +- [ ] Considerar prefijos por tipo de muestra + +### 4. Actualizar vistas de sale.order +**Archivo:** `lims_management/views/sale_order_views.xml` +- [ ] Agregar pestaña "Muestras Generadas" en formulario de orden +- [ ] Mostrar campo `generated_sample_ids` con vista de lista embebida +- [ ] Agregar botón para regenerar muestras (si es necesario) +- [ ] Incluir indicadores visuales del estado de generación + +### 5. Crear wizard de configuración (opcional) +**Archivos:** +- `lims_management/wizard/sample_generation_wizard.py` +- `lims_management/wizard/sample_generation_wizard_view.xml` +- [ ] Crear wizard para revisar/modificar la generación antes de confirmar +- [ ] Permitir ajustes manuales de agrupación si es necesario +- [ ] Opción para excluir ciertos análisis de la generación automática + +### 6. Notificaciones y alertas +**Archivo:** `lims_management/models/sale_order.py` +- [ ] Implementar sistema de notificaciones: + - Análisis sin tipo de muestra definido + - Muestras generadas exitosamente + - Errores en la generación +- [ ] Usar el sistema de mensajería de Odoo (`mail.thread`) + +### 7. Pruebas y validación +**Archivo:** `verify_automatic_sample_generation.py` +- [ ] Crear script de verificación que pruebe: + - Generación correcta de muestras + - Agrupación adecuada de análisis + - Cálculo correcto de volúmenes + - Unicidad de códigos de barras + - Manejo de casos edge (análisis sin tipo de muestra) + +### 8. Actualizar datos de demostración +**Archivo:** `lims_management/demo/z_automatic_generation_demo.xml` +- [ ] Crear órdenes de laboratorio de ejemplo que demuestren: + - Orden con múltiples análisis del mismo tipo de muestra + - Orden con análisis de diferentes tipos de muestra + - Orden mixta con algunos análisis sin tipo de muestra + +## Consideraciones Técnicas + +### Performance +- La generación debe ser eficiente incluso con órdenes grandes (20+ análisis) +- Usar creación en batch para múltiples muestras +- Considerar uso de SQL para verificación de unicidad de barcodes + +### Transaccionalidad +- Todo el proceso debe ser atómico: o se generan todas las muestras o ninguna +- Usar `@api.model` con manejo adecuado de excepciones +- Rollback automático en caso de error + +### Configurabilidad +- Considerar agregar configuración a nivel de compañía: + - Habilitar/deshabilitar generación automática + - Formato de código de barras personalizable + - Reglas de agrupación personalizables + +### Compatibilidad +- Mantener compatibilidad con flujo manual existente +- Permitir creación manual de muestras adicionales si es necesario +- No interferir con órdenes de venta regulares (no laboratorio) + +## Flujo de Trabajo + +```mermaid +graph TD + A[Orden de Laboratorio] --> B{¿Confirmar Orden?} + B -->|Sí| C[Analizar Líneas de Orden] + C --> D[Identificar Análisis] + D --> E[Agrupar por Tipo de Muestra] + E --> F{¿Todos tienen tipo de muestra?} + F -->|No| G[Generar Advertencia] + F -->|Sí| H[Continuar] + G --> H + H --> I[Crear Muestras por Grupo] + I --> J[Generar Códigos de Barras] + J --> K[Asociar a la Orden] + K --> L[Confirmar Orden] + L --> M[Notificar Usuario] +``` + +## Criterios de Aceptación + +1. [ ] Al confirmar una orden de laboratorio, se generan automáticamente las muestras necesarias +2. [ ] Los análisis que requieren el mismo tipo de muestra se agrupan en un solo contenedor +3. [ ] Cada muestra tiene un código de barras único +4. [ ] Se muestra claramente qué muestras fueron generadas para cada orden +5. [ ] Se manejan adecuadamente los análisis sin tipo de muestra definido +6. [ ] El sistema registra un log de la generación para auditoría +7. [ ] La funcionalidad se puede deshabilitar si es necesario +8. [ ] No afecta el rendimiento de confirmación de órdenes regulares + +## Estimación de Tiempo + +- Tarea 1-2: 2-3 horas (lógica principal) +- Tarea 3: 1 hora (mejoras barcode) +- Tarea 4: 1 hora (vistas) +- Tarea 5: 2 horas (wizard opcional) +- Tarea 6: 1 hora (notificaciones) +- Tarea 7-8: 1-2 horas (pruebas y demo) + +**Total estimado: 8-10 horas** + +## Dependencias + +- **Completo**: Issue #44 (Relaciones test-muestra) ✓ +- **Requerido**: Módulo `stock` de Odoo para `stock.lot` +- **Requerido**: Librería `python-barcode` para generación de códigos + +## Riesgos y Mitigaciones + +1. **Riesgo**: Conflictos con otros módulos que modifiquen `sale.order.action_confirm()` + - **Mitigación**: Usar `super()` correctamente y documentar la integración + +2. **Riesgo**: Rendimiento con órdenes muy grandes + - **Mitigación**: Implementar creación en batch y considerar procesamiento asíncrono + +3. **Riesgo**: Duplicación de códigos de barras + - **Mitigación**: Implementar verificación robusta y regeneración si es necesario \ No newline at end of file From a9ed1a23bdabbeab094873d7a42cceefd231dd4d Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:29:29 -0600 Subject: [PATCH 02/11] feat(#32): Add automatic sample generation - Task 1 completed - Added generated_sample_ids field to sale.order model - Override action_confirm() to intercept lab order confirmation - Implemented _generate_lab_samples() main logic method - Implemented _group_analyses_by_sample_type() for grouping - Implemented _create_sample_for_group() for sample creation - Added necessary fields to stock.lot model (doctor_id, origin, volume_ml, analysis_names) - Updated state field to include 'pending_collection' state - Added proper error handling and user notifications via message_post - Successful test with ephemeral instance restart --- .../__pycache__/product.cpython-312.pyc | Bin 2960 -> 2960 bytes .../__pycache__/sale_order.cpython-312.pyc | Bin 914 -> 7160 bytes .../__pycache__/stock_lot.cpython-312.pyc | Bin 4359 -> 5021 bytes lims_management/models/sale_order.py | 148 +++++++++++++++++- lims_management/models/stock_lot.py | 23 +++ pr_description_issue44.txt | 37 +++++ 6 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 pr_description_issue44.txt diff --git a/lims_management/models/__pycache__/product.cpython-312.pyc b/lims_management/models/__pycache__/product.cpython-312.pyc index 945c75b16eac0f33cb5386ca9878493a03ba04e2..7c7c8226d8bd29196aa4b427262850bdb6a78d9e 100644 GIT binary patch delta 19 ZcmbOrK0%!8G%qg~0}zB<+sM_%4FE3V1ikKd5lA5qW#>;MCZlLsO{DF@UUjQNBu5!3nt#n>lM(wT z=ich-Hcqq2wOw@|=bn4ct$V(6?(uhCuLnUnnV+6mXh7&caKkE0vGU>uROS(ncuGV` zDoQ17QCpIZ(n)*NPT_u=$Rr(62dUGdGwQS-HDbczJZB3cRs_|7k%X@hr@8<)2HDB|f_P)y;i+VC>jNrr{F3z2bUzX&!D#4W0j8i8B4m08iBQAmYh-E&q$tQw0Us2^mYTR)0 z!dQG-R1N#6G&N(`Cj@aytC9sJG8LEA6i5~r7RCe_6LSO7D9EFQKHSogP+~l3b!_E2 z_QT_>JS~JTY6s7P2<8ZPZ9?K?GFjk>Xj5@D0m5=)k_>H-u99rDj6b5`;H6|dk&?a8 zc;!}9Y4l1&lvJYu>lY*X#Yk@qdSkL68ZNUl!5a?TmEg6S(|FtDxWXM1K$SQjSF{#W zcvv~6#FJB^@VWwLwd1Qc-lTwCoK`^^6>q#a!;KSWDkybrLP(jKm>Yf#mKv2(V+lDa z@L@aAM8@aLaLGcPmr~*kQE0SiI^?`yb+LiG7>3@zpyrWEy75D`S=BW9vnqt{7n`$G z8cjN_vGSXzETbGi7i$rkwYgE+Ht8-?ST&0ig>DD!C{2x^cBGc9XCsn-I!mjpMLp@a zYUMX?1I`tI;WNb3Dy(gmG;K-r8T#CA)hR4h(4r~7)pb^#FVR-wr8y9Q%>&2%;{H(R z0^*&s^bPuU+I(N5uA%Gn1$2!HyR=3f2ZtP=lcL#Z?F5o6 z2G}61dH6t^CqT3Ha?>Az7A7qis=1!bC9Sha4%4@H@o7qeH3+@jCMCkX3oORsS0#C? zIYR*Xl=7rxJT)B`34O$1QxaG_%vTdq>MNUJzbx@H2AdQVB|a|1)R`&aOqewoWqJzq z#$Vs?#YZu5tav&N*H}VS1zF&YV9eSPqHIwm!+qrXs4#`+7)~&;@o_;m7~m$!hCKy> z8#Gu}dCe&an($Rnm@({%AdVR{fQZ46T`?%ZaK~b!ppg=bDcBovP;S2J*`7?P+lA|3 zqub&<*oU%ROeB>UXbXy9ZMU0FVtd)@BU3YS6L8QPMlAG-1J#G-FV0=ew(ndDzWp3kxoa{<3O0|oCLajq0$qBb>vw@|1qS*H zArz?1cz%2tM0L&2k;_~2G_Y>oJLk=E{g00P`s6n!AMJZAFAu+;J@>)V@cHa}7qTtU z<-muqMs-K^ZRk!~)vRBtU z!;e1{+_1uwGJTG+5)KY|5#^+2C5SJlzH&> zMrh31(o`ixvUCOPYeDEIuq(6ltQ|O2mpNFqbO~Q(nKYwNw?k=Ln$hYo_uCvGqD}sc zXLtwTR0ZEZMvudO#Y%X&O6N*@>n!71y0n{sldd=JMM*jc#N4wW1?aRDWMk3}-jil3 ze<{8SMj-TCTi!s;e$Hs%U1uG)BcQK?_nIw`37glNqBdI{yw5rhX$Qe!|ARmohi7OA z@v61HV?`fX31u(G74V%B_M8F-mM+dHg2E-tHUSK6OL0C1*;PS81-#vMMk@2TmQW{( zpv++uR3OxtPVtzx3D0L_?8$z>2VKU#Lv!_mW5%vrtF46Q_I=Z0pJv}DCi*m*+s0|k zK0v5G{DOh$4q#D=F-#bR7{d$~)*!r$ty;$ZE$lZuLQ0vIA&h~jWCL^=_CyMNNGqTmtA$83U@}EfNyx6vJ;WXfad_-)cq>gn3Lg z__!K}Xp@9mMpcnUOem^?@e8{FC~_FDhL4!Gp^WMk9n&5lQbqvjZlVKv0<XCnE$sQL{(JtVjeD}~?_}RO_4w>>FZ|}h`DOM37Ns>a zm=A@YBg*?z>W(8HXj-gW9Q*Rh-7AY1AGSXn%k_-tJtMiE^Lo$u?5+#h3zxDRVtODp zS5=_#lofW}!fu^y%M7ltfgIbcv&~O~_1Sd?mx6~L4eG(6d~;WULEe_%BiidL1VQ2# zu(#+nAA=77@iEw1=1~HX$P<&TDFf&ZbrH@9V6$x!Th^K~OQ-2c`}yi&{a%e6_N~;A`xa zyhWP2O38I;`#9nq@aO!cJxx`n+y^`IrZMdKco%+Fik!=S1vGvGcf!H_Mh+Es)57}y zgRAGd?Ya$fhMlR90Ju&mHt=NFn^y2o8u(1?dq(L|8{UCC)a?%QP7*K(U6u4enK!=@>!4RYiZ=p9AU1`GB}Kz;Ei^49V;`wAVa9}gM-lJsL>y<4 zN-%OdVyNq5|tUqL!2(nNtt5+ zg>_B2+RaaDH|GNl#a8!|+HM?;`{(@m`VEWDFWI~7!j0^<_p)`vFKJtKO~Hk#A)as5 z1FaB*XPYiAHD1cbCvx$m4!@1bJVf6Ns&0FUXp^{cCDfV=y`zWT$qd3Gfu>Jx+`6%N zPWN}Mv`6yI8}b`F@?6_9H`7^j$8qOp^Ec*h=z-0J0P@z}**o7i*9TC$2zc9-W4m>> z`{CQm>>ltb<*{C!jpW!}I=k!P_^-sj5trHi*T(ynVAi$V?je2m(39QgvLolR(M!2# zN{8Q`l+LDp$95O`DZD#{qZA6%XT0)$*u&A)Xp6HY1h4-Rdgl?`{wbcqc1ds<-fK%G ztOmrafYfiGtbp8dedV~e5{DE;ZUiNyFUwcikAOZj!7)3I&MWx^pl}ij$m4~Ogk(JKQe1qOLvA!Sk3zY{VQk; zp3DDcIbk&LIb%r;cx)#*n-bbW&SrR)S+z12bWP!_)lzJqF7bN(j#rNKRSYGh@HN)D zKettqyb{+j8}^*mykAhfYnHjetjf#OY7sN|=Hi?J0WG90O;jTJ!8k{Nhm*!i;XMJJ zqXZ?-8Z{WlU>0OFL{T8g4b8TdBS3oQ2W$fx9f?E?6n4w-eGwq3nSBI|G~8x}5?39P zoEV2ZV-Kcrm+{bBO#T!jS4L6cngD5(qKk>)9Ed`06Pn+;Llni=7@~pZE=jJfGt)sN zX2y3Kld_l7+DdsMJXXpQagcqDLtY5yhQa|*wiuXSjmwF6iUjD#ib|0zEe8G(=rFGk z_DKme+J)OYFqMO>YhE?vZMYlm$4KVzqYXcBC~gM(4ubk}FYdq?Z`iNKMMd6=+a2&w zu)UL*y$Jf<(AL^kfxfwx<&{bokw3wc`v}|@>@99zKD2T1q8{1_XzM~7I&&@i^p<_O zmIHdrfqYBb7t*Ivu4RYbvLoN!^Q*|;Mi$O~ap}`bx#q}|=Ex%*2p{`OB`mU5m~4gun0q+ViMa-*aZ^?V;?>ceC$a$OfX|eIS~4p}J7!^pD^B8=fP( z+lNE`)3;7%*YAWm)ZZO_xF;Lhx6JN;TGRA2P@S#mSqkj_dT(y;puTtTUq-U;4S#no z+j~|IoP9ytYJCL+EdNkofYB=jTlFvcBmHloZ{KnsVd-ys4|$I`>3?=I(14@jijkuk ziy59+43cNlkS&iHY%KN>vTLd(~WPY z_&^zM(u9QEjQlpVN$QQ<)MV`LW&HUKZ!xzwDu`kX{(&QJfnEi^H9BbC>~IQ_GH`jw!+GC->1OI3H_sG&sG7UuAWq&D%==sO z{;s@t2u7M4>Sl`r8r7&Q&%H2&un;r$gd*THw(SFU~LH|di_XyPqWu${t{jiUY& wbu6Kde?y!875P>?O_`3Ir%CrTEgWC+Y`W=uX}@Tre6Stt9Q%8O>xAw90QBO{J^%m! delta 342 zcmexiK8c<0G%qg~0}#~jDb3Jhn#d=iVh7|+XGmd4Va#F3WsG9XWr|{AWME=&XGmdc zVMt+4Wz1rm{E|>mP!^))|VjtnoPIYa`RJCbBb@VrDdk(q!cHEgkgXU%J`hY zz%cPsI!h(DCeP;0j1r6@>_C}YY$^G6NicG#xV&5RF$H(sESY`QH6pcl2);byX*|k!g{tH zds8Jsat>4tJpk!e;#3JZprQnc$~P`lvQ(+!0;jI_ALxNggwlK8tR3VEv4?Npy!UR;-%`g7<~==TSoAomB9YJ^FeWXQ;FY9}l>lC;tvl4$s9 zu085TGD@<$+D@pQTR9Z)Cb}8zn2eFbUA)vg{V*ACbP)#v;_%ys6^Y~?Ao3o`llONg z-ZVN$3OA&cAu(eGh?s_r7wHwsDXY_sOlh)U*2=^->NYP-jW70R5`!0z}OrjB)N>1_oj6k84bvdwjv!?dyR0tMyZ zMQe2(R~`6qeIv1KTTYcSJXy7h8@Yo~-LC7FaOvxxjF_xaHf+nHM%}cTlLpTVA}zK{_%2eY zQo}G?P3Epg9-=W9%O=9^Px2gEa9_x)eN&*}V)yv8i()DF%jijT-n|v=RK#rzDi`95 zBAfwmU&-g%y8J;k<^CDVqb2uMtfLdRv*XC)lk!)Aws|l98$zetvwgQvsp<7SMI*CN z@S{uY8neG=#V9`|h@T0VbssARvfQLfpy4(3mxka51V7PPw9U&V@#F0qA;R#PRh|1X z99_j^eq`RV^|~Kl3y!K>wR}})y0s2hq0ICut?^*>$As?*0UrqXP{2n5A^?8OX2z<{ zDs+OM5y{U19!WeSV)0~n^Z>*=t~K1c_NmRC#lLuUATDL6z7}cv{v|PC!23_ez7~cb4y#wt=7VlsU)` z%9~#RU&(IG|D70pnHYU7#m3~len}g;*Y||%$C5GS)6KcpDfeRkHB@&0=pUZB4Ba8o hVHRMo2O;#2lz%DZ_rnaK^w!$#>o>3eD~UK*+kZ9^Ku7=p delta 584 zcmbQM-mb)VnwOW00SLbDFU>IKnaDSZX&2+hMb?ay8=1uFB^NQKNTo=p$gGiF%>q)z zzz`*nBG4} zB1ILbOR`c;Q~ef4a7li0woiUZ6=yMsrMFp(nU$GIlWlSy`!YtE$)X&YjB=CfIb0b{ zCm-d=;MN2hQX~%|v?g0{dNEo|e!wL%c{*nZL`DH3^Nllr(;CFo1rn1VaQH%Gl_0Xq zxhxp%fci4{z^18yML`O?IV30Nb1Sepf<$yTPvu_0$Y?g%j<=7|W%F6yU5wK9K#5!I zp?TSP`Q>?<>_uiEg|;BVe)1yzWX90RYyv5aQj@aAtDn(w|4!7$(G8cJdJ~6P%v3=lU;1^%Oy&>fb zKRd5YgX<@u$*+X6rNTkR!A<|gVUwGmQks)$R}?ilUszH!oRLwoqpZKCv*s%Uhz(W> E0DsAdPXGV_ diff --git a/lims_management/models/sale_order.py b/lims_management/models/sale_order.py index a9217b4..e3cc54b 100644 --- a/lims_management/models/sale_order.py +++ b/lims_management/models/sale_order.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- -from odoo import models, fields +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) class SaleOrder(models.Model): _inherit = 'sale.order' @@ -17,3 +21,145 @@ class SaleOrder(models.Model): domain="[('is_doctor', '=', True)]", help="The doctor who referred the patient for this laboratory request." ) + + generated_sample_ids = fields.Many2many( + 'stock.lot', + 'sale_order_stock_lot_rel', + 'order_id', + 'lot_id', + string='Muestras Generadas', + domain="[('is_lab_sample', '=', True)]", + readonly=True, + help="Laboratory samples automatically generated when this order was confirmed" + ) + + def action_confirm(self): + """Override to generate laboratory samples automatically""" + res = super(SaleOrder, self).action_confirm() + + # Generate samples only for laboratory requests + for order in self.filtered('is_lab_request'): + try: + order._generate_lab_samples() + except Exception as e: + _logger.error(f"Error generating samples for order {order.name}: {str(e)}") + # Continue with order confirmation even if sample generation fails + # But notify the user + order.message_post( + body=_("Error al generar muestras automáticamente: %s. " + "Por favor, genere las muestras manualmente.") % str(e), + message_type='notification' + ) + + return res + + def _generate_lab_samples(self): + """Generate laboratory samples based on the analyses in the order""" + self.ensure_one() + _logger.info(f"Generating laboratory samples for order {self.name}") + + # Group analyses by sample type + sample_groups = self._group_analyses_by_sample_type() + + if not sample_groups: + _logger.warning(f"No analyses with sample types found in order {self.name}") + return + + # Create samples for each group + created_samples = self.env['stock.lot'] + + for sample_type_id, group_data in sample_groups.items(): + sample = self._create_sample_for_group(group_data) + if sample: + created_samples |= sample + + # Link created samples to the order + if created_samples: + self.generated_sample_ids = [(6, 0, created_samples.ids)] + _logger.info(f"Created {len(created_samples)} samples for order {self.name}") + + # Post message with created samples + sample_list = "
    " + for sample in created_samples: + sample_list += f"
  • {sample.name} - {sample.sample_type_product_id.name}
  • " + sample_list += "
" + + self.message_post( + body=_("Muestras generadas automáticamente: %s") % sample_list, + message_type='notification' + ) + + def _group_analyses_by_sample_type(self): + """Group order lines by required sample type""" + groups = {} + + for line in self.order_line: + product = line.product_id + + # Skip non-analysis products + if not product.is_analysis: + continue + + # Check if analysis has a required sample type + if not product.required_sample_type_id: + _logger.warning( + f"Analysis {product.name} has no required sample type defined" + ) + # Post warning message + self.message_post( + body=_("Advertencia: El análisis '%s' no tiene tipo de muestra definido") % product.name, + message_type='notification' + ) + continue + + sample_type = product.required_sample_type_id + + # Initialize group if not exists + if sample_type.id not in groups: + groups[sample_type.id] = { + 'sample_type': sample_type, + 'lines': [], + 'total_volume': 0.0, + 'analyses': [] + } + + # Add line to group + groups[sample_type.id]['lines'].append(line) + groups[sample_type.id]['analyses'].append(product.name) + groups[sample_type.id]['total_volume'] += (product.sample_volume_ml or 0.0) * line.product_uom_qty + + return groups + + def _create_sample_for_group(self, group_data): + """Create a single sample for a group of analyses""" + try: + sample_type = group_data['sample_type'] + + # Prepare sample values + vals = { + 'product_id': sample_type.product_variant_id.id, + 'patient_id': self.partner_id.id, + 'doctor_id': self.doctor_id.id if self.doctor_id else False, + 'origin': self.name, + 'sample_type_product_id': sample_type.id, + 'volume_ml': group_data['total_volume'], + 'is_lab_sample': True, + 'state': 'pending_collection', + 'analysis_names': ', '.join(group_data['analyses'][:3]) + + ('...' if len(group_data['analyses']) > 3 else '') + } + + # Create the sample + sample = self.env['stock.lot'].create(vals) + + _logger.info( + f"Created sample {sample.name} for {len(group_data['analyses'])} analyses" + ) + + return sample + + except Exception as e: + _logger.error(f"Error creating sample: {str(e)}") + raise UserError( + _("Error al crear muestra para %s: %s") % (sample_type.name, str(e)) + ) diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index e24cbf6..1cbb2a7 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -40,8 +40,31 @@ class StockLot(models.Model): string='Collected by', default=lambda self: self.env.user ) + + doctor_id = fields.Many2one( + 'res.partner', + string='Médico Referente', + domain="[('is_doctor', '=', True)]", + help="Médico que ordenó los análisis" + ) + + origin = fields.Char( + string='Origen', + help="Referencia a la orden de laboratorio que generó esta muestra" + ) + + volume_ml = fields.Float( + string='Volumen (ml)', + help="Volumen total de muestra requerido" + ) + + analysis_names = fields.Char( + string='Análisis', + help="Lista de análisis que se realizarán con esta muestra" + ) state = fields.Selection([ + ('pending_collection', 'Pendiente de Recolección'), ('collected', 'Recolectada'), ('received', 'Recibida en Laboratorio'), ('in_process', 'En Proceso'), diff --git a/pr_description_issue44.txt b/pr_description_issue44.txt new file mode 100644 index 0000000..a1d9983 --- /dev/null +++ b/pr_description_issue44.txt @@ -0,0 +1,37 @@ +## Resumen + +Este Pull Request implementa la relación entre análisis y tipos de muestra (Issue #44), estableciendo la base necesaria para la automatización de generación de muestras (Issue #32). + +## Cambios principales + +### 1. Modelos +- **ProductTemplate**: Añadidos campos `required_sample_type_id` y `sample_volume_ml` para definir requisitos de muestra en análisis +- **StockLot**: Añadido campo `sample_type_product_id` manteniendo compatibilidad con `container_type` + +### 2. Vistas +- Actualización de vistas de análisis para mostrar campos de tipo de muestra +- Actualización de vistas de stock.lot con nuevo campo de tipo de muestra +- Visualización de relaciones test-muestra en listas y formularios + +### 3. Datos +- Creación de 10 tipos de muestra comunes (Tubo Suero, EDTA, Orina, etc.) +- Actualización de análisis demo con tipos de muestra requeridos +- Actualización de muestras demo con referencias a productos tipo muestra + +### 4. Herramientas +- Script de verificación `verify_sample_relationships.py` para validar la implementación +- Documentación completa en `ISSUE44_IMPLEMENTATION.md` + +## Compatibilidad + +- Mantiene compatibilidad total con el campo legacy `container_type` +- Sincronización automática entre campos viejos y nuevos +- Sin ruptura de funcionalidad existente + +## Pruebas + +Todas las tareas fueron probadas individualmente con reinicio de instancia efímera y verificación de logs sin errores. + +## Próximos pasos + +Con esta base implementada, el Issue #32 puede proceder con la automatización de generación de muestras al confirmar órdenes de laboratorio. \ No newline at end of file From 57e87b46922d861dffab8541cdda9f5bbb01848f Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:30:17 -0600 Subject: [PATCH 03/11] docs(#32): Mark Task 1 and 2 as completed --- documents/plans/ISSUE32_PLAN.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/documents/plans/ISSUE32_PLAN.md b/documents/plans/ISSUE32_PLAN.md index acf276e..d724061 100644 --- a/documents/plans/ISSUE32_PLAN.md +++ b/documents/plans/ISSUE32_PLAN.md @@ -22,9 +22,9 @@ Automatizar la generación de muestras cuando se confirman órdenes de laborator ## Tareas de Implementación -### 1. Extender el modelo sale.order +### 1. Extender el modelo sale.order ✅ **Archivo:** `lims_management/models/sale_order.py` -- [ ] Agregar campo Many2many para referenciar las muestras generadas: +- [x] Agregar campo Many2many para referenciar las muestras generadas: ```python generated_sample_ids = fields.Many2many( 'stock.lot', @@ -36,13 +36,13 @@ Automatizar la generación de muestras cuando se confirman órdenes de laborator readonly=True ) ``` -- [ ] Override del método `action_confirm()` para interceptar la confirmación -- [ ] Implementar método `_generate_lab_samples()` con la lógica principal -- [ ] Agregar método `_group_analyses_by_sample_type()` para agrupar análisis +- [x] Override del método `action_confirm()` para interceptar la confirmación +- [x] Implementar método `_generate_lab_samples()` con la lógica principal +- [x] Agregar método `_group_analyses_by_sample_type()` para agrupar análisis -### 2. Lógica de generación de muestras +### 2. Lógica de generación de muestras ✅ **Archivo:** `lims_management/models/sale_order.py` -- [ ] Implementar algoritmo de agrupación: +- [x] Implementar algoritmo de agrupación: ```python def _group_analyses_by_sample_type(self): """Agrupa las líneas de orden por tipo de muestra requerida""" @@ -61,8 +61,8 @@ Automatizar la generación de muestras cuando se confirman órdenes de laborator groups[sample_type.id]['total_volume'] += line.product_id.sample_volume_ml or 0.0 return groups ``` -- [ ] Crear método para generar muestras por grupo -- [ ] Implementar logging para trazabilidad +- [x] Crear método para generar muestras por grupo +- [x] Implementar logging para trazabilidad ### 3. Generación de códigos de barras **Archivo:** `lims_management/models/stock_lot.py` From 5a4a65c65b54285a09c5015c79f889afebeefa44 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:38:18 -0600 Subject: [PATCH 04/11] feat(#32): Enhanced barcode generation with uniqueness - Task 3 completed - Added barcode field to stock.lot with automatic generation - Implemented unique barcode generation in format YYMMDDNNNNNNC - Added Luhn check digit for barcode validation - Handles high volume scenarios with sample type prefixes - Collision detection and retry mechanism for uniqueness - Successful test with ephemeral instance restart --- check_stock_lot_fields.py | 11 ++ .../__pycache__/stock_lot.cpython-312.pyc | Bin 5021 -> 8831 bytes lims_management/models/stock_lot.py | 103 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 check_stock_lot_fields.py diff --git a/check_stock_lot_fields.py b/check_stock_lot_fields.py new file mode 100644 index 0000000..c510ee9 --- /dev/null +++ b/check_stock_lot_fields.py @@ -0,0 +1,11 @@ +import odoo + +db_name = 'lims_demo' +registry = odoo.registry(db_name) +with registry.cursor() as cr: + env = odoo.api.Environment(cr, 1, {}) + fields = env['stock.lot']._fields.keys() + print("Stock.lot fields containing 'name' or 'barcode':") + for f in fields: + if 'name' in f or 'barcode' in f: + print(f" - {f}") \ No newline at end of file diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index 484c66eb99fd32157c65f35f1894338ae9db2ed7..ad21e4d999344c03075c8d91eb60ba39131dae8e 100644 GIT binary patch literal 8831 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 999999: + # Add prefix based on sample type to allow more barcodes + prefix_map = { + 'suero': '1', + 'edta': '2', + 'orina': '3', + 'hisopo': '4', + 'other': '9' + } + + type_prefix = '9' # default + if self.sample_type_product_id: + name_lower = self.sample_type_product_id.name.lower() + for key, val in prefix_map.items(): + if key in name_lower: + type_prefix = val + break + + sequence = int(type_prefix + str(sequence % 100000).zfill(5)) + + # Format sequence with leading zeros + sequence_str = str(sequence).zfill(6) + + # Calculate check digit using Luhn algorithm + barcode_without_check = date_prefix + sequence_str + check_digit = self._calculate_luhn_check_digit(barcode_without_check) + + final_barcode = barcode_without_check + str(check_digit) + + # Verify uniqueness + existing = self.search([ + ('barcode', '=', final_barcode), + ('id', '!=', self.id) + ], limit=1) + + if existing: + # If collision, add random component and retry + sequence = sequence * 10 + random.randint(0, 9) + sequence_str = str(sequence % 1000000).zfill(6) + barcode_without_check = date_prefix + sequence_str + check_digit = self._calculate_luhn_check_digit(barcode_without_check) + final_barcode = barcode_without_check + str(check_digit) + + return final_barcode + + def _calculate_luhn_check_digit(self, number_str): + """Calculate Luhn check digit for barcode validation""" + digits = [int(d) for d in number_str] + odd_sum = sum(digits[-1::-2]) + even_sum = sum([sum(divmod(2 * d, 10)) for d in digits[-2::-2]]) + total = odd_sum + even_sum + return (10 - (total % 10)) % 10 From e7074418f7334d52d5b1b0159c6362774a4bc445 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:38:44 -0600 Subject: [PATCH 05/11] docs(#32): Mark Task 3 as completed --- documents/plans/ISSUE32_PLAN.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/documents/plans/ISSUE32_PLAN.md b/documents/plans/ISSUE32_PLAN.md index d724061..6d744b7 100644 --- a/documents/plans/ISSUE32_PLAN.md +++ b/documents/plans/ISSUE32_PLAN.md @@ -64,11 +64,11 @@ Automatizar la generación de muestras cuando se confirman órdenes de laborator - [x] Crear método para generar muestras por grupo - [x] Implementar logging para trazabilidad -### 3. Generación de códigos de barras +### 3. Generación de códigos de barras ✅ **Archivo:** `lims_management/models/stock_lot.py` -- [ ] Mejorar el método `_compute_barcode()` para asegurar unicidad -- [ ] Agregar validación de duplicados -- [ ] Considerar prefijos por tipo de muestra +- [x] Mejorar el método `_compute_barcode()` para asegurar unicidad +- [x] Agregar validación de duplicados +- [x] Considerar prefijos por tipo de muestra ### 4. Actualizar vistas de sale.order **Archivo:** `lims_management/views/sale_order_views.xml` From f3443619cec8120d719733de2509f37a2dc4699f Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 22:42:51 -0600 Subject: [PATCH 06/11] feat(#32): Update sale.order views for generated samples - Task 4 completed - Added 'Muestras Generadas' tab in sale.order form view - Shows generated samples with barcode, type, volume, and analyses - Added action buttons for sample workflow in the embedded list - Added indicators in sale.order list view for lab requests and samples - Successful test with ephemeral instance restart --- lims_management/views/sale_order_views.xml | 29 ++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lims_management/views/sale_order_views.xml b/lims_management/views/sale_order_views.xml index 7aeec47..9ae923f 100644 --- a/lims_management/views/sale_order_views.xml +++ b/lims_management/views/sale_order_views.xml @@ -18,6 +18,31 @@ [('is_analysis', '=', True)] + + + + + + + + + + + + +