feature/6-lab-requests #26

Merged
luis_portillo merged 9 commits from feature/6-lab-requests into dev 2025-07-14 09:21:53 +00:00
29 changed files with 1497 additions and 5 deletions

171
GEMINI.md
View File

@ -240,4 +240,175 @@ Esto envía la cadena `"{'default_categ_id': ref(...)}"` al cliente, que no pued
}"/>
```
Al usar `eval`, Odoo ejecuta la expresión en el servidor, reemplaza `ref(...)` por el ID numérico correspondiente, y envía un diccionario JSON válido al cliente.
### Herencia de Vistas y XPath
Al heredar vistas para modificarlas, es crucial que las expresiones `XPath` sean precisas. Un error común es hacer referencia a campos o estructuras que han cambiado en la nueva versión de Odoo.
**Ejemplo de Error:**
Al intentar modificar las líneas de una orden de venta (`sale.order`), una expresión XPath que funcionaba en versiones anteriores puede fallar en Odoo 18.
**Expresión Incorrecta (para Odoo < 18):**
```xml
<xpath expr="//field[@name='order_line']/tree/field[@name='product_template_id']" position="attributes">
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
</xpath>
```
Esta expresión falla por dos razones:
1. La vista de líneas ahora usa `<list>` en lugar de `<tree>`.
2. El campo del producto en las líneas de venta es `product_id`, no `product_template_id`.
**Expresión Correcta (para Odoo 18):**
Para hacer la expresión más robusta y compatible, se puede usar `//` para buscar en cualquier nivel descendiente.
```xml
<xpath expr="//field[@name='order_line']//field[@name='product_id']" position="attributes">
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
</xpath>
```
Esta expresión busca el campo `product_id` dentro del campo `order_line`, sin importar si está dentro de una etiqueta `<list>` o `<tree>`, haciendo la herencia más resistente a cambios menores de estructura.
---
## Consultar la Base de Datos con un Script
Interactuar con la base de datos directamente usando `psql` a través de `docker-compose exec` puede ser complicado debido a la forma en que el shell maneja las comillas. Una alternativa más robusta y confiable es utilizar un script de Python que aproveche el ORM de Odoo.
### Procedimiento
1. **Crear un Script de Python:**
Crea un script que se conecte a la base de datos y ejecute la consulta deseada.
**Ejemplo (`verify_products.py`):**
```python
import odoo
import json
def verify_lab_order_products(cr):
cr.execute("""
SELECT
so.name AS order_name,
sol.id AS line_id,
pt.name->>'en_US' AS product_name,
pt.is_analysis
FROM
sale_order so
JOIN
sale_order_line sol ON so.id = sol.order_id
JOIN
product_product pp ON sol.product_id = pp.id
JOIN
product_template pt ON pp.product_tmpl_id = pt.id
WHERE
so.is_lab_request = TRUE;
""")
return cr.fetchall()
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
results = verify_lab_order_products(cr)
print(json.dumps(results, indent=4))
```
2. **Copiar el Script al Contenedor:**
Usa el comando `docker cp` para copiar el script al contenedor de Odoo.
```bash
docker cp verify_products.py lims_odoo:/tmp/verify_products.py
```
3. **Ejecutar el Script:**
Ejecuta el script dentro del contenedor usando `docker-compose exec`.
```bash
docker-compose exec odoo python3 /tmp/verify_products.py
```
Este método evita los problemas de entrecomillado y permite ejecutar consultas complejas de manera confiable.
---
## Creación de Datos de Demostración Complejos
Cuando los datos de demostración tienen dependencias complejas o requieren lógica de negocio (por ejemplo, cambiar el estado de un registro, o crear registros relacionados que dependen de otros), el uso de archivos XML puede ser limitado y propenso a errores de carga.
En estos casos, es preferible utilizar un script de Python para crear los datos de demostración.
### Procedimiento
1. **Crear un Script de Python:**
Crea un script que utilice el ORM de Odoo para crear los registros de demostración. Esto permite utilizar la lógica de negocio de los modelos, como los métodos `create` y `write`, y buscar registros existentes con `search` y `ref`.
**Ejemplo (`create_lab_requests.py`):**
```python
import odoo
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Eliminar órdenes de venta de demostración no deseadas
unwanted_orders = env['sale.order'].search([('name', 'in', ['S00001', ...])])
for order in unwanted_orders:
try:
order.action_cancel()
except Exception:
pass
try:
unwanted_orders.unlink()
except Exception:
pass
# Crear solicitudes de laboratorio
patient1 = env.ref('lims_management.demo_patient_1')
doctor1 = env.ref('lims_management.demo_doctor_1')
hemograma = env.ref('lims_management.analysis_hemograma')
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})
]
})
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
create_lab_requests(cr)
cr.commit()
```
2. **Integrar el Script en la Inicialización:**
Modifica el script `init_odoo.py` para que ejecute el script de creación de datos después de que Odoo haya terminado de instalar los módulos.
**En `docker-compose.yml`**, asegúrate de que el script esté disponible en el contenedor `odoo_init`:
```yaml
volumes:
- ./create_lab_requests.py:/app/create_lab_requests.py
```
**En `init_odoo.py`**, añade la lógica para ejecutar el script:
```python
# --- Lógica para crear datos de demostración personalizados ---
print("Creando datos de demostración complejos...")
sys.stdout.flush()
with open("/app/create_lab_requests.py", "r") as f:
script_content = f.read()
create_requests_command = f"""
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
{script_content}
EOF
"""
result = subprocess.run(
create_requests_command,
shell=True,
capture_output=True,
text=True,
check=False
)
```
Este enfoque proporciona un control total sobre la creación de datos de demostración y evita los problemas de dependencia y orden de carga de los archivos XML.

