From 39318f9073ca34b879fc54e6a642d4052c86f025 Mon Sep 17 00:00:00 2001 From: Luis Ernesto Portillo Zaldivar Date: Tue, 15 Jul 2025 18:53:19 -0600 Subject: [PATCH] =?UTF-8?q?feat(#54):=20Cancelar=20autom=C3=A1ticamente=20?= =?UTF-8?q?muestras=20y=20pruebas=20al=20cancelar=20orden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agregar estado 'cancelled' a stock.lot para muestras - Implementar método action_cancel() en stock.lot - Override action_cancel() en sale.order para: * Cancelar muestras en estados: pending_collection, collected, received, in_process * Cancelar pruebas asociadas que no estén validadas * Registrar mensajes en el chatter de cada elemento cancelado * Mostrar resumen de elementos cancelados en la orden - Agregar tests unitarios completos para verificar: * Cancelación correcta de muestras y pruebas * No cancelación de elementos en estados finales * Generación de mensajes en chatter * Órdenes normales no afectadas La funcionalidad asegura que no queden muestras o pruebas "huérfanas" cuando se cancela una orden de laboratorio. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- issue_body.txt | 24 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 524 -> 524 bytes .../__pycache__/partner.cpython-312.pyc | Bin 4720 -> 4720 bytes .../__pycache__/product.cpython-312.pyc | Bin 3046 -> 3046 bytes .../__pycache__/sale_order.cpython-312.pyc | Bin 10740 -> 12947 bytes .../__pycache__/stock_lot.cpython-312.pyc | Bin 9318 -> 9551 bytes lims_management/models/sale_order.py | 53 ++++ lims_management/models/stock_lot.py | 7 +- lims_management/tests/__init__.py | 3 +- .../tests/test_order_cancel_cascade.py | 263 ++++++++++++++++++ test/run_cancel_tests.py | 53 ++++ test/test_cancel_cascade.py | 149 ++++++++++ test/verify_order_state.py | 49 ++++ 13 files changed, 599 insertions(+), 2 deletions(-) create mode 100644 issue_body.txt create mode 100644 lims_management/tests/test_order_cancel_cascade.py create mode 100644 test/run_cancel_tests.py create mode 100644 test/test_cancel_cascade.py create mode 100644 test/verify_order_state.py diff --git a/issue_body.txt b/issue_body.txt new file mode 100644 index 0000000..7cf6864 --- /dev/null +++ b/issue_body.txt @@ -0,0 +1,24 @@ +## Descripción + +Actualmente, cuando se cancela una orden de laboratorio, las muestras asociadas permanecen activas y no se descartan automáticamente. Esto puede causar confusión ya que quedan muestras "huérfanas" en el sistema que ya no tienen una orden válida. + +## Comportamiento esperado + +Cuando se cancela una orden de laboratorio: +1. Todas las muestras generadas asociadas a esa orden deben cambiar automáticamente su estado a "cancelled" +2. Si hay pruebas (lims.test) asociadas a esas muestras, también deben cancelarse +3. Se debe registrar en el chatter de la muestra que fue cancelada debido a la cancelación de la orden + +## Criterios de aceptación + +- [ ] Al cancelar una orden de laboratorio, todas sus muestras asociadas se marcan como canceladas +- [ ] Las pruebas asociadas a las muestras también se cancelan +- [ ] Se registra un mensaje en el chatter de cada muestra indicando la razón de cancelación +- [ ] Si una muestra ya estaba cancelada o completada, no se modifica +- [ ] La acción es reversible: si se vuelve a poner la orden en borrador, las muestras NO deben reactivarse automáticamente + +## Notas técnicas + +- El método a modificar es `action_cancel()` en el modelo `sale.order` +- Verificar el campo `generated_sample_ids` para obtener las muestras asociadas +- Solo cancelar muestras que estén en estados: 'pending_collection', 'collected', 'in_analysis' \ No newline at end of file diff --git a/lims_management/models/__pycache__/__init__.cpython-312.pyc b/lims_management/models/__pycache__/__init__.cpython-312.pyc index 4830c5f06a0b09f480c71263186ae837409dc067..902966496aa38fd59151600e974c23a7c829ead8 100644 GIT binary patch delta 19 ZcmeBS>0#kI&CAQh00d@VH*zsC0RSqa1Oxy8 delta 19 ZcmeBS>0#kI&CAQh00c@4H*zsC0RSk61DpT= diff --git a/lims_management/models/__pycache__/partner.cpython-312.pyc b/lims_management/models/__pycache__/partner.cpython-312.pyc index 0ba6f1010efbb703b8c95701f5f996ea7468b67b..0b833d5c24e2bacf7b0c5c62270b4fef5cd1c0ec 100644 GIT binary patch delta 19 ZcmeyM@eBg}jsB00OAda?9A|`n2 z3*u@g@$gnGe2{?UX4j`?VqWDUXFE8;AgEq8SWWP(`T#EwXB$+)%&5QE+|&|*XJ!yj zTqWLa$e>x9dTygz-O=a{EH*$k@{H|zmFE%9v(b zYMCCdic{5{ZuO?utNtFcsE?cu%VsmmAoVciG4a+mb!*5!-oV>30_)VVJfwDRT~mg{6C z34zqFeVrzk(x&dQd)r!{NM0wogm}05iPx9Cgx<5(n4t~?%!MZ@0ntQCz&Oqe^gN4M znqx&yNU)e@lZrHV=ZX^N*f~K|1bQwh$O>j<`gM99CxsYTtSoVHmIsSVvLY|hvQQ(S zlOjt?m={DEs1xi&Ov0=p;kd+@@hgzF*vLtVgup2RU$kL?6XGeb>~V2w9!s1c%j-td zII$a^RT|&|aS^FsdVJZph!lcz5Qi87Pr(a@EE00jUp+%CAxz2a+`O?UtOAq<3%!70 z9MMDDP=cL{@$5*NnPd~f1W3hOSWbyc;#7rT%TPk%*n~U+MrG?G2$Wp_e=VTp@cY@w zyAiE-SPKjf?~MkJmKQ=@z=uFc>Y7r?CiG87B_H zf*S#fE$bST#a`phkO3hBdJTZD2H|c4o&_jcWr4-qF%16!goh0X8?Y0AX~Y{1I|5L2 zRWna1Qnmkh-0(YLa1{9ouWWc5=3tz%?2Ldj216f!Y$&8F*Jx$P@M&GPS5#$%Zl#UmUHY_G?lEVnaVeHY4_&ycRQ~f&Cf0r77;Lfcp$n5ehy;$%B@}7w9iQEfyXr0G%p@b&M zdFg}>cj!c+cYDc#ywL~9fG+x426PK;6n5Cf-!vU^Wrw)sLA0@06KvFIf#450GJ(+5Q0<-8-BB delta 125 zcmbQ7`X!j}G%qg~0}yPhD9gxGoyd2e@yx_`Cpjt@qXdH)G^IA5WLzZAShYDv(U%cO z?NZvr!k9NXSG}D{Q)qIHK^2hm$H16z(qsd}eztFUjNGiElg}FlGL}qMG}2~ln(S+2 YD4NC0C^~^{I?qI&uM8k|kt@(X08Py%-v9sr diff --git a/lims_management/models/__pycache__/stock_lot.cpython-312.pyc b/lims_management/models/__pycache__/stock_lot.cpython-312.pyc index 5556171bf610f1224a4b952885692a2ad3006d61..7c490219229e1051d6e3c0a8eb156f8ffb111f3b 100644 GIT binary patch delta 939 zcmY+DOH30{6o%)t6sDyuNK1{pT3*GGNWxoQK0+vn1)@OY8Jx@v6e&}@tq24aBQX#S zpf^SnHmpnti9u)QPTlE3S@wDbBM;=@qbL7p(y1ahzN{&8L01_NrJ6d7c~(HaU!L*78B;6FXUZUk)%Q`|@1Ja3vcE!5xberXe4@dt>L z>0t+5H8`}V))@c~^(M|iP+L!Y1gR}(#cLgMI@4s^kASLR71NOX{PT|yii8nsgI$Teob}5Z7m_~mY&C{1w9GT zWoH+R(HNHipJ;(=5%TCWSB)9l49C>^M8X=iWx7*KnLvtBHMS~#C9+t83m_iamRa8( zS$;Xjm53U|5yUY>Eus#ABZc{ZD)~fpKp`hl>p(3|Rz?(64ibC_q!%$pzh`E^Tg{Yp z3X+W(u^UNT!rrC$Re8&1v{X;kZ93?=>~zB>uQTM+x$N=2aV%tL3do9|@L1s<0{gR4**}doHZ`z1DC$eS+-(CJ?x(uou8bY+~)8cDe?;d CWb$+X delta 766 zcmYk4O-vI}6ou!uv>l*RsD+r=TG|4YS{KrSfKm*If*?wi8fZ{cI)mkJN?wZuL##%O zxDoNXBN}7Ef{+-^Yz(@=jSClOT-mrY(H$Wm3t9PY+Zbl?<=%77yYDBn(e!CK z1pWoKrsBVyixv&Oy#07hXVeLbbF>BTfvvK36(}JHa$rta&6PsLB?sq(h&!8Wr<|`& zu3y#VZ282#WTo7|lT~sfXZ1{z@1*uzzf%AvTok`*w+n*+SMhbxlI1vG%c?mpt^f;Q z+Ty@uu?r&jtT=7XnqD$l16G*3;5v?)@8{$rdHJuo*EmE&nsD86313JDa=tI`e~=ns z7#!DwCXdlHp-qSbmM|8 z25D`_rT~O-pgal>wfE&^hN3XH_aM~A`tkdr{?c|zpCfbmjX=G?T1MDyp1h^oy|pLJBuLPWYy6 zdM-mibQ-rxg_46Wi{Hq9tfJI>_w06=&Jc!eQD-") + message += _("- %d muestras
") % len(samples_to_cancel) + if tests_to_cancel: + message += _("- %d pruebas de laboratorio") % len(tests_to_cancel) + + self.message_post( + body=message, + message_type='notification' + ) + + _logger.info(f"Cancelled {len(samples_to_cancel)} samples and {len(tests_to_cancel)} tests for order {self.name}") + + return res diff --git a/lims_management/models/stock_lot.py b/lims_management/models/stock_lot.py index 9ec953e..02d42e1 100644 --- a/lims_management/models/stock_lot.py +++ b/lims_management/models/stock_lot.py @@ -80,7 +80,8 @@ class StockLot(models.Model): ('in_process', 'En Proceso'), ('analyzed', 'Analizada'), ('stored', 'Almacenada'), - ('disposed', 'Desechada') + ('disposed', 'Desechada'), + ('cancelled', 'Cancelada') ], string='Estado', default='collected', tracking=True) def action_collect(self): @@ -107,6 +108,10 @@ class StockLot(models.Model): """Dispose of the sample""" self.write({'state': 'disposed'}) + def action_cancel(self): + """Cancel the sample""" + self.write({'state': 'cancelled'}) + @api.onchange('sample_type_product_id') def _onchange_sample_type_product_id(self): """Synchronize container_type when sample_type_product_id changes""" diff --git a/lims_management/tests/__init__.py b/lims_management/tests/__init__.py index 0a66197..103baea 100644 --- a/lims_management/tests/__init__.py +++ b/lims_management/tests/__init__.py @@ -2,4 +2,5 @@ from . import test_analysis_parameter from . import test_parameter_range from . import test_result_parameter_integration -from . import test_auto_result_generation \ No newline at end of file +from . import test_auto_result_generation +from . import test_order_cancel_cascade \ No newline at end of file diff --git a/lims_management/tests/test_order_cancel_cascade.py b/lims_management/tests/test_order_cancel_cascade.py new file mode 100644 index 0000000..adaa118 --- /dev/null +++ b/lims_management/tests/test_order_cancel_cascade.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +""" +Test para verificar la cancelación en cascada de muestras y pruebas +cuando se cancela una orden de laboratorio +""" + +from odoo.tests import TransactionCase +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + + +class TestOrderCancelCascade(TransactionCase): + """Test de cancelación en cascada de órdenes de laboratorio""" + + def setUp(self): + super().setUp() + + # Obtener modelos + self.Partner = self.env['res.partner'] + self.Product = self.env['product.product'] + self.SaleOrder = self.env['sale.order'] + self.StockLot = self.env['stock.lot'] + self.LimsTest = self.env['lims.test'] + + # Crear datos de prueba + self.patient = self.Partner.create({ + 'name': 'Test Patient Cancel', + 'is_patient': True, + 'birthdate_date': '1990-01-01', + 'gender': 'male' + }) + + self.doctor = self.Partner.create({ + 'name': 'Test Doctor Cancel', + 'is_doctor': True + }) + + # Crear tipo de muestra + self.sample_type = self.env['product.template'].create({ + 'name': 'Tubo EDTA Test', + 'is_sample_type': True, + 'type': 'service', + 'categ_id': self.env.ref('product.product_category_all').id + }) + + # Crear análisis + self.analysis = self.env['product.template'].create({ + 'name': 'Hemograma Test Cancel', + 'is_analysis': True, + 'type': 'service', + 'required_sample_type_id': self.sample_type.id, + 'categ_id': self.env.ref('product.product_category_all').id + }) + + # Crear parámetro para el análisis + self.parameter = self.env['lims.analysis.parameter'].create({ + 'name': 'Hemoglobina Test', + 'code': 'HGB_TEST', + 'value_type': 'numeric', + 'unit': 'g/dL' + }) + + # Configurar parámetro en el análisis + self.env['product.template.parameter'].create({ + 'product_tmpl_id': self.analysis.id, + 'parameter_id': self.parameter.id, + 'sequence': 10 + }) + + def test_01_cancel_order_cancels_samples(self): + """Test que al cancelar una orden se cancelan las muestras asociadas""" + # Crear orden de laboratorio + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar la orden (debe generar muestras) + order.action_confirm() + + # Verificar que se generaron muestras + self.assertTrue(order.generated_sample_ids, "No se generaron muestras") + samples = order.generated_sample_ids + + # Verificar estado inicial de las muestras + for sample in samples: + self.assertIn(sample.state, ['pending_collection', 'collected'], + f"Estado inicial incorrecto: {sample.state}") + + # Cancelar la orden + order.action_cancel() + + # Verificar que las muestras fueron canceladas + for sample in samples: + self.assertEqual(sample.state, 'cancelled', + f"Muestra no fue cancelada: {sample.state}") + + def test_02_cancel_order_cancels_tests(self): + """Test que al cancelar una orden se cancelan las pruebas asociadas""" + # Crear orden de laboratorio + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar la orden + order.action_confirm() + + # Obtener las pruebas generadas + tests = self.LimsTest.search([ + ('sale_order_line_id.order_id', '=', order.id) + ]) + self.assertTrue(tests, "No se generaron pruebas") + + # Verificar estado inicial + for test in tests: + self.assertEqual(test.state, 'draft', + f"Estado inicial incorrecto: {test.state}") + + # Iniciar proceso en una prueba + if tests: + tests[0].write({'sample_id': order.generated_sample_ids[0].id}) + tests[0].action_start_process() + self.assertEqual(tests[0].state, 'in_process') + + # Cancelar la orden + order.action_cancel() + + # Verificar que las pruebas fueron canceladas + for test in tests: + self.assertEqual(test.state, 'cancelled', + f"Prueba no fue cancelada: {test.state}") + + def test_03_dont_cancel_completed_samples(self): + """Test que no se cancelan muestras en estados finales""" + # Crear orden + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar + order.action_confirm() + + # Marcar una muestra como analizada + sample = order.generated_sample_ids[0] + sample.write({'state': 'analyzed'}) + + # Cancelar la orden + order.action_cancel() + + # Verificar que la muestra analizada no fue cancelada + self.assertEqual(sample.state, 'analyzed', + "Muestra analizada fue cancelada incorrectamente") + + def test_04_dont_cancel_validated_tests(self): + """Test que no se cancelan pruebas validadas""" + # Crear orden + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar + order.action_confirm() + + # Obtener prueba y marcarla como validada + test = self.LimsTest.search([ + ('sale_order_line_id.order_id', '=', order.id) + ], limit=1) + + if test: + test.write({ + 'state': 'validated', + 'sample_id': order.generated_sample_ids[0].id + }) + + # Cancelar la orden + order.action_cancel() + + # Verificar que la prueba validada no fue cancelada + self.assertEqual(test.state, 'validated', + "Prueba validada fue cancelada incorrectamente") + + def test_05_chatter_messages_created(self): + """Test que se crean mensajes en el chatter""" + # Crear orden + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'doctor_id': self.doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar + order.action_confirm() + + # Obtener conteo inicial de mensajes + initial_order_messages = len(order.message_ids) + sample = order.generated_sample_ids[0] + initial_sample_messages = len(sample.message_ids) + + # Cancelar + order.action_cancel() + + # Verificar que se agregaron mensajes + self.assertGreater(len(order.message_ids), initial_order_messages, + "No se agregó mensaje en la orden") + self.assertGreater(len(sample.message_ids), initial_sample_messages, + "No se agregó mensaje en la muestra") + + # Verificar contenido del mensaje + last_order_msg = order.message_ids[0].body + self.assertIn("cancelaron automáticamente", last_order_msg, + "Mensaje de orden no contiene texto esperado") + + def test_06_non_lab_order_not_affected(self): + """Test que órdenes normales no son afectadas""" + # Crear orden normal (no de laboratorio) + order = self.SaleOrder.create({ + 'partner_id': self.patient.id, + 'is_lab_request': False, # NO es orden de laboratorio + 'order_line': [(0, 0, { + 'product_id': self.analysis.product_variant_id.id, + 'product_uom_qty': 1.0 + })] + }) + + # Confirmar + order.action_confirm() + + # No deberían generarse muestras + self.assertFalse(order.generated_sample_ids, + "Se generaron muestras en orden normal") + + # Cancelar - no debería causar error + order.action_cancel() + self.assertEqual(order.state, 'cancel') \ No newline at end of file diff --git a/test/run_cancel_tests.py b/test/run_cancel_tests.py new file mode 100644 index 0000000..0cc22d3 --- /dev/null +++ b/test/run_cancel_tests.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Ejecutar tests de cancelación en cascada +""" + +import odoo +import sys + +def run_tests(cr): + """Ejecutar los tests""" + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + # Importar y ejecutar tests + from odoo.addons.lims_management.tests.test_order_cancel_cascade import TestOrderCancelCascade + + suite = odoo.tests.loader.make_suite(TestOrderCancelCascade) + result = odoo.tests.runner.run(suite) + + print(f"\n📊 Resultados de los tests:") + print(f" Tests ejecutados: {result.testsRun}") + print(f" Errores: {len(result.errors)}") + print(f" Fallos: {len(result.failures)}") + + if result.errors: + print("\n❌ Errores:") + for test, error in result.errors: + print(f" - {test}: {error}") + + if result.failures: + print("\n❌ Fallos:") + for test, failure in result.failures: + print(f" - {test}: {failure}") + + if result.wasSuccessful(): + print("\n✅ Todos los tests pasaron exitosamente!") + else: + print("\n❌ Algunos tests fallaron") + + return result.wasSuccessful() + +if __name__ == '__main__': + db_name = 'lims_demo' + try: + registry = odoo.modules.registry.Registry(db_name) + with registry.cursor() as cr: + success = run_tests(cr) + sys.exit(0 if success else 1) + except Exception as e: + print(f"Error: {e}") + import traceback + traceback.print_exc() + sys.exit(1) \ No newline at end of file diff --git a/test/test_cancel_cascade.py b/test/test_cancel_cascade.py new file mode 100644 index 0000000..b7ffdb6 --- /dev/null +++ b/test/test_cancel_cascade.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Script para probar la funcionalidad de cancelación en cascada +""" + +import odoo +import traceback +from datetime import datetime + + +def test_cancel_cascade(cr): + """Probar la cancelación en cascada de órdenes""" + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + print("🧪 Probando cancelación en cascada de órdenes de laboratorio\n") + + try: + # Buscar un paciente y doctor de prueba + patient = env['res.partner'].search([('is_patient', '=', True)], limit=1) + doctor = env['res.partner'].search([('is_doctor', '=', True)], limit=1) + + if not patient or not doctor: + print("❌ No se encontraron pacientes o doctores de prueba") + return + + # Buscar un análisis + analysis = env['product.product'].search([ + ('is_analysis', '=', True), + ('required_sample_type_id', '!=', False) + ], limit=1) + + if not analysis: + print("❌ No se encontró un análisis con tipo de muestra requerido") + return + + print(f"📋 Creando orden de laboratorio de prueba...") + print(f" Paciente: {patient.name}") + print(f" Doctor: {doctor.name}") + print(f" Análisis: {analysis.name}") + + # Crear orden de laboratorio + order = env['sale.order'].create({ + 'partner_id': patient.id, + 'doctor_id': doctor.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': analysis.id, + 'product_uom_qty': 1.0 + })] + }) + + print(f"\n✓ Orden creada: {order.name}") + print(f" Estado inicial: {order.state}") + + # Confirmar la orden + print("\n🔄 Confirmando orden...") + order.action_confirm() + + print(f"✓ Orden confirmada") + print(f" Estado: {order.state}") + print(f" Muestras generadas: {len(order.generated_sample_ids)}") + + # Mostrar muestras generadas + if order.generated_sample_ids: + print("\n📦 Muestras generadas:") + for sample in order.generated_sample_ids: + print(f" - {sample.name}: {sample.sample_type_product_id.name} (Estado: {sample.state})") + + # Buscar pruebas generadas + tests = env['lims.test'].search([ + ('sale_order_line_id.order_id', '=', order.id) + ]) + + print(f"\n🔬 Pruebas generadas: {len(tests)}") + if tests: + for test in tests: + print(f" - {test.name}: {test.product_id.name} (Estado: {test.state})") + + # Iniciar proceso en una prueba para hacerlo más realista + if tests and order.generated_sample_ids: + print("\n🔄 Iniciando proceso en primera prueba...") + test = tests[0] + test.sample_id = order.generated_sample_ids[0] + test.action_start_process() + print(f" ✓ Prueba {test.name} en proceso") + + # CANCELAR LA ORDEN + print("\n❌ Cancelando la orden de laboratorio...") + order.action_cancel() + + print(f"\n✓ Orden cancelada") + print(f" Estado de la orden: {order.state}") + + # Verificar estado de las muestras + print("\n📦 Estado final de las muestras:") + for sample in order.generated_sample_ids: + print(f" - {sample.name}: {sample.state}") + # Verificar si hay mensaje en el chatter + last_msg = sample.message_ids[0] if sample.message_ids else None + if last_msg and "cancelada automáticamente" in last_msg.body: + print(f" ✓ Mensaje de cancelación registrado") + + # Verificar estado de las pruebas + print("\n🔬 Estado final de las pruebas:") + for test in tests: + test_updated = env['lims.test'].browse(test.id) + print(f" - {test_updated.name}: {test_updated.state}") + # Verificar mensaje + last_msg = test_updated.message_ids[0] if test_updated.message_ids else None + if last_msg and "cancelada automáticamente" in last_msg.body: + print(f" ✓ Mensaje de cancelación registrado") + + # Verificar mensaje en la orden + print("\n📝 Mensajes en la orden:") + for msg in order.message_ids[:3]: # Últimos 3 mensajes + if "cancelaron automáticamente" in msg.body: + print(f" ✓ Mensaje de resumen de cancelación encontrado") + # Extraer números del mensaje + import re + samples_match = re.search(r'(\d+) muestras', msg.body) + tests_match = re.search(r'(\d+) pruebas', msg.body) + if samples_match: + print(f" - Muestras canceladas: {samples_match.group(1)}") + if tests_match: + print(f" - Pruebas canceladas: {tests_match.group(1)}") + break + + print("\n✅ Prueba completada exitosamente!") + + # Hacer rollback para no dejar datos de prueba + cr.rollback() + print("\n⚠️ Cambios revertidos (rollback)") + + except Exception as e: + print(f"\n❌ Error durante la prueba: {str(e)}") + traceback.print_exc() + cr.rollback() + + +if __name__ == '__main__': + db_name = 'lims_demo' + try: + registry = odoo.modules.registry.Registry(db_name) + with registry.cursor() as cr: + test_cancel_cascade(cr) + except Exception as e: + print(f"Error general: {e}") + traceback.print_exc() \ No newline at end of file diff --git a/test/verify_order_state.py b/test/verify_order_state.py new file mode 100644 index 0000000..99540b0 --- /dev/null +++ b/test/verify_order_state.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Verificar estado de orden después de cancelar +""" + +import odoo + +def verify_order_state(cr): + """Verificar que el estado de la orden cambia correctamente""" + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + + # Buscar datos necesarios + patient = env['res.partner'].search([('is_patient', '=', True)], limit=1) + analysis = env['product.product'].search([('is_analysis', '=', True)], limit=1) + + # Crear orden simple + order = env['sale.order'].create({ + 'partner_id': patient.id, + 'is_lab_request': True, + 'order_line': [(0, 0, { + 'product_id': analysis.id, + 'product_uom_qty': 1.0 + })] + }) + + print(f"Orden creada: {order.name}") + print(f"Estado inicial: {order.state}") + + # Confirmar + order.action_confirm() + print(f"Estado después de confirmar: {order.state}") + + # Cancelar + order.action_cancel() + print(f"Estado después de cancelar: {order.state}") + + # Verificar nuevamente + order_check = env['sale.order'].browse(order.id) + print(f"Estado verificado nuevamente: {order_check.state}") + +if __name__ == '__main__': + db_name = 'lims_demo' + try: + registry = odoo.modules.registry.Registry(db_name) + with registry.cursor() as cr: + verify_order_state(cr) + except Exception as e: + print(f"Error: {e}") \ No newline at end of file