From 4be56fc9f7f7fd71f885e7a18c48a2800ceeb264 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Mon, 14 Jul 2025 23:46:31 -0600 Subject: [PATCH] fix(#32): Spanish translations and workflow fixes - Fixed missing action_collect method for pending_collection state - Updated all model field labels to Spanish - Updated view labels and strings to Spanish - Fixed readonly conditions for pending_collection state - Added barcode and new fields to stock.lot views - Updated sale.order embedded view with correct button - Added 5-minute timeout note to CLAUDE.md - Removed problematic demo sale.order XML records - Updated test script location guidance in CLAUDE.md - Marked all acceptance criteria as completed in plan --- .claude/settings.local.json | 6 +- CLAUDE.md | 8 ++ create_lab_requests.py | 79 ++++++---- documents/plans/ISSUE32_PLAN.md | 16 +-- init_odoo.py | 1 + .../demo/z_automatic_generation_demo.xml | 92 +----------- .../__pycache__/product.cpython-312.pyc | Bin 2960 -> 2974 bytes .../__pycache__/sale_order.cpython-312.pyc | Bin 7160 -> 7204 bytes .../__pycache__/stock_lot.cpython-312.pyc | Bin 8831 -> 9318 bytes lims_management/models/product.py | 4 +- lims_management/models/sale_order.py | 10 +- lims_management/models/stock_lot.py | 31 ++-- lims_management/views/sale_order_views.xml | 4 +- lims_management/views/stock_lot_views.xml | 44 +++--- test/test_sample_generation.py | 136 ++++++++++++++++++ 15 files changed, 268 insertions(+), 163 deletions(-) create mode 100644 test/test_sample_generation.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index b8988b6..e9b9f67 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -10,7 +10,11 @@ "Bash(git stash:*)", "Bash(git commit:*)", "Bash(docker-compose up:*)", - "Bash(docker:*)" + "Bash(docker:*)", + "Bash(curl:*)", + "Bash(mkdir:*)", + "Bash(mv:*)", + "Bash(rm:*)" ], "deny": [] } diff --git a/CLAUDE.md b/CLAUDE.md index c735b10..064b638 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,6 +27,8 @@ docker-compose logs odoo_init docker-compose down -v ``` +**IMPORTANT**: Odoo initialization takes approximately 5 minutes. When using docker-compose commands, set timeout to 5 minutes (300000ms) to avoid premature timeouts. + ### Instance Persistence Policy After successful installation/update, the instance must remain active for user validation. Do NOT stop the instance until user explicitly confirms testing is complete. @@ -188,10 +190,16 @@ STATE_CANCELLED = 'cancelled' - Use for basic records without complex dependencies - Place in `lims_management/demo/` - Use `noupdate="1"` to prevent reloading +- **IMPORTANT**: Do NOT create sale.order records in XML demo files - use Python scripts instead #### Python Scripts (Complex Data) For data with dependencies or business logic: +#### Test Scripts +- **IMPORTANT**: Always create test scripts inside the `test/` folder within the project directory +- Example: `test/test_sample_generation.py` +- This ensures scripts are properly organized and accessible + 1. Create script: ```python import odoo diff --git a/create_lab_requests.py b/create_lab_requests.py index 227645e..226bc25 100644 --- a/create_lab_requests.py +++ b/create_lab_requests.py @@ -16,34 +16,59 @@ def create_lab_requests(cr): except Exception: pass - # Get patient and doctor - patient1 = env.ref('lims_management.demo_patient_1') - doctor1 = env.ref('lims_management.demo_doctor_1') - patient2 = env.ref('lims_management.demo_patient_2') + try: + # Get patients and doctors - using search instead of ref to be more robust + patient1 = env['res.partner'].search([('patient_identifier', '=', 'P-A87B01'), ('is_patient', '=', True)], limit=1) + patient2 = env['res.partner'].search([('patient_identifier', '=', 'P-C45D02'), ('is_patient', '=', True)], limit=1) + doctor1 = env['res.partner'].search([('doctor_license', '=', 'L-98765'), ('is_doctor', '=', True)], limit=1) + + if not patient1: + print("Warning: Patient 1 not found, skipping lab requests creation") + return + + # Get analysis products - using search instead of ref + hemograma = env['product.template'].search([('name', '=', 'Hemograma Completo'), ('is_analysis', '=', True)], limit=1) + perfil_lipidico = env['product.template'].search([('name', '=', 'Perfil Lipídico'), ('is_analysis', '=', True)], limit=1) + glucosa = env['product.template'].search([('name', '=', 'Glucosa en Sangre'), ('is_analysis', '=', True)], limit=1) + urocultivo = env['product.template'].search([('name', '=', 'Urocultivo'), ('is_analysis', '=', True)], limit=1) + + # Create Lab Request 1 - Multiple analyses with same sample type + if patient1 and hemograma and perfil_lipidico: + order1 = env['sale.order'].create({ + 'partner_id': patient1.id, + 'doctor_id': doctor1.id if doctor1 else False, + 'is_lab_request': True, + 'order_line': [ + (0, 0, {'product_id': hemograma.product_variant_id.id, 'product_uom_qty': 1}), + (0, 0, {'product_id': perfil_lipidico.product_variant_id.id, 'product_uom_qty': 1}) + ] + }) + print(f"Created Lab Order 1: {order1.name}") + + # Confirm the order to test automatic sample generation + order1.action_confirm() + print(f"Confirmed Lab Order 1. Generated samples: {len(order1.generated_sample_ids)}") - # Get analysis products - hemograma = env.ref('lims_management.analysis_hemograma') - perfil_lipidico = env.ref('lims_management.analysis_perfil_lipidico') - - # Create Lab Request 1 - env['sale.order'].create({ - 'partner_id': patient1.id, - 'doctor_id': doctor1.id, - 'is_lab_request': True, - 'order_line': [ - (0, 0, {'product_id': hemograma.product_variant_id.id, 'product_uom_qty': 1}), - (0, 0, {'product_id': perfil_lipidico.product_variant_id.id, 'product_uom_qty': 1}) - ] - }) - - # Create Lab Request 2 - env['sale.order'].create({ - 'partner_id': patient2.id, - 'is_lab_request': True, - 'order_line': [ - (0, 0, {'product_id': hemograma.product_variant_id.id, 'product_uom_qty': 1}) - ] - }) + # Create Lab Request 2 - Different sample types + if patient2 and glucosa and urocultivo: + order2 = env['sale.order'].create({ + 'partner_id': patient2.id, + 'is_lab_request': True, + 'order_line': [ + (0, 0, {'product_id': glucosa.product_variant_id.id, 'product_uom_qty': 1}), + (0, 0, {'product_id': urocultivo.product_variant_id.id, 'product_uom_qty': 1}) + ] + }) + print(f"Created Lab Order 2: {order2.name}") + + # Confirm to test automatic sample generation with different types + order2.action_confirm() + print(f"Confirmed Lab Order 2. Generated samples: {len(order2.generated_sample_ids)}") + + except Exception as e: + print(f"Error creating lab requests: {str(e)}") + import traceback + traceback.print_exc() if __name__ == '__main__': db_name = 'lims_demo' diff --git a/documents/plans/ISSUE32_PLAN.md b/documents/plans/ISSUE32_PLAN.md index a3a1e03..dbf1845 100644 --- a/documents/plans/ISSUE32_PLAN.md +++ b/documents/plans/ISSUE32_PLAN.md @@ -153,14 +153,14 @@ graph TD ## 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 +1. [x] Al confirmar una orden de laboratorio, se generan automáticamente las muestras necesarias +2. [x] Los análisis que requieren el mismo tipo de muestra se agrupan en un solo contenedor +3. [x] Cada muestra tiene un código de barras único +4. [x] Se muestra claramente qué muestras fueron generadas para cada orden +5. [x] Se manejan adecuadamente los análisis sin tipo de muestra definido +6. [x] El sistema registra un log de la generación para auditoría +7. [ ] La funcionalidad se puede deshabilitar si es necesario (opcional - no implementado) +8. [x] No afecta el rendimiento de confirmación de órdenes regulares ## Estimación de Tiempo diff --git a/init_odoo.py b/init_odoo.py index d1c5a08..7ab37ef 100644 --- a/init_odoo.py +++ b/init_odoo.py @@ -35,6 +35,7 @@ odoo_command = [ "-c", ODOO_CONF, "-d", DB_NAME, "-i", MODULES_TO_INSTALL, + "--load-language", "es_ES", "--stop-after-init" ] diff --git a/lims_management/demo/z_automatic_generation_demo.xml b/lims_management/demo/z_automatic_generation_demo.xml index bba7732..dc5fdf6 100644 --- a/lims_management/demo/z_automatic_generation_demo.xml +++ b/lims_management/demo/z_automatic_generation_demo.xml @@ -1,91 +1,7 @@ - - - - - - - draft - - - - - - - 1 - - - - - - 1 - - - - - - - - draft - - - - - - - 1 - - - - - - 1 - - - - - - 1 - - - - - - - draft - - - - - - 1 - - - - - - 1 - - - - - - - - draft - - - - - - 1 - - - - - - 1 - - + \ No newline at end of file diff --git a/lims_management/models/__pycache__/product.cpython-312.pyc b/lims_management/models/__pycache__/product.cpython-312.pyc index 7c7c8226d8bd29196aa4b427262850bdb6a78d9e..59f71a22718db5dffcdefd962fc77b369630336b 100644 GIT binary patch delta 122 zcmbOrK2MzYG%qg~0}!~sD9xC-k(ZH0RLHehAtbXPUm+z`!M8NExTGktDtPidCJC#e z)Pkba;?%s7M1|5kg%YTm?*nd diff --git a/lims_management/models/__pycache__/sale_order.cpython-312.pyc b/lims_management/models/__pycache__/sale_order.cpython-312.pyc index 560f98c1a5524b768133de1e9d08df358e2fd629..8e543021c868d5b78b781d2cd08821ea392ab152 100644 GIT binary patch delta 373 zcmZ{fKQ9D97{>1w=U>+53M+A!>>Hh2qR?=)9EE=&p*Ch_d1ZI!%OG6Ckhm=%&AK!}Dxggy0km+6AyOhiAw&ek&;K)<5a0@QLO_WKDW;kg z&Czf3!u)W*qEd;;QGb+~rW5$OYF(RImlcv-9&%}mxH)m^>w3*btq9`p^i*?@JVc3E z4@hJuQ7quEZCqLZe`53Kbn1GiP-zQFqW&y1oyl4NRFi}WP#ma8tSGjkLGR$q-t|y( pCSdR5BYx##>usu3h0Dr~cje_{)44a7dyU-PH?Ome@`3Sd&!2mIgf9R9 delta 311 zcmZ2t@xz?=G%qg~0}wRaDb1L%k#|31yl1gOqJmFiQhrflNq$kKLQra9X=-taTa{Z# zYH~(iW^!VVLRw~OPKrWFzCvb7YF-hp(o^$Nft~{tvs<@bYB#|bZoK`6!ytKAUe1CWeW40BpqW0^2@pB z{LZ<*d+y1782fps?EBJEuLPg^U#+=c8rI6PWc{;mj@o?cj&ssX$R^Q-Q~Slsq_Vm5 zGC`gssa@x!%LP-Kk=32RYs~Xv$cuKXyDr-bVRiRo%w}~DFrQZU0!Eg4qR)smd$6u{ zz>r5RYM5M+RVt4t3C0zZagB*D?Votu6G~E}x=DrU=m}JfbCv2(@~$`<&sqW4agZkG z@>LGgBumj`QoEMbqt2);=QSv|7G_N=L6=LXtOUavlNRL+f*z`xN+IUS#|C0k2f*(z z7L`QKHKZ9VjXbBtHJvV3_A}6d)xNmbqq zRZzQODkjbOivf-4vnn{oqK=$%k!vR94j|^-F>spN0?pZVwkSUK)E+EmELTsOEIB{I z%0L+?OXMp_rCSQ)gxccUYWhsJ8`{t;zsY`>y?u~xTgg14(&s+ zktf7*nxxuks>+%!r#8o9iSK}#JOVIwiSRD?1vx#w88Gfu0>F0SFHgJ;D+VV`g`2XX zE2$+zGvqX9Nop8{5-nJQ?-9qmhkP%BZER+L>Wz0`c3oc2qvU-1u#wolyolCfh3YL6 zo&?yu2LO-Lgz4qb7*1uFjp79qlA^_=Z@29AodEmMwmx6H9rFS)%}6tY%Iw7dlD8F- zaT~Y;k@3l{Q?TGY+0T6+5z;4S%ibV*_K&hJ$Wy}*$dyd%^KfH&w1oE|@gTymxK^Gd zXS1zaDIwG1^$LZ&n*Fn)%ITek@B`92#p9y1CcZ6(>_LPfgy#{00NT5+5a~A@Mt&S1 zN_awKYTCPaH>`4Q_)nj}Vd1m5bQx6P$vGa!g77!@JPji|{N$FT!&O2N3WmIXBbc8PQKtK8n66^p&VIO?B1a_*C*^ z2&VAL4Rtr*O*|)U`0G}NZdWyB1}~1Sjjgv2+^iZDeR5}g?S{YOeu<;PwQ}f=PpW9l z1TTiy!W-_Y4Sz(e%GJ%Wo56vV@!Nq&=E$u;I&-OJGqO< z;$QjAu+2G5?z(Nxc7%>cR$Jb>;}(}17q?lc*Ez~mn&NI0`W`|UAq4Qiy>Ef3R*LTD aABf?maNxL|w7qtE_00J*e@kfRss91~#>LkF delta 1234 zcmYk6O>7%Q6vuaB@7hk*aY!7Od>F6oIL)SQ)TE&_$ZplxjY2U=gx#b>plI!#)TYj^ zJG*XVtE8zEm2!cELF$1^CAhSaP{B8DzyYa%)Iuf9mP!=|KE#2jgi9_k&uNPH@SFd< z_n$X!S2O$T=91+D)s^H#kG;QRWP*FEI9%kI7?}GiB=8^GV&TxC~H3 z1txtj8ImF;m5fdlQzvC9r7RT-n0{^h#Oat(%skdix?&#_TbM_B6<;V}!=&HZgO@9w zxLzpWteMv}DWRBH=H(`<*}$ZgEv1=e85dSfWjEnEl}Ml5i`$K5C1crB zjLgNCNM6Gg&lwn`xUQKB69h84nZv~5r(vkQmEPk0j=W}=SW)Ts{KszbN#o3rDOaxwf{m^`3!ekT!gYVQlK{j^QcJel8kJIZP%YHVrX{ii=oN4FcDhqya_xXYjf1@7V2K<7%Gn)yYTR(E7yn zsg0?-gX8zTaT*Q{_dZ+m9Nc!;+lAHBTO!vUT=!iMYy@gfZ_U$7i=lntsaw4s^Z9e-VIN?f?J) diff --git a/lims_management/models/product.py b/lims_management/models/product.py index c3eb347..0f23f1c 100644 --- a/lims_management/models/product.py +++ b/lims_management/models/product.py @@ -29,8 +29,8 @@ class ProductTemplate(models.Model): ) is_sample_type = fields.Boolean( - string="Is a Sample Type", - help="Check if this product represents a type of laboratory sample container." + string="Es Tipo de Muestra", + help="Marcar si este producto representa un tipo de contenedor de muestra de laboratorio." ) required_sample_type_id = fields.Many2one( diff --git a/lims_management/models/sale_order.py b/lims_management/models/sale_order.py index e3cc54b..4f2227f 100644 --- a/lims_management/models/sale_order.py +++ b/lims_management/models/sale_order.py @@ -9,17 +9,17 @@ class SaleOrder(models.Model): _inherit = 'sale.order' is_lab_request = fields.Boolean( - string="Is a Laboratory Request", + string="Es Orden de Laboratorio", default=False, copy=False, - help="Technical field to identify if the sale order is a laboratory request." + help="Campo técnico para identificar si la orden de venta es una solicitud de laboratorio." ) doctor_id = fields.Many2one( 'res.partner', - string="Referring Doctor", + string="Médico Referente", domain="[('is_doctor', '=', True)]", - help="The doctor who referred the patient for this laboratory request." + help="El médico que refirió al paciente para esta solicitud de laboratorio." ) generated_sample_ids = fields.Many2many( @@ -30,7 +30,7 @@ class SaleOrder(models.Model): string='Muestras Generadas', domain="[('is_lab_sample', '=', True)]", readonly=True, - help="Laboratory samples automatically generated when this order was confirmed" + help="Muestras de laboratorio generadas automáticamente cuando se confirmó esta orden" ) def action_confirm(self): diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index 5de18a4..9ec953e 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -6,7 +6,7 @@ import random class StockLot(models.Model): _inherit = 'stock.lot' - is_lab_sample = fields.Boolean(string='Is a Laboratory Sample') + is_lab_sample = fields.Boolean(string='Es Muestra de Laboratorio') barcode = fields.Char( string='Código de Barras', @@ -18,25 +18,25 @@ class StockLot(models.Model): patient_id = fields.Many2one( 'res.partner', - string='Patient', + string='Paciente', domain="[('is_patient', '=', True)]" ) request_id = fields.Many2one( 'sale.order', - string='Lab Request', + string='Orden de Laboratorio', domain="[('is_lab_request', '=', True)]" ) - collection_date = fields.Datetime(string='Collection Date') + collection_date = fields.Datetime(string='Fecha de Recolección') container_type = fields.Selection([ - ('serum_tube', 'Serum Tube'), - ('edta_tube', 'EDTA Tube'), - ('swab', 'Swab'), - ('urine', 'Urine Container'), - ('other', 'Other') - ], string='Container Type (Legacy)', help='Deprecated field, use sample_type_product_id instead') + ('serum_tube', 'Tubo de Suero'), + ('edta_tube', 'Tubo EDTA'), + ('swab', 'Hisopo'), + ('urine', 'Contenedor de Orina'), + ('other', 'Otro') + ], string='Tipo de Contenedor (Obsoleto)', help='Campo obsoleto, use sample_type_product_id en su lugar') sample_type_product_id = fields.Many2one( 'product.template', @@ -47,7 +47,7 @@ class StockLot(models.Model): collector_id = fields.Many2one( 'res.users', - string='Collected by', + string='Recolectado por', default=lambda self: self.env.user ) @@ -83,19 +83,28 @@ class StockLot(models.Model): ('disposed', 'Desechada') ], string='Estado', default='collected', tracking=True) + def action_collect(self): + """Mark sample as collected""" + self.write({'state': 'collected', 'collection_date': fields.Datetime.now()}) + def action_receive(self): + """Mark sample as received in laboratory""" self.write({'state': 'received'}) def action_start_analysis(self): + """Start analysis process""" self.write({'state': 'in_process'}) def action_complete_analysis(self): + """Mark analysis as completed""" self.write({'state': 'analyzed'}) def action_store(self): + """Store the sample""" self.write({'state': 'stored'}) def action_dispose(self): + """Dispose of the sample""" self.write({'state': 'disposed'}) @api.onchange('sample_type_product_id') diff --git a/lims_management/views/sale_order_views.xml b/lims_management/views/sale_order_views.xml index 9ae923f..9c0945f 100644 --- a/lims_management/views/sale_order_views.xml +++ b/lims_management/views/sale_order_views.xml @@ -30,7 +30,7 @@ -