26
README.md Normal file
View File

@ -0,0 +1,26 @@
# Proyecto de Laboratorio Clínico (LIMS)
Este proyecto contiene el desarrollo de un módulo de gestión de laboratorios clínicos para Odoo 18.
## Desarrollo
### Hook de Pre-Commit
Para asegurar la integridad de los commits y evitar que se suban cambios incompletos, este repositorio incluye un hook de `pre-commit`.
**Propósito:**
El hook revisa automáticamente si existen archivos modificados que no han sido agregados al "staging area" cada vez que se intenta realizar un commit. Si se detectan cambios sin agregar, el commit es abortado.
**Instalación (Obligatoria para todos los desarrolladores):**
Para activar el hook en tu copia local del repositorio, ejecuta los siguientes comandos desde la raíz del proyecto:
```bash
# Copia el hook desde el directorio de scripts a tu directorio local de git
cp scripts/hooks/pre-commit .git/hooks/
# Dale permisos de ejecución (necesario en macOS y Linux)
chmod +x .git/hooks/pre-commit
```
Una vez instalado, el hook se ejecutará en cada commit, ayudando a mantener un historial de cambios limpio y completo.

53
create_lab_requests.py Normal file
View File

@ -0,0 +1,53 @@
import odoo
import json
def create_lab_requests(cr):
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
# Delete unwanted demo sale orders
unwanted_orders = env['sale.order'].search([('name', 'in', ['S00001', 'S00002', 'S00003', 'S00004', 'S00005', 'S00006', 'S00007', 'S00008', 'S00009', 'S00010', 'S00011', 'S00012', 'S00013', 'S00014', 'S00015', 'S00016', 'S00017', 'S00018', 'S00019', 'S00020', 'S00021', 'S00022'])])
for order in unwanted_orders:
try:
order.action_cancel()
except Exception:
pass
try:
unwanted_orders.unlink()
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')
# 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})
]
})
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
create_lab_requests(cr)
cr.commit()

View File

