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
This commit is contained in:
Luis Ernesto Portillo Zaldivar 2025-07-14 23:46:31 -06:00
parent b88ce446c8
commit 4be56fc9f7
15 changed files with 268 additions and 163 deletions

View File

@ -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": []
}

View File

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

View File

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

View File

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

View File

@ -35,6 +35,7 @@ odoo_command = [
"-c", ODOO_CONF,
"-d", DB_NAME,
"-i", MODULES_TO_INSTALL,
"--load-language", "es_ES",
"--stop-after-init"
]

View File

@ -1,91 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!-- Demo Lab Order 1: Multiple analyses with same sample type -->
<record id="demo_lab_order_1" model="sale.order">
<field name="partner_id" ref="lims_management.demo_patient_1"/>
<field name="doctor_id" ref="lims_management.demo_doctor_1"/>
<field name="is_lab_request" eval="True"/>
<field name="state">draft</field>
</record>
<!-- Order lines - Multiple EDTA tube analyses -->
<record id="demo_lab_order_1_line_1" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_1"/>
<field name="product_id" ref="lims_management.analysis_hemograma" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<record id="demo_lab_order_1_line_2" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_1"/>
<field name="product_id" ref="lims_management.analysis_hemoglobina_glicosilada" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<!-- Demo Lab Order 2: Different sample types -->
<record id="demo_lab_order_2" model="sale.order">
<field name="partner_id" ref="lims_management.demo_patient_2"/>
<field name="doctor_id" ref="lims_management.demo_doctor_2"/>
<field name="is_lab_request" eval="True"/>
<field name="state">draft</field>
</record>
<!-- Order lines - Different sample types -->
<record id="demo_lab_order_2_line_1" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_2"/>
<field name="product_id" ref="lims_management.analysis_glucosa" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<record id="demo_lab_order_2_line_2" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_2"/>
<field name="product_id" ref="lims_management.analysis_perfil_lipidico" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<record id="demo_lab_order_2_line_3" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_2"/>
<field name="product_id" ref="lims_management.analysis_urocultivo" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<!-- Demo Lab Order 3: Mixed analyses (some without sample type) -->
<record id="demo_lab_order_3" model="sale.order">
<field name="partner_id" ref="lims_management.demo_patient_3"/>
<field name="is_lab_request" eval="True"/>
<field name="state">draft</field>
</record>
<record id="demo_lab_order_3_line_1" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_3"/>
<field name="product_id" ref="lims_management.analysis_tsh" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<record id="demo_lab_order_3_line_2" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_3"/>
<field name="product_id" ref="lims_management.analysis_t4_libre" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<!-- Demo Lab Order 4: Pediatric order -->
<record id="demo_lab_order_4" model="sale.order">
<field name="partner_id" ref="lims_management.demo_patient_minor_1"/>
<field name="doctor_id" ref="lims_management.demo_doctor_1"/>
<field name="is_lab_request" eval="True"/>
<field name="state">draft</field>
</record>
<record id="demo_lab_order_4_line_1" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_4"/>
<field name="product_id" ref="lims_management.analysis_hemograma" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<record id="demo_lab_order_4_line_2" model="sale.order.line">
<field name="order_id" ref="demo_lab_order_4"/>
<field name="product_id" ref="lims_management.analysis_parasitos_heces" eval="obj().product_variant_id.id"/>
<field name="product_uom_qty">1</field>
</record>
<!--
Note: Sale orders are created via Python script in create_lab_requests.py
This file is kept for future non-order demo data if needed
-->
</odoo>

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@
<field name="volume_ml" string="Volumen (ml)"/>
<field name="analysis_names" string="Análisis"/>
<field name="state" string="Estado"/>
<button name="action_receive" string="Recibir" type="object"
<button name="action_collect" string="Recolectar" type="object"
class="btn-primary" invisible="state != 'pending_collection'"/>
</list>
</field>
@ -56,7 +56,7 @@
<field name="doctor_id"/>
</xpath>
<xpath expr="//field[@name='state']" position="before">
<field name="is_lab_request" optional="show" string="Lab Request"/>
<field name="is_lab_request" optional="show" string="Orden Lab"/>
<field name="generated_sample_ids" widget="many2many_tags" optional="hide" string="Muestras"/>
</xpath>
</field>

