diff --git a/spp_base_common/CHANGELOG.md b/spp_base_common/CHANGELOG.md new file mode 100644 index 000000000..b8aeebe1f --- /dev/null +++ b/spp_base_common/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +## 2025-11-20 + +### 2025-11-20 16:30:00 - [FIX] queue job test creation with proper sentinel context + +- Fixed test job creation to use `_job_edit_sentinel` context for protected fields +- Added `_create_test_job` helper method to simplify job creation in tests +- Updated all test methods to use helper instead of direct create() calls +- Tests now properly respect queue.job model's protected fields constraint + +### 2025-11-20 16:00:00 - [ADD] comprehensive tests for queue job tracking + +- Added `test_queue_job_tracking.py` with 10 comprehensive test cases +- Tests verify `res_id` and `res_model` field functionality +- Tests verify `_compute_job_ids` correctly filters jobs by record +- Tests verify `_compute_has_ongoing_jobs` detects all job states (pending, enqueued, started) +- Tests verify completed jobs (done/failed) don't trigger ongoing flag +- Tests verify model isolation - jobs from different models don't interfere +- Tests cover edge cases including empty results and mixed job states +- Updated `__init__.py` to import new test module diff --git a/spp_base_common/__manifest__.py b/spp_base_common/__manifest__.py index b3ad50cb0..57952bc17 100644 --- a/spp_base_common/__manifest__.py +++ b/spp_base_common/__manifest__.py @@ -13,6 +13,7 @@ "maintainers": ["jeremi", "gonzalesedwin1123", "emjay0921"], "depends": [ "base", + "queue_job", "theme_openspp_muk", "spp_user_roles", "spp_area_base", @@ -29,6 +30,8 @@ "security/ir.model.access.csv", "views/main_view.xml", "views/phone_validation_view.xml", + "views/queue_job_views.xml", + "views/area_import_view.xml", ], "assets": { "web.assets_backend": [ diff --git a/spp_base_common/models/__init__.py b/spp_base_common/models/__init__.py index 02cc6ae6c..c3db96fce 100644 --- a/spp_base_common/models/__init__.py +++ b/spp_base_common/models/__init__.py @@ -1,4 +1,7 @@ +from . import base from . import ir_module_module from . import phone_validation from . import phone_number +from . import queue_job from . import res_partner +from . import area_import diff --git a/spp_base_common/models/area_import.py b/spp_base_common/models/area_import.py new file mode 100644 index 000000000..18ac8fb20 --- /dev/null +++ b/spp_base_common/models/area_import.py @@ -0,0 +1,50 @@ +from odoo import api, fields, models + + +class OpenSPPAreaImport(models.Model): + _inherit = "spp.area.import" + + job_ids = fields.One2many( + "queue.job", + compute="_compute_job_ids", + string="Related Jobs", + help="Queue jobs related to this area import", + ) + + has_ongoing_jobs = fields.Boolean( + compute="_compute_has_ongoing_jobs", + string="Has Ongoing Jobs", + help="True if there are any ongoing queue jobs for this model", + ) + + def _compute_job_ids(self): + """ + Compute related queue jobs based on res_id and res_model fields. + """ + for rec in self: + jobs = self.env["queue.job"].search( + [ + ("res_model", "=", "spp.area.import"), + ("res_id", "=", rec.id), + ] + ) + rec.job_ids = jobs + + @api.depends("job_ids", "job_ids.state") + def _compute_has_ongoing_jobs(self): + """ + Check if there are any ongoing jobs for the spp.area.import model. + This checks across ALL area import records to prevent concurrent operations. + """ + # Check for any ongoing jobs for the entire model + ongoing_jobs_count = self.env["queue.job"].search_count( + [ + ("res_model", "=", "spp.area.import"), + ("state", "in", ["pending", "enqueued", "started"]), + ] + ) + has_ongoing = ongoing_jobs_count > 0 + + # Set the same value for all records + for rec in self: + rec.has_ongoing_jobs = has_ongoing diff --git a/spp_base_common/models/base.py b/spp_base_common/models/base.py new file mode 100644 index 000000000..d28807641 --- /dev/null +++ b/spp_base_common/models/base.py @@ -0,0 +1,37 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class Base(models.AbstractModel): + """Extend base model to automatically populate res_id in queue jobs.""" + + _inherit = "base" + + @api.model + def _job_store_values(self, job): + """ + Override to automatically populate res_id and res_model in queue jobs. + + This method is called when a job is being stored in the database. + It extracts the record ID from the recordset that created the job + and stores it in res_id field for later monitoring. + + :param job: current queue_job.job.Job instance. + :return: dictionary for setting job values. + """ + vals = super()._job_store_values(job) + + # Get the recordset that is creating the job + recordset = job.recordset + + # If the recordset has a single record, store its ID + if recordset and len(recordset) == 1: + vals["res_id"] = recordset.id + vals["res_model"] = recordset._name + # If the recordset has multiple records, store the first ID + elif recordset and len(recordset) > 1: + vals["res_id"] = recordset[0].id + vals["res_model"] = recordset._name + + return vals diff --git a/spp_base_common/models/queue_job.py b/spp_base_common/models/queue_job.py new file mode 100644 index 000000000..0f341561f --- /dev/null +++ b/spp_base_common/models/queue_job.py @@ -0,0 +1,20 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class QueueJob(models.Model): + """Extend queue.job to add res_id field for record tracking.""" + + _inherit = "queue.job" + + res_id = fields.Integer( + string="Record ID", + help="ID of the record that created this job", + index=True, + ) + res_model = fields.Char( + string="Record Model", + help="Model name of the record that created this job", + index=True, + ) diff --git a/spp_base_common/tests/__init__.py b/spp_base_common/tests/__init__.py index cca885d91..cb8786b17 100644 --- a/spp_base_common/tests/__init__.py +++ b/spp_base_common/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_ir_module_module from . import test_phone_number_validation +from . import test_queue_job_tracking diff --git a/spp_base_common/tests/test_queue_job_tracking.py b/spp_base_common/tests/test_queue_job_tracking.py new file mode 100644 index 000000000..b17f02bef --- /dev/null +++ b/spp_base_common/tests/test_queue_job_tracking.py @@ -0,0 +1,186 @@ +# Part of OpenSPP. See LICENSE file for full copyright and licensing details. + +import uuid + +from odoo.tests.common import TransactionCase + + +class TestQueueJobTracking(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.queue_job_model = cls.env["queue.job"] + cls.area_import_model = cls.env["spp.area.import"] + + # Get the EDIT_SENTINEL for protected fields + cls.job_edit_sentinel = cls.queue_job_model.EDIT_SENTINEL + + # Create test area import records + cls.area_import_1 = cls.area_import_model.create( + { + "name": "Test Area Import 1", + } + ) + cls.area_import_2 = cls.area_import_model.create( + { + "name": "Test Area Import 2", + } + ) + + def _create_test_job(self, name, res_model, res_id, state="done", method_name="test_method"): + """Helper method to create test queue jobs with proper sentinel context""" + return self.queue_job_model.with_context(_job_edit_sentinel=self.job_edit_sentinel).create( + { + "uuid": str(uuid.uuid4()), + "name": name, + "model_name": res_model, + "method_name": method_name, + "res_model": res_model, + "res_id": res_id, + "state": state, + } + ) + + def test_01_queue_job_res_fields(self): + """Test that queue.job model has res_id and res_model fields""" + job = self._create_test_job("Test Job", "spp.area.import", self.area_import_1.id, state="done") + + self.assertEqual(job.res_model, "spp.area.import") + self.assertEqual(job.res_id, self.area_import_1.id) + + def test_02_compute_job_ids(self): + """Test that area import correctly computes related jobs""" + # Create jobs for area_import_1 + job1 = self._create_test_job("Job 1 for Import 1", "spp.area.import", self.area_import_1.id, state="done") + job2 = self._create_test_job("Job 2 for Import 1", "spp.area.import", self.area_import_1.id, state="pending") + + # Create job for area_import_2 + job3 = self._create_test_job("Job 1 for Import 2", "spp.area.import", self.area_import_2.id, state="done") + + # Trigger compute + self.area_import_1._compute_job_ids() + self.area_import_2._compute_job_ids() + + # Assert area_import_1 has 2 jobs + self.assertEqual(len(self.area_import_1.job_ids), 2) + self.assertIn(job1, self.area_import_1.job_ids) + self.assertIn(job2, self.area_import_1.job_ids) + self.assertNotIn(job3, self.area_import_1.job_ids) + + # Assert area_import_2 has 1 job + self.assertEqual(len(self.area_import_2.job_ids), 1) + self.assertIn(job3, self.area_import_2.job_ids) + self.assertNotIn(job1, self.area_import_2.job_ids) + self.assertNotIn(job2, self.area_import_2.job_ids) + + def test_03_has_ongoing_jobs_no_jobs(self): + """Test has_ongoing_jobs when there are no jobs""" + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + self.assertFalse(self.area_import_1.has_ongoing_jobs) + self.assertFalse(self.area_import_2.has_ongoing_jobs) + + def test_04_has_ongoing_jobs_with_pending_job(self): + """Test has_ongoing_jobs when there is a pending job""" + # Create a pending job + self._create_test_job("Pending Job", "spp.area.import", self.area_import_1.id, state="pending") + + # Trigger compute on both records + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + # Both should show has_ongoing_jobs = True because it checks the entire model + self.assertTrue(self.area_import_1.has_ongoing_jobs) + self.assertTrue(self.area_import_2.has_ongoing_jobs) + + def test_05_has_ongoing_jobs_with_enqueued_job(self): + """Test has_ongoing_jobs when there is an enqueued job""" + # Clean up previous jobs + self.queue_job_model.search([("res_model", "=", "spp.area.import")]).unlink() + + # Create an enqueued job + self._create_test_job("Enqueued Job", "spp.area.import", self.area_import_2.id, state="enqueued") + + # Trigger compute + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + # Both should show has_ongoing_jobs = True + self.assertTrue(self.area_import_1.has_ongoing_jobs) + self.assertTrue(self.area_import_2.has_ongoing_jobs) + + def test_06_has_ongoing_jobs_with_started_job(self): + """Test has_ongoing_jobs when there is a started job""" + # Clean up previous jobs + self.queue_job_model.search([("res_model", "=", "spp.area.import")]).unlink() + + # Create a started job + self._create_test_job("Started Job", "spp.area.import", self.area_import_1.id, state="started") + + # Trigger compute + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + # Both should show has_ongoing_jobs = True + self.assertTrue(self.area_import_1.has_ongoing_jobs) + self.assertTrue(self.area_import_2.has_ongoing_jobs) + + def test_07_has_ongoing_jobs_with_done_job(self): + """Test has_ongoing_jobs when all jobs are done""" + # Clean up previous jobs + self.queue_job_model.search([("res_model", "=", "spp.area.import")]).unlink() + + # Create only done/failed jobs + self._create_test_job("Done Job", "spp.area.import", self.area_import_1.id, state="done") + self._create_test_job("Failed Job", "spp.area.import", self.area_import_2.id, state="failed") + + # Trigger compute + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + # Both should show has_ongoing_jobs = False + self.assertFalse(self.area_import_1.has_ongoing_jobs) + self.assertFalse(self.area_import_2.has_ongoing_jobs) + + def test_08_has_ongoing_jobs_mixed_states(self): + """Test has_ongoing_jobs with mixed job states""" + # Clean up previous jobs + self.queue_job_model.search([("res_model", "=", "spp.area.import")]).unlink() + + # Create jobs with different states + self._create_test_job("Done Job", "spp.area.import", self.area_import_1.id, state="done") + self._create_test_job("Pending Job", "spp.area.import", self.area_import_2.id, state="pending") + + # Trigger compute + self.area_import_1._compute_has_ongoing_jobs() + self.area_import_2._compute_has_ongoing_jobs() + + # Both should show has_ongoing_jobs = True because there's one pending job + self.assertTrue(self.area_import_1.has_ongoing_jobs) + self.assertTrue(self.area_import_2.has_ongoing_jobs) + + def test_09_job_ids_empty_for_different_model(self): + """Test that job_ids is empty when jobs belong to different model""" + # Create a job with different res_model + self._create_test_job("Job for different model", "res.partner", 1, state="done") + + # Trigger compute + self.area_import_1._compute_job_ids() + + # Should not include jobs from other models + self.assertEqual(len(self.area_import_1.job_ids), 0) + + def test_10_has_ongoing_jobs_different_model(self): + """Test that has_ongoing_jobs is not affected by jobs from different models""" + # Clean up previous jobs + self.queue_job_model.search([("res_model", "=", "spp.area.import")]).unlink() + + # Create a pending job for a different model + self._create_test_job("Job for different model", "res.partner", 1, state="pending") + + # Trigger compute + self.area_import_1._compute_has_ongoing_jobs() + + # Should not be affected by jobs from other models + self.assertFalse(self.area_import_1.has_ongoing_jobs) diff --git a/spp_base_common/views/area_import_view.xml b/spp_base_common/views/area_import_view.xml new file mode 100644 index 000000000..07c3f4f50 --- /dev/null +++ b/spp_base_common/views/area_import_view.xml @@ -0,0 +1,81 @@ + + + + view_spparea_import_form_inherit + spp.area.import + + + + + + + + + + state not in ('New', 'Uploaded') or has_ongoing_jobs + + + + + state not in ('New', 'Parsed') or has_ongoing_jobs + + + + + state != 'Validated' or has_ongoing_jobs + + + + + state != 'Imported' or has_ongoing_jobs + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
diff --git a/spp_base_common/views/queue_job_views.xml b/spp_base_common/views/queue_job_views.xml new file mode 100644 index 000000000..6ed143d15 --- /dev/null +++ b/spp_base_common/views/queue_job_views.xml @@ -0,0 +1,55 @@ + + + + + + + queue.job.form.inherit + queue.job + + + + + + + + + + + + + queue.job.tree.inherit + queue.job + + + + + + + + + + + + + queue.job.search.inherit + queue.job + + + + + + + + + + + + + + + diff --git a/spp_demo_common/CHANGELOG.md b/spp_demo_common/CHANGELOG.md new file mode 100644 index 000000000..685f313cf --- /dev/null +++ b/spp_demo_common/CHANGELOG.md @@ -0,0 +1,40 @@ +# Changelog + +## 2025-11-20 + +### 2025-11-20 16:30:00 - [FIX] queue job test creation with proper sentinel context + +- Fixed test job creation to use `_job_edit_sentinel` context for protected fields +- Added `_create_test_job` helper method to simplify job creation in tests +- Updated all test methods (test_28 through test_37) to use helper instead of direct create() calls +- Tests now properly respect queue.job model's protected fields constraint + +### 2025-11-20 16:15:00 - [ADD] comprehensive tests for queue job tracking in demo data generator + +- Added test_28: Tests `_compute_queue_job_ids` correctly filters jobs by generator record +- Added test_29: Tests `queue_job_count` computation +- Added test_30: Tests `has_ongoing_jobs` with no jobs +- Added test_31: Tests detection of pending jobs +- Added test_32: Tests detection of enqueued jobs +- Added test_33: Tests detection of started jobs +- Added test_34: Tests that completed jobs don't trigger ongoing flag +- Added test_35: Tests cross-record behavior - ongoing jobs affect all generator records +- Added test_36: Tests mixed job states scenario +- Added test_37: Tests model isolation - jobs from other models don't affect demo generator +- Total 10 new tests providing comprehensive coverage for queue job tracking functionality + +### 2025-11-20 15:45:00 - [IMP] enhance queue job tracking and prevent concurrent operations + +- Added `has_ongoing_jobs` computed field to detect concurrent job operations +- Added `ongoing_job_generator_id` computed field to identify which generator has ongoing jobs +- Combined compute logic into single efficient method `_compute_ongoing_jobs_info()` +- Enhanced Queue Jobs page with state decorations, detailed form view, and exception information +- Added info alert banner to notify users when another generator is running +- Hidden Generate button when any generator has ongoing jobs to prevent race conditions + +### 2025-11-20 14:30:00 - [ADD] track queue jobs in demo data generator + +- Added `queue_job_ids` computed field to link queue jobs to demo data generator records +- Added `queue_job_count` field to display number of associated jobs +- Added "Queue Jobs" page in form view to display job status and details +- Leverages `res_id` and `res_model` fields from queue_job model for automatic tracking diff --git a/spp_demo_common/models/demo_data_generator.py b/spp_demo_common/models/demo_data_generator.py index 239fc77e7..ebaf19953 100644 --- a/spp_demo_common/models/demo_data_generator.py +++ b/spp_demo_common/models/demo_data_generator.py @@ -6,7 +6,7 @@ from faker import Faker -from odoo import fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError from odoo.addons.queue_job.delay import group @@ -110,11 +110,70 @@ def _default_queue_job_minimum_size(self): string="Failed Generations", compute="_compute_generation_log_count", ) + queue_job_ids = fields.One2many( + "queue.job", + compute="_compute_queue_job_ids", + string="Queue Jobs", + readonly=True, + ) + queue_job_count = fields.Integer( + string="Queue Jobs", + compute="_compute_queue_job_count", + ) + has_ongoing_jobs = fields.Boolean( + compute="_compute_ongoing_jobs_info", + string="Has Ongoing Jobs", + help="True if there are any ongoing queue jobs for this model", + ) + ongoing_job_generator_id = fields.Many2one( + "spp.demo.data.generator", + compute="_compute_ongoing_jobs_info", + string="Generator with Ongoing Jobs", + help="Demo data generator record that currently has ongoing jobs", + ) def _compute_generation_log_count(self): for rec in self: rec.generation_log_count = len(rec.generation_log_ids) + def _compute_queue_job_ids(self): + for rec in self: + rec.queue_job_ids = self.env["queue.job"].search( + [ + ("res_model", "=", "spp.demo.data.generator"), + ("res_id", "=", rec.id), + ] + ) + + def _compute_queue_job_count(self): + for rec in self: + rec.queue_job_count = len(rec.queue_job_ids) + + @api.depends("queue_job_ids", "queue_job_ids.state") + def _compute_ongoing_jobs_info(self): + """ + Compute both has_ongoing_jobs and ongoing_job_generator_id fields. + Checks for any ongoing jobs across ALL demo data generator records + to prevent concurrent operations. + """ + # Find any ongoing job for this model + ongoing_job = self.env["queue.job"].search( + [ + ("res_model", "=", "spp.demo.data.generator"), + ("state", "in", ["pending", "enqueued", "started"]), + ], + limit=1, + ) + + # Determine values based on job existence + has_ongoing = bool(ongoing_job) + generator_id = ongoing_job.res_id if ongoing_job and ongoing_job.res_id else None + + # Set the same values for all records + for rec in self: + rec.has_ongoing_jobs = has_ongoing + rec.ongoing_job_generator_id = generator_id + def generate_demo_data(self): self.ensure_one() faker_code = self.locale_origin.faker_locale or "en_US" diff --git a/spp_demo_common/tests/test_demo_data_generator.py b/spp_demo_common/tests/test_demo_data_generator.py index 97b1a2acd..3cc8657d3 100644 --- a/spp_demo_common/tests/test_demo_data_generator.py +++ b/spp_demo_common/tests/test_demo_data_generator.py @@ -1,6 +1,7 @@ # Part of OpenSPP. See LICENSE file for full copyright and licensing details. import datetime import logging +import uuid from dateutil.relativedelta import relativedelta @@ -22,6 +23,10 @@ def setUpClass(cls): ) ) + # Get the EDIT_SENTINEL for protected fields in queue.job + cls.queue_job_model = cls.env["queue.job"] + cls.job_edit_sentinel = cls.queue_job_model.EDIT_SENTINEL + # Create test country with faker locale cls.test_country = cls.env["res.country"].create( { @@ -624,3 +629,242 @@ def test_27_edge_case_regex_generation_complex(self): result2 = generator.generate_id_from_regex(pattern2) self.assertIsNotNone(result2) self.assertEqual(len(result2), 5) + + def _create_test_job(self, name, res_model, res_id, state="done", method_name="_process_batch"): + """Helper method to create test queue jobs with proper sentinel context""" + return self.queue_job_model.with_context(_job_edit_sentinel=self.job_edit_sentinel).create( + { + "uuid": str(uuid.uuid4()), + "name": name, + "model_name": res_model, + "method_name": method_name, + "res_model": res_model, + "res_id": res_id, + "state": state, + } + ) + + def test_28_compute_queue_job_ids(self): + """Test that generator correctly computes related queue jobs""" + generator1 = self.env["spp.demo.data.generator"].create( + { + "name": "Generator with Jobs", + "locale_origin": self.test_country.id, + } + ) + generator2 = self.env["spp.demo.data.generator"].create( + { + "name": "Generator without Jobs", + "locale_origin": self.test_country.id, + } + ) + + # Create jobs for generator1 + job1 = self._create_test_job("Job 1 for Generator 1", "spp.demo.data.generator", generator1.id, state="done") + job2 = self._create_test_job("Job 2 for Generator 1", "spp.demo.data.generator", generator1.id, state="pending") + + # Trigger compute + generator1._compute_queue_job_ids() + generator2._compute_queue_job_ids() + + # Assert generator1 has 2 jobs + self.assertEqual(len(generator1.queue_job_ids), 2) + self.assertIn(job1, generator1.queue_job_ids) + self.assertIn(job2, generator1.queue_job_ids) + + # Assert generator2 has no jobs + self.assertEqual(len(generator2.queue_job_ids), 0) + + def test_29_compute_queue_job_count(self): + """Test queue job count computation""" + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator for Count Test", + "locale_origin": self.test_country.id, + } + ) + + # Initially no jobs + generator._compute_queue_job_ids() + generator._compute_queue_job_count() + self.assertEqual(generator.queue_job_count, 0) + + # Create 3 jobs + for i in range(3): + self._create_test_job(f"Job {i+1}", "spp.demo.data.generator", generator.id, state="done") + + # Recompute + generator._compute_queue_job_ids() + generator._compute_queue_job_count() + self.assertEqual(generator.queue_job_count, 3) + + def test_30_has_ongoing_jobs_no_jobs(self): + """Test has_ongoing_jobs when there are no jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator No Jobs", + "locale_origin": self.test_country.id, + } + ) + + generator._compute_ongoing_jobs_info() + self.assertFalse(generator.has_ongoing_jobs) + self.assertFalse(generator.ongoing_job_generator_id) + + def test_31_has_ongoing_jobs_with_pending_job(self): + """Test has_ongoing_jobs detects pending jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator with Pending Job", + "locale_origin": self.test_country.id, + } + ) + + # Create a pending job + self._create_test_job("Pending Job", "spp.demo.data.generator", generator.id, state="pending") + + generator._compute_ongoing_jobs_info() + self.assertTrue(generator.has_ongoing_jobs) + self.assertEqual(generator.ongoing_job_generator_id, generator) + + def test_32_has_ongoing_jobs_with_enqueued_job(self): + """Test has_ongoing_jobs detects enqueued jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator with Enqueued Job", + "locale_origin": self.test_country.id, + } + ) + + # Create an enqueued job + self._create_test_job("Enqueued Job", "spp.demo.data.generator", generator.id, state="enqueued") + + generator._compute_ongoing_jobs_info() + self.assertTrue(generator.has_ongoing_jobs) + self.assertEqual(generator.ongoing_job_generator_id, generator) + + def test_33_has_ongoing_jobs_with_started_job(self): + """Test has_ongoing_jobs detects started jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator with Started Job", + "locale_origin": self.test_country.id, + } + ) + + # Create a started job + self._create_test_job("Started Job", "spp.demo.data.generator", generator.id, state="started") + + generator._compute_ongoing_jobs_info() + self.assertTrue(generator.has_ongoing_jobs) + self.assertEqual(generator.ongoing_job_generator_id, generator) + + def test_34_has_ongoing_jobs_with_done_job(self): + """Test has_ongoing_jobs with completed jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator with Done Job", + "locale_origin": self.test_country.id, + } + ) + + # Create only done jobs + self._create_test_job("Done Job", "spp.demo.data.generator", generator.id, state="done") + + generator._compute_ongoing_jobs_info() + self.assertFalse(generator.has_ongoing_jobs) + self.assertFalse(generator.ongoing_job_generator_id) + + def test_35_has_ongoing_jobs_cross_record(self): + """Test has_ongoing_jobs affects all records when any generator has ongoing jobs""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator1 = self.env["spp.demo.data.generator"].create( + { + "name": "Generator 1", + "locale_origin": self.test_country.id, + } + ) + generator2 = self.env["spp.demo.data.generator"].create( + { + "name": "Generator 2", + "locale_origin": self.test_country.id, + } + ) + + # Create a pending job for generator1 only + self._create_test_job("Pending Job for Generator 1", "spp.demo.data.generator", generator1.id, state="pending") + + # Trigger compute on both + generator1._compute_ongoing_jobs_info() + generator2._compute_ongoing_jobs_info() + + # Both should show has_ongoing_jobs = True + self.assertTrue(generator1.has_ongoing_jobs) + self.assertTrue(generator2.has_ongoing_jobs) + + # Both should point to generator1 as the one with ongoing jobs + self.assertEqual(generator1.ongoing_job_generator_id, generator1) + self.assertEqual(generator2.ongoing_job_generator_id, generator1) + + def test_36_has_ongoing_jobs_mixed_states(self): + """Test has_ongoing_jobs with mixed job states""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator Mixed States", + "locale_origin": self.test_country.id, + } + ) + + # Create jobs with different states + self._create_test_job("Done Job", "spp.demo.data.generator", generator.id, state="done") + self._create_test_job("Failed Job", "spp.demo.data.generator", generator.id, state="failed") + self._create_test_job("Pending Job", "spp.demo.data.generator", generator.id, state="pending") + + generator._compute_ongoing_jobs_info() + + # Should be True because there's one pending job + self.assertTrue(generator.has_ongoing_jobs) + self.assertEqual(generator.ongoing_job_generator_id, generator) + + def test_37_queue_job_isolation_from_other_models(self): + """Test that jobs from other models don't affect demo generator""" + # Clean up any existing jobs + self.env["queue.job"].search([("res_model", "=", "spp.demo.data.generator")]).unlink() + + generator = self.env["spp.demo.data.generator"].create( + { + "name": "Generator Isolation Test", + "locale_origin": self.test_country.id, + } + ) + + # Create a pending job for a different model + self._create_test_job("Job for different model", "res.partner", 1, state="pending", method_name="test_method") + + generator._compute_ongoing_jobs_info() + generator._compute_queue_job_ids() + + # Should not be affected by jobs from other models + self.assertFalse(generator.has_ongoing_jobs) + self.assertFalse(generator.ongoing_job_generator_id) + self.assertEqual(len(generator.queue_job_ids), 0) diff --git a/spp_demo_common/views/demo_data_generator_view.xml b/spp_demo_common/views/demo_data_generator_view.xml index d61178513..d2c848abd 100644 --- a/spp_demo_common/views/demo_data_generator_view.xml +++ b/spp_demo_common/views/demo_data_generator_view.xml @@ -29,6 +29,8 @@ /> + +
+
+ Info: Another demo data generation is currently in progress: + +
Generate @@ -302,6 +320,50 @@ + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+
diff --git a/spp_programs/__manifest__.py b/spp_programs/__manifest__.py index 2cb6c5037..716a19116 100644 --- a/spp_programs/__manifest__.py +++ b/spp_programs/__manifest__.py @@ -56,7 +56,7 @@ }, "demo": [], "images": [], - "application": False, + "application": True, "installable": True, "auto_install": False, }