@ -24,6 +24,7 @@ services:
- ./lims_management:/mnt/extra-addons/lims_management
- ./odoo.conf:/etc/odoo/odoo.conf
- ./init_odoo.py:/app/init_odoo.py
- ./create_lab_requests.py:/app/create_lab_requests.py
command: ["/usr/bin/python3", "/app/init_odoo.py"]
environment:
HOST: db

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,169 @@
{
"account_tag_ids": [],
"active": true,
"activity_date_deadline": false,
"activity_exception_decoration": false,
"activity_exception_icon": false,
"activity_ids": [],
"activity_state": false,
"activity_summary": false,
"activity_type_icon": false,
"activity_type_id": false,
"activity_user_id": false,
"analysis_type": false,
"attribute_line_ids": [],
"barcode": false,
"can_image_1024_be_zoomed": false,
"categ_id": [
10,
"All / Home Construction"
],
"color": 0,
"combo_ids": [],
"company_id": false,
"cost_currency_id": [
1,
"USD"
],
"cost_method": "standard",
"create_date": "2025-07-14 07:23:12",
"create_uid": [
1,
"OdooBot"
],
"currency_id": [
1,
"USD"
],
"default_code": false,
"description": false,
"description_picking": false,
"description_pickingin": false,
"description_pickingout": false,
"description_purchase": false,
"description_sale": false,
"display_name": "Furniture Assembly",
"expense_policy": "no",
"fiscal_country_codes": "US",
"has_available_route_ids": false,
"has_configurable_attributes": false,
"has_message": true,
"id": 29,
"image_1024": false,
"image_128": false,
"image_1920": false,
"image_256": false,
"image_512": false,
"incoming_qty": 0,
"invoice_policy": "order",
"is_analysis": false,
"is_favorite": false,
"is_product_variant": false,
"is_storable": false,
"list_price": 2000,
"location_id": false,
"lot_valuated": false,
"message_attachment_count": 0,
"message_follower_ids": [],
"message_has_error": false,
"message_has_error_counter": 0,
"message_has_sms_error": false,
"message_ids": [
188,
114
],
"message_is_follower": false,
"message_needaction": false,
"message_needaction_counter": 0,
"message_partner_ids": [],
"my_activity_date_deadline": false,
"name": "Furniture Assembly",
"nbr_moves_in": 0,
"nbr_moves_out": 0,
"nbr_reordering_rules": 0,
"optional_product_ids": [],
"outgoing_qty": 0,
"packaging_ids": [],
"pricelist_item_count": 0,
"product_document_count": 0,
"product_document_ids": [],
"product_properties": [],
"product_tag_ids": [],
"product_tooltip": "Invoice ordered quantities as soon as this service is sold.",
"product_variant_count": 1,
"product_variant_id": [
38,
"Furniture Assembly"
],
"product_variant_ids": [
38
],
"property_account_expense_id": false,
"property_account_income_id": false,
"property_stock_inventory": [
14,
"Virtual Locations/Inventory adjustment"
],
"property_stock_production": [
15,
"Virtual Locations/Production"
],
"purchase_ok": true,
"qty_available": 0,
"reordering_max_qty": 0,
"reordering_min_qty": 0,
"responsible_id": [
1,
"OdooBot"
],
"route_from_categ_ids": [],
"route_ids": [],
"sale_delay": 0,
"sale_line_warn": "no-message",
"sale_line_warn_msg": false,
"sale_ok": true,
"sales_count": 0,
"seller_ids": [],
"sequence": 1,
"service_tracking": "no",
"service_type": "manual",
"show_forecasted_qty_status_button": false,
"show_on_hand_qty_status_button": false,
"standard_price": 2500,
"supplier_taxes_id": [],
"tax_string": " ",
"taxes_id": [],
"technical_specifications": false,
"tracking": "none",
"type": "service",
"uom_category_id": [
3,
"Working Time"
],
"uom_id": [
4,
"Hours"
],
"uom_name": "Hours",
"uom_po_id": [
4,
"Hours"
],
"valid_product_template_attribute_line_ids": [],
"valuation": "manual_periodic",
"value_range_ids": [],
"variant_seller_ids": [],
"virtual_available": 0,
"visible_expense_policy": false,
"volume": 0,
"volume_uom_name": "m³",
"warehouse_id": false,
"website_message_ids": [],
"weight": 0,
"weight_uom_name": "kg",
"write_date": "2025-07-14 07:23:55",
"write_uid": [
1,
"OdooBot"
]
}

158
documents/logs/s00021.json Normal file
View File