View File

@ -7,15 +7,15 @@
<field name="name">lab.sample.list</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<list string="Lab Samples">
<field name="name"/>
<field name="patient_id"/>
<field name="product_id" string="Sample Type"/>
<field name="sample_type_product_id"/>
<field name="collection_date"/>
<field name="collector_id"/>
<field name="container_type" optional="hide"/>
<field name="state" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-muted="state == 'stored' or state == 'disposed'" widget="badge"/>
<list string="Muestras de Laboratorio">
<field name="name" string="Código"/>
<field name="patient_id" string="Paciente"/>
<field name="product_id" string="Tipo de Muestra"/>
<field name="sample_type_product_id" string="Tipo de Muestra"/>
<field name="collection_date" string="Fecha de Recolección"/>
<field name="collector_id" string="Recolectado por"/>
<field name="container_type" optional="hide" string="Tipo Contenedor (Obsoleto)"/>
<field name="state" string="Estado" decoration-success="state == 'analyzed'" decoration-info="state == 'in_process'" decoration-muted="state == 'stored' or state == 'disposed'" widget="badge"/>
</list>
</field>
</record>
@ -25,14 +25,15 @@
<field name="name">lab.sample.form</field>
<field name="model">stock.lot</field>
<field name="arch" type="xml">
<form string="Lab Sample">
<form string="Muestra de Laboratorio">
<header>
<button name="action_collect" string="Recolectar" type="object" class="oe_highlight" invisible="state != 'pending_collection'"/>
<button name="action_receive" string="Recibir" type="object" class="oe_highlight" invisible="state != 'collected'"/>
<button name="action_start_analysis" string="Iniciar Análisis" type="object" class="oe_highlight" invisible="state != 'received'"/>
<button name="action_complete_analysis" string="Completar Análisis" type="object" class="oe_highlight" invisible="state != 'in_process'"/>
<button name="action_store" string="Almacenar" type="object" invisible="state == 'stored' or state == 'disposed'"/>
<button name="action_store" string="Almacenar" type="object" invisible="state not in ['analyzed', 'in_process', 'received']"/>
<button name="action_dispose" string="Desechar" type="object" invisible="state == 'disposed'"/>
<field name="state" widget="statusbar" statusbar_visible="collected,received,in_process,analyzed,stored"/>
<field name="state" widget="statusbar" statusbar_visible="pending_collection,collected,received,in_process,analyzed,stored"/>
</header>
<sheet>
<div class="oe_title">
@ -42,24 +43,29 @@
</div>
<group>
<group>
<field name="patient_id" readonly="state != 'collected'"/>
<field name="patient_id" readonly="state not in ['pending_collection', 'collected']"/>
<field name="doctor_id" readonly="state not in ['pending_collection', 'collected']"/>
<field name="origin" readonly="1"/>
<field name="request_id"
readonly="state != 'collected'"
readonly="state not in ['pending_collection', 'collected']"
domain="[('is_lab_request', '=', True), '|', ('partner_id', '=', False), ('partner_id', '=', patient_id)]"/>
<field name="product_id"
string="Sample Type"
domain="[('is_sample_type', '=', True)]"
options="{'no_create': True, 'no_create_edit': True}"
readonly="state != 'collected'"/>
readonly="state not in ['pending_collection', 'collected']"/>
</group>
<group>
<field name="collection_date" readonly="state != 'collected'"/>
<field name="collector_id" readonly="state != 'collected'"/>
<field name="barcode" readonly="1"/>
<field name="collection_date" readonly="state not in ['pending_collection', 'collected']"/>
<field name="collector_id" readonly="state not in ['pending_collection', 'collected']"/>
<field name="sample_type_product_id"
readonly="state != 'collected'"
readonly="state not in ['pending_collection', 'collected']"
options="{'no_create': True, 'no_create_edit': True}"/>
<field name="volume_ml" readonly="1"/>
<field name="analysis_names" readonly="1"/>
<field name="container_type"
readonly="state != 'collected'"
readonly="state not in ['pending_collection', 'collected']"
invisible="sample_type_product_id != False"/>
</group>
</group>