@ -0,0 +1,158 @@
{
"access_token": false,
"access_url": "/my/orders/21",
"access_warning": "",
"activity_date_deadline": false,
"activity_exception_decoration": false,
"activity_exception_icon": false,
"activity_ids": [],
"activity_state": false,
"activity_summary": false,
"activity_type_icon": false,
"activity_type_id": false,
"activity_user_id": false,
"amount_invoiced": 0,
"amount_paid": 0,
"amount_tax": 0,
"amount_to_invoice": 2589,
"amount_total": 2589,
"amount_undiscounted": 2589,
"amount_untaxed": 2589,
"authorized_transaction_ids": [],
"available_product_document_ids": [
2
],
"campaign_id": false,
"client_order_ref": false,
"commitment_date": false,
"company_id": [
1,
"My Company (San Francisco)"
],
"company_price_include": "tax_excluded",
"country_code": "US",
"create_date": "2025-07-14 07:24:01",
"create_uid": [
1,
"OdooBot"
],
"currency_id": [
1,
"USD"
],
"currency_rate": 1,
"customizable_pdf_form_fields": false,
"date_order": "2025-07-14 07:24:02",
"delivery_count": 0,
"delivery_status": false,
"display_name": "S00021",
"doctor_id": [
46,
"Dr. Luis Herrera"
],
"duplicated_order_ids": [],
"effective_date": false,
"expected_date": "2025-07-14 07:26:23",
"fiscal_position_id": false,
"has_active_pricelist": false,
"has_archived_products": false,
"has_message": true,
"id": 21,
"incoterm": false,
"incoterm_location": false,
"invoice_count": 0,
"invoice_ids": [],
"invoice_status": "no",
"is_expired": false,
"is_lab_request": true,
"is_pdf_quote_builder_available": true,
"journal_id": false,
"json_popover": "{\"popoverTemplate\": \"sale_stock.DelayAlertWidget\", \"late_elements\": []}",
"locked": false,
"medium_id": false,
"message_attachment_count": 0,
"message_follower_ids": [],
"message_has_error": false,
"message_has_error_counter": 0,
"message_has_sms_error": false,
"message_ids": [
310
],
"message_is_follower": false,
"message_needaction": false,
"message_needaction_counter": 0,
"message_partner_ids": [],
"my_activity_date_deadline": false,
"name": "S00021",
"note": false,
"order_line": [
45,
46
],
"origin": false,
"partner_credit_warning": "",
"partner_id": [
44,
"Ana Torres"
],
"partner_invoice_id": [
44,
"Ana Torres"
],
"partner_shipping_id": [
44,
"Ana Torres"
],
"payment_term_id": false,
"pending_email_template_id": false,
"picking_ids": [],
"picking_policy": "direct",
"prepayment_percent": 1,
"pricelist_id": false,
"procurement_group_id": false,
"quotation_document_ids": [],
"reference": false,
"require_payment": true,
"require_signature": true,
"sale_order_option_ids": [],
"sale_order_template_id": false,
"show_json_popover": false,
"show_update_fpos": false,
"show_update_pricelist": false,
"signature": false,
"signed_by": false,
"signed_on": false,
"source_id": false,
"state": "draft",
"tag_ids": [],
"tax_calculation_rounding_method": "round_per_line",
"tax_country_id": [
233,
"United States"
],
"tax_totals": {
"currency_id": 1
},
"team_id": [
1,
"Sales"
],
"terms_type": "plain",
"transaction_ids": [],
"type_name": "Quotation",
"user_id": [
1,
"OdooBot"
],
"validity_date": "2025-08-13",
"warehouse_id": [
1,
"YourCompany"
],
"website_message_ids": [],
"write_date": "2025-07-14 07:24:04",
"write_uid": [
1,
"OdooBot"
]
}

650
documents/metadata.json Normal file
View File

@ -0,0 +1,650 @@
{
"sale_order": [
[
"id",
"integer"
],
[
"message_main_attachment_id",
"integer"
],
[
"access_token",
"character varying"
],
[
"name",
"character varying"
],
[
"origin",
"character varying"
],
[
"client_order_ref",
"character varying"
],
[
"reference",
"character varying"
],
[
"state",
"character varying"
],
[
"date_order",
"timestamp without time zone"
],
[
"validity_date",
"date"
],
[
"commitment_date",
"timestamp without time zone"
],
[
"expected_date",
"timestamp without time zone"
],
[
"user_id",
"integer"
],
[
"partner_id",
"integer"
],
[
"partner_invoice_id",
"integer"
],
[
"partner_shipping_id",
"integer"
],
[
"pricelist_id",
"integer"
],
[
"currency_id",
"integer"
],
[
"analytic_account_id",
"integer"
],
[
"order_line",
"integer"
],
[
"invoice_count",
"integer"
],
[
"invoice_status",
"character varying"
],
[
"note",
"text"
],
[
"amount_untaxed",
"numeric"
],
[
"amount_tax",
"numeric"
],
[
"amount_total",
"numeric"
],
[
"currency_rate",
"numeric"
],
[
"payment_term_id",
"integer"
],
[
"fiscal_position_id",
"integer"
],
[
"company_id",
"integer"
],
[
"team_id",
"integer"
],
[
"signature",
"text"
],
[
"signed_by",
"character varying"
],
[
"signed_on",
"timestamp without time zone"
],
[
"create_uid",
"integer"
],
[
"create_date",
"timestamp without time zone"
],
[
"write_uid",
"integer"
],
[
"write_date",
"timestamp without time zone"
],
[
"sale_order_template_id",
"integer"
],
[
"incoterm",
"integer"
],
[
"picking_policy",
"character varying"
],
[
"warehouse_id",
"integer"
],
[
"procurement_group_id",
"integer"
],
[
"campaign_id",
"integer"
],
[
"medium_id",
"integer"
],
[
"source_id",
"integer"
],
[
"delivery_count",
"integer"
],
[
"is_lab_request",
"boolean"
],
[
"doctor_id",
"integer"
]
],
"sale_order_line": [
[
"id",
"integer"
],
[
"order_id",
"integer"
],
[
"name",
"text"
],
[
"sequence",
"integer"
],
[
"invoice_status",
"character varying"
],
[
"price_unit",
"numeric"
],
[
"price_subtotal",
"numeric"
],
[
"price_tax",
"double precision"
],
[
"price_total",
"numeric"
],
[
"price_reduce",
"numeric"
],
[
"tax_id",
"integer"
],
[
"price_reduce_taxinc",
"numeric"
],
[
"price_reduce_taxexcl",
"numeric"
],
[
"discount",
"numeric"
],
[
"product_id",
"integer"
],
[
"product_template_id",
"integer"
],
[
"product_uom_category_id",
"integer"
],
[
"product_uom",
"integer"
],
[
"product_uom_qty",
"numeric"
],
[
"product_uom_readonly",
"boolean"
],
[
"qty_delivered_method",
"character varying"
],
[
"qty_delivered",
"numeric"
],
[
"qty_delivered_manual",
"numeric"
],
[
"qty_to_invoice",
"numeric"
],
[
"qty_invoiced",
"numeric"
],
[
"untaxed_amount_invoiced",
"numeric"
],
[
"untaxed_amount_to_invoice",
"numeric"
],
[
"salesman_id",
"integer"
],
[
"currency_id",
"integer"
],
[
"company_id",
"integer"
],
[
"order_partner_id",
"integer"
],
[
"is_expense",
"boolean"
],
[
"is_downpayment",
"boolean"
],
[
"state",
"character varying"
],
[
"customer_lead",
"double precision"
],
[
"display_type",
"character varying"
],
[
"create_uid",
"integer"
],
[
"create_date",
"timestamp without time zone"
],
[
"write_uid",
"integer"
],
[
"write_date",
"timestamp without time zone"
],
[
"analytic_distribution",
"jsonb"
],
[
"analytic_line_ids",
"integer"
],
[
"is_service",
"boolean"
],
[
"sale_order_option_ids",
"integer"
],
[
"linked_line_id",
"integer"
],
[
"product_packaging_id",
"integer"
],
[
"product_packaging_qty",
"numeric"
],
[
"product_packaging_description",
"text"
]
],
"product_template": [
[
"id",
"integer"
],
[
"message_main_attachment_id",
"integer"
],
[
"sequence",
"integer"
],
[
"name",
"jsonb"
],
[
"description",
"jsonb"
],
[
"description_purchase",
"text"
],
[
"description_sale",
"jsonb"
],
[
"type",
"character varying"
],
[
"categ_id",
"integer"
],
[
"currency_id",
"integer"
],
[
"cost_currency_id",
"integer"
],
[
"list_price",
"numeric"
],
[
"volume",
"double precision"
],
[
"weight",
"double precision"
],
[
"sale_ok",
"boolean"
],
[
"purchase_ok",
"boolean"
],
[
"uom_id",
"integer"
],
[
"uom_po_id",
"integer"
],
[
"company_id",
"integer"
],
[
"active",
"boolean"
],
[
"color",
"integer"
],
[
"default_code",
"character varying"
],
[
"can_image_1024_be_zoomed",
"boolean"
],
[
"create_uid",
"integer"
],
[
"create_date",
"timestamp without time zone"
],
[
"write_uid",
"integer"
],
[
"write_date",
"timestamp without time zone"
],
[
"service_type",
"character varying"
],
[
"sale_line_warn",
"character varying"
],
[
"sale_line_warn_msg",
"text"
],
[
"expense_policy",
"character varying"
],
[
"visible_expense_policy",
"boolean"
],
[
"invoice_policy",
"character varying"
],
[
"sale_delay",
"double precision"
],
[
"tracking",
"character varying"
],
[
"description_picking",
"text"
],
[
"description_pickingout",
"text"
],
[
"description_pickingin",
"text"
],
[
"responsible_id",
"integer"
],
[
"property_stock_production",
"integer"
],
[
"property_stock_inventory",
"integer"
],
[
"service_tracking",
"character varying"
],
[
"is_analysis",
"boolean"
],
[
"analysis_type",
"character varying"
],
[
"technical_specifications",
"text"
]
],
"product_product": [
[
"id",
"integer"
],
[
"message_main_attachment_id",
"integer"
],
[
"product_tmpl_id",
"integer"
],
[
"default_code",
"character varying"
],
[
"barcode",
"character varying"
],
[
"combination_indices",
"character varying"
],
[
"volume",
"double precision"
],
[
"weight",
"double precision"
],
[
"active",
"boolean"
],
[
"can_be_expensed",
"boolean"
],
[
"create_uid",
"integer"
],
[
"create_date",
"timestamp without time zone"
],
[
"write_uid",
"integer"
],
[
"write_date",
"timestamp without time zone"
],
[
"lst_price",
"numeric"
],
[
"standard_price",
"numeric"
],
[
"property_stock_production",
"integer"
],
[
"property_stock_inventory",
"integer"
]
]
}