View File

@ -0,0 +1,136 @@
import odoo
def test_sample_generation(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
print("=== TESTING AUTOMATIC SAMPLE GENERATION ===\n")
# Create test patient
patient = env['res.partner'].create({
'name': 'Test Patient for Validation',
'is_patient': True,
'patient_identifier': 'P-TEST01'
})
print(f"Created patient: {patient.name}")
# Create test doctor
doctor = env['res.partner'].create({
'name': 'Dr. Test Validation',
'is_doctor': True,
'doctor_license': 'L-TEST01'
})
print(f"Created doctor: {doctor.name}")
# Get or create sample types
sample_types = {}
# EDTA Tube
edta = env['product.template'].search([('name', 'like', 'EDTA'), ('is_sample_type', '=', True)], limit=1)
if not edta:
edta = env['product.template'].create({
'name': 'Test EDTA Tube',
'is_sample_type': True,
'type': 'consu'
})
sample_types['edta'] = edta
# Serum Tube
serum = env['product.template'].search([('name', 'like', 'Suero'), ('is_sample_type', '=', True)], limit=1)
if not serum:
serum = env['product.template'].create({
'name': 'Test Serum Tube',
'is_sample_type': True,
'type': 'consu'
})
sample_types['serum'] = serum
# Create test analyses
analyses = []
# Analysis 1 - requires EDTA
analysis1 = env['product.template'].create({
'name': 'Test Hemograma',
'is_analysis': True,
'type': 'service',
'required_sample_type_id': edta.id,
'sample_volume_ml': 3.0
})
analyses.append(analysis1)
print(f"Created analysis: {analysis1.name} (requires {edta.name}, {analysis1.sample_volume_ml} ml)")
# Analysis 2 - also requires EDTA
analysis2 = env['product.template'].create({
'name': 'Test HbA1c',
'is_analysis': True,
'type': 'service',
'required_sample_type_id': edta.id,
'sample_volume_ml': 2.0
})
analyses.append(analysis2)
print(f"Created analysis: {analysis2.name} (requires {edta.name}, {analysis2.sample_volume_ml} ml)")
# Analysis 3 - requires Serum
analysis3 = env['product.template'].create({
'name': 'Test Glucose',
'is_analysis': True,
'type': 'service',
'required_sample_type_id': serum.id,
'sample_volume_ml': 1.0
})
analyses.append(analysis3)
print(f"Created analysis: {analysis3.name} (requires {serum.name}, {analysis3.sample_volume_ml} ml)")
# Analysis 4 - no sample type defined
analysis4 = env['product.template'].create({
'name': 'Test Special Analysis',
'is_analysis': True,
'type': 'service'
})
analyses.append(analysis4)
print(f"Created analysis: {analysis4.name} (no sample type defined)")
# Create lab order
print("\n--- Creating Lab Order ---")
order = env['sale.order'].create({
'partner_id': patient.id,
'doctor_id': doctor.id,
'is_lab_request': True,
'order_line': [(0, 0, {
'product_id': a.product_variant_id.id,
'product_uom_qty': 1
}) for a in analyses]
})
print(f"Created order: {order.name}")
print(f"Order lines: {len(order.order_line)}")
# Confirm order - this should trigger automatic sample generation
print("\n--- Confirming Order (triggering sample generation) ---")
order.action_confirm()
print(f"Order state: {order.state}")
# Check generated samples
print(f"\n--- Generated Samples: {len(order.generated_sample_ids)} ---")
for sample in order.generated_sample_ids:
print(f"\nSample: {sample.name}")
print(f" Barcode: {sample.barcode}")
print(f" Sample Type: {sample.sample_type_product_id.name if sample.sample_type_product_id else 'None'}")
print(f" Total Volume: {sample.volume_ml} ml")
print(f" Analyses: {sample.analysis_names}")
print(f" State: {sample.state}")
# Check messages
print("\n--- Order Messages ---")
messages = order.message_ids.filtered(lambda m: m.body and m.message_type == 'notification')
for msg in messages[:5]:
print(f"Message: {msg.body[:200]}...")
print("\n=== TEST COMPLETED ===")
return order
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
test_sample_generation(cr)
cr.commit()