View File

@ -0,0 +1,58 @@
# Plan de Desarrollo: Issue #6 - Solicitudes de Laboratorio
## Análisis
El objetivo de este issue es implementar la funcionalidad para que un recepcionista pueda registrar una **"Solicitud de Laboratorio"**. Esta solicitud debe estar vinculada a un paciente, a un médico remitente (opcional) y debe contener los análisis clínicos que se realizarán.
Basándose en los documentos de diseño (`ToBeDesing.md`) y los requerimientos, la estrategia principal es **reutilizar el modelo de Órdenes de Venta (`sale.order`) de Odoo** para representar las solicitudes de laboratorio. Esta decisión es clave porque aprovecha el flujo de facturación y contabilidad ya existente en Odoo, evitando desarrollar una lógica de cobro paralela y asegurando una integración nativa.
El trabajo realizado en el **Issue #5** ya nos proporciona el "Catálogo de Análisis Clínicos" como productos de tipo servicio, que serán los elementos que se añadirán a estas solicitudes.
Por lo tanto, el plan se centrará en adaptar y extender el modelo `sale.order` para que se comporte y se presente al usuario como una "Solicitud de Laboratorio".
---
## Plan de Actividades
- **1. Extender el Modelo `sale.order`:**
- [x] Crear el archivo `lims_management/models/sale_order.py`.
- [x] Heredar del modelo `sale.order` para añadir los siguientes campos:
- `is_lab_request` (Booleano): Un campo técnico para identificar que la orden de venta es una solicitud de laboratorio. Será invisible en la interfaz y se usará para filtrar y aplicar lógica específica.
- `doctor_id` (Many2one a `res.partner`): Para seleccionar al médico que remite la solicitud. Se debe aplicar un dominio para que solo muestre los contactos que estén marcados como doctores (`is_doctor = True`).
- [x] Añadir el nuevo archivo `sale_order.py` al `__init__.py` de la carpeta `models`.
- **2. Crear Vistas para Solicitudes de Laboratorio:**
- [x] Crear el archivo de vistas `lims_management/views/sale_order_views.xml`.
- [x] **Heredar la vista de formulario de `sale.order`** para:
- [x] Añadir el campo `doctor_id` cerca del campo del paciente.
- [x] Cambiar la etiqueta (string) del campo `partner_id` de "Cliente" a "Paciente".
- [x] **(Nuevo)** Aplicar un dominio al campo `partner_id` para que solo muestre contactos que sean pacientes (`is_patient = True`).
- [x] **(Nuevo)** Corregir y asegurar que el dominio en el campo `product_template_id` de las líneas de la orden restrinja la selección únicamente a análisis clínicos (`is_analysis = True`).
- [x] **Heredar la vista de lista (tree/list) de `sale.order`** para:
- Añadir la columna "Médico Remitente" (`doctor_id`).
- **3. Crear Men<65><6E> y Acción de Ventana:**
- [x] Modificar el archivo `lims_management/views/menus.xml`.
- [x] Crear una nueva **Acción de Ventana** (`ir.actions.act_window`) para las solicitudes de laboratorio:
- `name`: "Solicitudes de Laboratorio".
- `res_model`: `sale.order`.
- `view_mode`: `list,form`.
- `domain`: `[('is_lab_request', '=', True)]` para mostrar solo las solicitudes de laboratorio.
- `context`: `{'default_is_lab_request': True}` para que las nuevas solicitudes se marquen correctamente por defecto.
- [x] Crear un nuevo `menuitem` llamado "Solicitudes de Laboratorio" bajo el menú principal de "Laboratorio", que dispare la acción anterior.
- **4. Actualizar Manifiesto y Seguridad:**
- [x] Añadir la dependencia del módulo `sale` en el archivo `__manifest__.py`.
- [x] Añadir el nuevo archivo de vistas `sale_order_views.xml` a la lista `data` en `__manifest__.py`.
- [x] Asegurar que los grupos de usuarios del laboratorio (ej. Recepcionista) tengan los permisos adecuados para crear y modificar órdenes de venta (`sale.order`).
- **5. Crear Datos de Demostración:**
- [x] Crear un nuevo archivo de datos de demostración para las solicitudes de laboratorio.
- [x] Definir al menos dos solicitudes de ejemplo que incluyan diferentes análisis y pacientes.
- [x] Añadir el nuevo archivo a la clave `demo` en `__manifest__.py`.
- **6. Verificación Final:**
- [x] Reiniciar la instancia de Odoo para aplicar los cambios.
- [x] Verificar en la interfaz que el nuevo menú "Solicitudes de Laboratorio" aparece y funciona.
- [x] Comprobar que al crear una nueva solicitud, solo se puedan añadir análisis del catálogo y que se pueda seleccionar un médico remitente.
- [x] Revisar los logs para asegurar que no haya errores.

21
get_metadata.py Normal file
View File

@ -0,0 +1,21 @@
import odoo
import json
def get_table_metadata(cr, table_name):
cr.execute("""
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = %s
""", (table_name,))
return cr.fetchall()
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
metadata = {}
tables = ['sale_order', 'sale_order_line', 'product_template', 'product_product']
for table in tables:
metadata[table] = get_table_metadata(cr, table)
print(json.dumps(metadata, indent=4))

14
get_view_arch.py Normal file
View File

@ -0,0 +1,14 @@
import odoo
def get_view_arch(cr, view_id):
cr.execute("SELECT arch_db FROM ir_ui_view WHERE id = %s", (view_id,))
return cr.fetchone()[0]
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
view = env.ref('sale.view_order_form')
print(f"View ID: {view.id}")
print(get_view_arch(cr, view.id))

View File

@ -58,7 +58,46 @@ try:
sys.exit(result.returncode)
print("Inicialización de Odoo completada exitosamente.")
sys.exit(0)
# --- Lógica para crear datos de demostración personalizados ---
print("Creando solicitudes de laboratorio de demostración...")
sys.stdout.flush()
with open("/app/create_lab_requests.py", "r") as f:
script_content = f.read()
# Reutilizamos el entorno de Odoo para ejecutar un script
create_requests_command = f"""
odoo shell -c {ODOO_CONF} -d {DB_NAME} <<'EOF'
{script_content}
EOF
"""
try:
result = subprocess.run(
create_requests_command,
shell=True,
capture_output=True,
text=True,
check=False
)
print("--- Create Lab Requests stdout ---")
print(result.stdout)
print("--- Create Lab Requests stderr ---")
print(result.stderr)
sys.stdout.flush()
if result.returncode != 0:
print(f"Fallo al crear las solicitudes de laboratorio con código de salida {result.returncode}")
sys.exit(result.returncode)
print("Solicitudes de laboratorio de demostración creadas exitosamente.")
sys.exit(0)
except Exception as e:
print(f"Ocurrió un error inesperado al crear las solicitudes de laboratorio: {e}")
sys.exit(1)
except FileNotFoundError:
print("Error: El comando 'odoo' no se encontró. Asegúrate de que la imagen del contenedor es correcta y odoo está en el PATH.")

View File

@ -16,7 +16,7 @@
'website': "https://gitea.grupoconsiti.com/luis_portillo/clinical_laboratory",
'category': 'Industries',
'version': '18.0.1.0.0',
'depends': ['base', 'product'],
'depends': ['base', 'product', 'sale'],
'data': [
'security/lims_security.xml',
'security/ir.model.access.csv',
@ -24,11 +24,12 @@
'data/product_category.xml',
'views/partner_views.xml',
'views/analysis_views.xml',
'views/sale_order_views.xml',
'views/menus.xml',
],
'demo': [
'demo/lims_demo.xml',
'demo/analysis_demo.xml',
'demo/z_lims_demo.xml',
'demo/z_analysis_demo.xml',
],
'installable': True,
'application': True,

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<function model="sale.order" name="search" eval="[('name', 'in', ['S00001', 'S00002', 'S00003', 'S00004', 'S00005', 'S00006', 'S00007', 'S00008', 'S00009', 'S00010', 'S00011', 'S00012', 'S00013', 'S00014', 'S00015', 'S00016', 'S00017', 'S00018', 'S00019', 'S00020', 'S00021', 'S00022'])]"/>
<function model="sale.order" name="action_cancel"/>
<function model="sale.order" name="unlink"/>
</data>
</odoo>

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from . import partner
from . import product
from . import analysis_range
from . import analysis_range
from . import sale_order

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class SaleOrder(models.Model):
_inherit = 'sale.order'
is_lab_request = fields.Boolean(
string="Is a Laboratory Request",
default=False,
copy=False,
help="Technical field to identify if the sale order is a laboratory request."
)
doctor_id = fields.Many2one(
'res.partner',
string="Referring Doctor",
domain="[('is_doctor', '=', True)]",
help="The doctor who referred the patient for this laboratory request."
)

View File

@ -1,2 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_lims_analysis_range_user,lims.analysis.range.user,model_lims_analysis_range,base.group_user,1,1,1,1
access_sale_order_receptionist,sale.order.receptionist,sale.model_sale_order,group_lims_receptionist,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_lims_analysis_range_user lims.analysis.range.user model_lims_analysis_range base.group_user 1 1 1 1
3 access_sale_order_receptionist sale.order.receptionist sale.model_sale_order group_lims_receptionist 1 1 1 0

View File

@ -53,6 +53,28 @@
action="action_lims_doctor"
sequence="30"/>
<!-- Acción de Ventana para Solicitudes de Laboratorio -->
<record id="action_lims_lab_request" model="ir.actions.act_window">
<field name="name">Solicitudes de Laboratorio</field>
<field name="res_model">sale.order</field>
<field name="view_mode">list,form</field>
<field name="domain">[('is_lab_request', '=', True)]</field>
<field name="context">{'default_is_lab_request': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Crea una nueva solicitud de laboratorio
</p>
</field>
</record>
<!-- Menú para Solicitudes de Laboratorio -->
<menuitem
id="lims_menu_lab_requests"
name="Solicitudes de Laboratorio"
parent="lims_menu_root"
action="action_lims_lab_request"
sequence="15"/>
<!-- Submenú de Configuración -->
<menuitem
id="lims_menu_config"

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Inherit Form View to Modify Sale Order for Lab Requests -->
<record id="view_order_form_inherit_lims" model="ir.ui.view">
<field name="name">sale.order.form.inherit.lims</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="doctor_id" invisible="not is_lab_request"/>
</xpath>
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="string">Paciente</attribute>
<attribute name="domain">[('is_patient', '=', True)]</attribute>
</xpath>
<xpath expr="//notebook/page[@name='order_lines']//field[@name='product_template_id']" position="attributes">
<attribute name="domain">[('is_analysis', '=', True)]</attribute>
</xpath>
</field>
</record>
<!-- Inherit List View to Add Referring Doctor -->
<record id="view_order_tree_inherit_lims" model="ir.ui.view">
<field name="name">sale.order.tree.inherit.lims</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="doctor_id"/>
</xpath>
</field>
</record>
</data>
</odoo>

13
scripts/hooks/pre-commit Normal file
View File

@ -0,0 +1,13 @@
#!/bin/sh
#
# Pre-commit hook que verifica si hay cambios sin agregar al staging area.
# Si se encuentran cambios sin agregar, el commit se aborta.
# Revisa si hay archivos modificados pero no agregados (unstaged)
if ! git diff-index --quiet HEAD --; then
echo "Error: Hay cambios sin agregar al commit."
echo "Por favor, agrega todos los archivos relevantes con 'git add .' o 'git add <file>' antes de hacer commit."
exit 1
fi
exit 0

30
verify_products.py Normal file
View File

@ -0,0 +1,30 @@
import odoo
import json
def verify_lab_order_products(cr):
cr.execute("""
SELECT
so.name AS order_name,
sol.id AS line_id,
pt.name->>'en_US' AS product_name,
pt.is_analysis
FROM
sale_order so
JOIN
sale_order_line sol ON so.id = sol.order_id
JOIN
product_product pp ON sol.product_id = pp.id
JOIN
product_template pt ON pp.product_tmpl_id = pt.id
WHERE
so.is_lab_request = TRUE;
""")
return cr.fetchall()
if __name__ == '__main__':
db_name = 'lims_demo'
registry = odoo.registry(db_name)
with registry.cursor() as cr:
results = verify_lab_order_products(cr)
print(json.dumps(results, indent=4))