From 07cdaba7e5bc1e867b739ce63e1f7a11ee9720fd Mon Sep 17 00:00:00 2001 From: Emanuel Cino Date: Thu, 25 Jun 2026 16:56:44 +0200 Subject: [PATCH] REFACTOR Make queue_job dependencies optional - NEW module queue_job_sh_safe that will safely use queue_jobs when available and fallback to an alternate mechanism with ir_cron in case the module is not installed. - This is useful for Nordic using odoo.sh on which queue jobs are prohibited and not optimal for the infrastructure --- child_compassion/__manifest__.py | 2 +- child_compassion/data/ir_cron.xml | 6 +- child_compassion/data/queue_job.xml | 24 - .../migrations/18.0.1.1.1/post-migration.py | 7 + child_compassion/models/child_compassion.py | 14 - .../models/field_office_disaster.py | 2 +- child_compassion/views/child_holds_view.xml | 2 +- crm_compassion/models/contract.py | 14 +- gift_compassion/models/account_move_line.py | 6 +- gift_compassion/models/sponsorship_gift.py | 3 +- .../models/abstract_interaction_source.py | 5 +- interaction_resume/models/crm_request.py | 5 +- .../models/partner_communication.py | 5 +- .../partner_log_other_interaction_wizard.py | 5 +- message_center_compassion/__manifest__.py | 3 +- message_center_compassion/data/queue_job.xml | 7 - .../models/gmc_message.py | 10 +- partner_auto_match/__manifest__.py | 2 +- .../models/res_partner_match.py | 6 +- partner_communication/__manifest__.py | 3 +- partner_communication/data/queue_job.xml | 34 -- partner_communication/models/__init__.py | 1 - .../models/communication_config.py | 6 +- .../models/communication_job.py | 33 +- partner_communication/models/ir_actions.py | 56 +-- partner_communication/models/queue_job.py | 31 -- .../wizards/generate_communication_wizard.py | 11 +- .../models/contracts.py | 4 + .../models/correspondence.py | 5 +- .../wizards/generate_communication_wizard.py | 7 +- .../models/recurring_contract.py | 7 +- queue_job_optional/README.rst | 58 +++ queue_job_optional/__init__.py | 1 + queue_job_optional/__manifest__.py | 48 ++ queue_job_optional/data/ir_cron.xml | 10 + queue_job_optional/models/__init__.py | 2 + queue_job_optional/models/base.py | 96 ++++ .../models/queue_job_replacement.py | 228 ++++++++++ queue_job_optional/pyproject.toml | 3 + queue_job_optional/readme/DESCRIPTION.md | 4 + .../security/ir.model.access.csv | 3 + queue_job_optional/security/res_group.xml | 7 + .../static/description/index.html | 412 ++++++++++++++++++ .../views/queue_job_replacement_views.xml | 121 +++++ sbc_compassion/__manifest__.py | 1 - sbc_compassion/data/queue_job.xml | 23 - sbc_compassion/models/correspondence.py | 6 +- .../models/correspondence_s2b_generator.py | 6 +- .../models/import_letters_history.py | 12 +- sbc_translation/__manifest__.py | 1 - sbc_translation/data/queue_job.xml | 8 - sbc_translation/models/correspondence.py | 5 +- sponsorship_compassion/__manifest__.py | 1 - sponsorship_compassion/data/queue_job.xml | 19 - .../models/contract_group.py | 8 +- sponsorship_compassion/models/contracts.py | 6 +- .../models/project_compassion.py | 5 +- sponsorship_compassion/models/res_partner.py | 2 +- thankyou_letters/__manifest__.py | 1 - thankyou_letters/data/queue_job.xml | 11 - thankyou_letters/models/account_move.py | 18 +- .../wizards/generate_communication_wizard.py | 8 +- 62 files changed, 1165 insertions(+), 295 deletions(-) delete mode 100644 child_compassion/data/queue_job.xml create mode 100644 child_compassion/migrations/18.0.1.1.1/post-migration.py delete mode 100644 message_center_compassion/data/queue_job.xml delete mode 100644 partner_communication/data/queue_job.xml delete mode 100644 partner_communication/models/queue_job.py create mode 100644 queue_job_optional/README.rst create mode 100644 queue_job_optional/__init__.py create mode 100644 queue_job_optional/__manifest__.py create mode 100644 queue_job_optional/data/ir_cron.xml create mode 100644 queue_job_optional/models/__init__.py create mode 100644 queue_job_optional/models/base.py create mode 100644 queue_job_optional/models/queue_job_replacement.py create mode 100644 queue_job_optional/pyproject.toml create mode 100644 queue_job_optional/readme/DESCRIPTION.md create mode 100644 queue_job_optional/security/ir.model.access.csv create mode 100644 queue_job_optional/security/res_group.xml create mode 100644 queue_job_optional/static/description/index.html create mode 100644 queue_job_optional/views/queue_job_replacement_views.xml delete mode 100644 sbc_compassion/data/queue_job.xml delete mode 100644 sbc_translation/data/queue_job.xml delete mode 100644 sponsorship_compassion/data/queue_job.xml delete mode 100644 thankyou_letters/data/queue_job.xml diff --git a/child_compassion/__manifest__.py b/child_compassion/__manifest__.py index 8937366da..308737e66 100644 --- a/child_compassion/__manifest__.py +++ b/child_compassion/__manifest__.py @@ -29,7 +29,7 @@ # pylint: disable=C8101 { "name": "Compassion Children", - "version": "18.0.1.1.0", + "version": "18.0.1.1.1", "category": "Compassion", "author": "Compassion CH", "license": "AGPL-3", diff --git a/child_compassion/data/ir_cron.xml b/child_compassion/data/ir_cron.xml index 9e5a1625d..b3c4c1372 100644 --- a/child_compassion/data/ir_cron.xml +++ b/child_compassion/data/ir_cron.xml @@ -25,9 +25,9 @@ code for record in model.search([]): - record.with_delay(channel="root.child_compassion").refresh_worldbank_data() - record.with_delay(channel="root.child_compassion").refresh_capital_city() - record.with_delay(channel="root.child_compassion").refresh_factbook_data() + record.with_delay_sh("refresh_worldbank_data", channel="root.child_compassion") + record.with_delay_sh("refresh_capital_city", channel="root.child_compassion") + record.with_delay_sh("refresh_factbook_data", channel="root.child_compassion") - - - child_compassion - - - - fcp_compassion - - - - field_office_compassion - - - diff --git a/child_compassion/migrations/18.0.1.1.1/post-migration.py b/child_compassion/migrations/18.0.1.1.1/post-migration.py new file mode 100644 index 000000000..0dafb28f1 --- /dev/null +++ b/child_compassion/migrations/18.0.1.1.1/post-migration.py @@ -0,0 +1,7 @@ +from openupgradelib import openupgrade + + +def migrate(cr, version): + openupgrade.load_data( + cr, "child_compassion", "data/ir_cron.xml", "refresh_worldbank_data" + ) diff --git a/child_compassion/models/child_compassion.py b/child_compassion/models/child_compassion.py index b911bf6dc..576e9dcbf 100644 --- a/child_compassion/models/child_compassion.py +++ b/child_compassion/models/child_compassion.py @@ -562,20 +562,6 @@ def child_waiting_hold(self): def child_consigned(self, hold_id): """Called on child allocation.""" self.write({"state": "N", "hold_id": hold_id, "date": fields.Datetime.now()}) - # Cancel planned deletion - jobs = ( - self.env["queue.job"] - .sudo() - .search( - [ - ("name", "=", "Job for deleting released children."), - ("func_string", "like", self.ids), - ("state", "=", "enqueued"), - ] - ) - ) - jobs.button_done() - jobs.unlink() self.get_infos() return True diff --git a/child_compassion/models/field_office_disaster.py b/child_compassion/models/field_office_disaster.py index 48e69dffa..464a569ce 100644 --- a/child_compassion/models/field_office_disaster.py +++ b/child_compassion/models/field_office_disaster.py @@ -349,7 +349,7 @@ def process_commkit(self, commkit_data): "action_id": action_id, "object_id": fo_disaster.id, } - message_obj.with_delay(eta=600).create(message_vals) + message_obj.with_delay_sh("create", message_vals, eta=600) fo_ids.append(fo_disaster.id) return fo_ids diff --git a/child_compassion/views/child_holds_view.xml b/child_compassion/views/child_holds_view.xml index a10999d5a..304211b20 100644 --- a/child_compassion/views/child_holds_view.xml +++ b/child_compassion/views/child_holds_view.xml @@ -160,7 +160,7 @@ records.delayable().release_hold().set(channel="root.gmc_pool.child_compassion").split(5).delay() + >records.with_delay_sh("release_hold", channel="root.gmc_pool.child_compassion", split=5) - - - gmc_pool - - - diff --git a/message_center_compassion/models/gmc_message.py b/message_center_compassion/models/gmc_message.py index 73c18a0c5..9e991b38e 100644 --- a/message_center_compassion/models/gmc_message.py +++ b/message_center_compassion/models/gmc_message.py @@ -110,9 +110,13 @@ def process_messages(self): new_messages.write({"state": "pending", "failure_reason": False}) priority = min(new_messages.mapped("action_id.priority")) channel = new_messages[0].action_id.job_channel - new_messages.delayable()._process_messages().set( - priority=priority, channel=channel - ).split(10, chain=True).delay() + new_messages.with_delay_sh( + "_process_messages", + priority=priority, + channel=channel, + split=10, + chain=True, + ) return True def get_answer_dict(self, index=0): diff --git a/partner_auto_match/__manifest__.py b/partner_auto_match/__manifest__.py index 74ab3a69e..d60f30856 100644 --- a/partner_auto_match/__manifest__.py +++ b/partner_auto_match/__manifest__.py @@ -39,7 +39,7 @@ "installable": True, "depends": [ "mail", - "queue_job", # OCA/queue + "queue_job_optional", # OCA/queue "advanced_translation", ], "data": [ diff --git a/partner_auto_match/models/res_partner_match.py b/partner_auto_match/models/res_partner_match.py index 7e565956c..f17f72e21 100644 --- a/partner_auto_match/models/res_partner_match.py +++ b/partner_auto_match/models/res_partner_match.py @@ -96,10 +96,12 @@ def _process_create_infos(self, vals): def update_partner(self, partner, vals): filtered_vals = self._process_update_vals(partner, vals) partner_context = {"skip_check_zip": True, "no_upsert": True} - partner.with_context(**partner_context).with_delay( + partner.with_context(**partner_context).with_delay_sh( + "write", + filtered_vals, eta=60, channel="root.res_partner", - ).write(filtered_vals) + ) @api.model def _preprocess_vals(self, vals): diff --git a/partner_communication/__manifest__.py b/partner_communication/__manifest__.py index 86909164b..b0d020061 100644 --- a/partner_communication/__manifest__.py +++ b/partner_communication/__manifest__.py @@ -38,7 +38,7 @@ "depends": [ "base_report_to_printer", # OCA/report-print-send "contacts", - "queue_job", # OCA/queue + "queue_job_optional", # OCA/queue "mass_mailing_sms", "utm", "mail", @@ -60,7 +60,6 @@ "views/settings_view.xml", "views/communication_snippet_view.xml", "data/default_communication.xml", - "data/queue_job.xml", ], "qweb": [], "demo": ["demo/demo_data.xml"], diff --git a/partner_communication/data/queue_job.xml b/partner_communication/data/queue_job.xml deleted file mode 100644 index 89390087f..000000000 --- a/partner_communication/data/queue_job.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - partner_communication - - - - - - - create_communication_job - - - - - - generate_communications - - - - - create_communication - - - diff --git a/partner_communication/models/__init__.py b/partner_communication/models/__init__.py index 970f6f744..2ed436fb8 100644 --- a/partner_communication/models/__init__.py +++ b/partner_communication/models/__init__.py @@ -17,6 +17,5 @@ ir_actions, ir_attachment, mail_mail, - queue_job, res_partner, ) diff --git a/partner_communication/models/communication_config.py b/partner_communication/models/communication_config.py index 5da69b81c..2e5a05e58 100644 --- a/partner_communication/models/communication_config.py +++ b/partner_communication/models/communication_config.py @@ -216,9 +216,9 @@ def write(self, vals): ] ) jobs_to_update.write({"email_template_id": vals["email_template_id"]}) - jobs_to_update.with_delay( - priority=500, channel="root.partner_communication" - ).refresh_text() + jobs_to_update.with_delay_sh( + "refresh_text", priority=500, channel="root.partner_communication" + ) return res diff --git a/partner_communication/models/communication_job.py b/partner_communication/models/communication_job.py index e8d6ba83e..ab5da275e 100644 --- a/partner_communication/models/communication_job.py +++ b/partner_communication/models/communication_job.py @@ -204,8 +204,10 @@ def _compute_content(self): except QWebException as e: _logger.error("Error during template rendering for job %s", job.id) if job.state == "pending": - job.with_delay(channel="root.partner_communication").write( - {"state": "failure", "body_html": str(e)} + job.with_delay_sh( + "write", + {"state": "failure", "body_html": str(e)}, + channel="root.partner_communication", ) if job.id in template_vals: job.body_html = template_vals[job.id]["body_html"] @@ -412,13 +414,14 @@ def create(self, vals_list): job.schedule_call() if job.auto_send: # T2221 Using a job avoids multiple sends in case of rollbacks - job.with_delay( + job.with_delay_sh( + "send", eta=10, max_retries=1, description="Autosend communication", channel="root.partner_communication", identity_key=f"{self._name}.send.{job.config_id.id}+{job.partner_id.id}", - ).send() + ) return updated + created @@ -595,11 +598,12 @@ def send(self): # Process email jobs (digital or both) asynchronously email_jobs = todo.filtered(lambda j: j.send_mode in digital_modes) for job in email_jobs: - job.with_delay( + job.with_delay_sh( + "_send_mail_asynchronous", channel=_JOB_CHANNEL, priority=50, identity_key=self._name + "._send_mail." + str(job.id), - )._send_mail_asynchronous() + ) return self.download_data() @@ -636,11 +640,12 @@ def _send_by_sms(self): :return: list of sms_texts """ for job in self.filtered("partner_id.mobile"): - job.with_delay( + job.with_delay_sh( + "_send_by_sms_asynchronous", channel=_JOB_CHANNEL, priority=40, identity_key=self._name + "._send_sms." + str(job.id), - )._send_by_sms_asynchronous() + ) def _convert_html_for_sms(self): """ @@ -905,10 +910,12 @@ def _print_batch(self): for job in self: if job.attachment_ids: print_name = name[:3] + " " + (job.subject or "") - job.with_company(job.company_id).with_delay( + job.with_company(job.company_id).with_delay_sh( + "_print_job_asynchronous", + print_name, channel=_JOB_CHANNEL + ".print_individual", identity_key=self._name + "._print." + str(job.ids), - )._print_job_asynchronous(print_name) + ) else: batch_print[job.partner_id.lang][job.config_id.name] += ( job.with_company(job.company_id) @@ -917,10 +924,12 @@ def _print_batch(self): for configs in batch_print.values(): for config, jobs in configs.items(): print_name = name[:3] + " " + config - jobs.with_delay( + jobs.with_delay_sh( + "_print_job_asynchronous", + print_name, channel=_JOB_CHANNEL, identity_key=self._name + "._print." + str(jobs.ids), - )._print_job_asynchronous(print_name) + ) return self.download_data() def _print_job_asynchronous(self, print_name): diff --git a/partner_communication/models/ir_actions.py b/partner_communication/models/ir_actions.py index 904c5f6f1..5d34589e1 100644 --- a/partner_communication/models/ir_actions.py +++ b/partner_communication/models/ir_actions.py @@ -40,44 +40,34 @@ def _run_action_communication(self, eval_context=None): return False model_name = self.model_name - if "records" in eval_context: - for raw_record in eval_context["records"]: - is_self = self.partner_field == "self" - partner = raw_record if is_self else raw_record[self.partner_field] - children = eval_context["records"] - records = self.env[model_name].search( - [ - (self.partner_field, "=", partner.id), - ("id", "in", children.ids), - ] - ) - # Use same job if possible to group communications for one partner - identity_key = f"create_communication.{self.config_id.id}.{partner.id}" - existing_job = self.env["queue.job"].search( - [ - ("state", "=", "pending"), - ("identity_key", "=", identity_key), - ], - limit=1, - ) - if existing_job: - vals = existing_job.args[0] - vals["object_ids"].extend(records.ids) - existing_job.unlink() - else: - vals = { - "partner_id": partner.id, - "object_ids": records.ids, - "config_id": self.config_id.id, - } + records_to_process = eval_context.get("records") + if records_to_process: + is_self = self.partner_field == "self" + # Group records by partner to avoid redundant processing and duplicate jobs + partner_map = {} + for rec in records_to_process: + partner = rec if is_self else rec[self.partner_field] + partner_map.setdefault(partner, self.env[model_name]) + partner_map[partner] |= rec + + for partner, records in partner_map.items(): + vals = { + "partner_id": partner.id, + "object_ids": records.ids, + "config_id": self.config_id.id, + } if self.send_mode: vals["send_mode"] = self.send_mode if self.auto_send: vals["auto_send"] = self.auto_send delay = datetime.now() + timedelta(minutes=3) - self.with_delay( - identity_key=identity_key, eta=delay - ).create_communication_job(vals) + identity_key = f"create_communication.{self.config_id.id}.{partner.id}" + self.with_delay_sh( + "create_communication_job", + vals, + identity_key=identity_key, + eta=delay, + ) return {} def create_communication_job(self, vals): diff --git a/partner_communication/models/queue_job.py b/partner_communication/models/queue_job.py deleted file mode 100644 index 50f8b0313..000000000 --- a/partner_communication/models/queue_job.py +++ /dev/null @@ -1,31 +0,0 @@ -############################################################################## -# -# Copyright (C) 2021 Compassion CH (http://www.compassion.ch) -# Releasing children from poverty in Jesus' name -# @author: Emanuel Cino -# -# The licence is in the file __manifest__.py -# -############################################################################## - -from odoo import _, models -from odoo.tools.safe_eval import safe_eval - - -class QueueJob(models.Model): - _inherit = "queue.job" - - def related_action_automation(self): - records = self.record_ids - model = "ir.actions.server" - if self.result: - records = safe_eval(self.result.split("job")[1]) - model = "partner.communication.job" - action = { - "name": _("Automation"), - "type": "ir.actions.act_window", - "res_model": model, - "domain": [("id", "in", records)], - "view_mode": "list,form", - } - return action diff --git a/partner_communication/wizards/generate_communication_wizard.py b/partner_communication/wizards/generate_communication_wizard.py index aea363320..aa5dfee39 100644 --- a/partner_communication/wizards/generate_communication_wizard.py +++ b/partner_communication/wizards/generate_communication_wizard.py @@ -89,7 +89,9 @@ def _compute_partners(self): def generate(self): self.state = "generation" if len(self.partner_ids) > 5: - self.with_delay().generate_communications() + self.with_delay_sh( + "generate_communications", channel="root.partner_communication" + ) return self.reload() else: self.generate_communications(async_mode=False) @@ -142,11 +144,14 @@ def generate_communications(self, async_mode=True): "force_language": self.force_language, } if async_mode or self.scheduled_date: - self.with_delay( + self.with_delay_sh( + "create_communication", + vals, + options, eta=self.scheduled_date, priority=500, identity_key=self._name + ".create_comm.partner." + str(partner.id), - ).create_communication(vals, options) + ) else: self.create_communication(vals, options) return True diff --git a/partner_communication_compassion/models/contracts.py b/partner_communication_compassion/models/contracts.py index 8c42aa449..3b8a31bdc 100644 --- a/partner_communication_compassion/models/contracts.py +++ b/partner_communication_compassion/models/contracts.py @@ -63,6 +63,10 @@ def send_communication(self, communication, correspondent=True, both=False): partner_field = "correspondent_id" if correspondent else "partner_id" partners = self.mapped(partner_field) communications = self.env["partner.communication.job"] + if isinstance(communication, int): + communication = self.env["partner.communication.config"].browse( + communication + ) if not communication.active: return communications if both: diff --git a/partner_communication_compassion/models/correspondence.py b/partner_communication_compassion/models/correspondence.py index c0cb66191..641c0e46b 100644 --- a/partner_communication_compassion/models/correspondence.py +++ b/partner_communication_compassion/models/correspondence.py @@ -105,12 +105,13 @@ def attach_zip(self): def publish_b2s_letter(self): # Prepare the communication when a letter is published res = super().publish_b2s_letter() - self.with_delay( + self.with_delay_sh( + "send_communication", channel="root.partner_communication", priority=100, description="Send B2S letter communication", identity_key=f"sbc.send_communication.{self.ids}", - ).send_communication() + ) return res def send_communication(self): diff --git a/partner_communication_compassion/wizards/generate_communication_wizard.py b/partner_communication_compassion/wizards/generate_communication_wizard.py index 08bf2fd7e..a8607be34 100644 --- a/partner_communication_compassion/wizards/generate_communication_wizard.py +++ b/partner_communication_compassion/wizards/generate_communication_wizard.py @@ -127,13 +127,16 @@ def generate_communications(self, async_mode=True): ) options = {"force_language": self.force_language} if async_mode or self.scheduled_date: - self.with_delay( + self.with_delay_sh( + "create_communication", + vals, + options, eta=self.scheduled_date, priority=50, identity_key=self._name + ".create_comm.sponsorship." + str(sponsorship.id), - ).create_communication(vals, options) + ) else: self.create_communication(vals, options) return True diff --git a/partner_communication_reminder/models/recurring_contract.py b/partner_communication_reminder/models/recurring_contract.py index 036b79167..329d35a77 100644 --- a/partner_communication_reminder/models/recurring_contract.py +++ b/partner_communication_reminder/models/recurring_contract.py @@ -128,10 +128,13 @@ def create_reminder_communication(self, include_suspended=False): ): sponsorships = eligible_reminders[key] if sponsorships: - sponsorships.with_delay( + sponsorships.with_delay_sh( + "send_communication", + config.id, + False, channel="root.partner_communication", priority=500, identity_key=f"create_reminder_communication.{sponsorships.ids}", - ).send_communication(config, correspondent=False) + ) _logger.info("Sponsorship Reminders created!") return True diff --git a/queue_job_optional/README.rst b/queue_job_optional/README.rst new file mode 100644 index 000000000..20b87244b --- /dev/null +++ b/queue_job_optional/README.rst @@ -0,0 +1,58 @@ +==================================== +Sponsor to Participant communication +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:17bec126ca5261adae59bc473f05dd6395f3b48717c45a8167e72dfe5a876d6f + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-CompassionCH%2Fcompassion--modules-lightgray.png?logo=github + :target: https://github.com/CompassionCH/compassion-modules/tree/18.0/queue_job_optional + :alt: CompassionCH/compassion-modules + +|badge1| |badge2| |badge3| + +Abstract the queue_job module from OCA to use it when available and +fallback to a dedicated CRON when the module is not installed. This is +helpful for sharing code between odoo.sh environment and dedicated +environment because odoo.sh discourages the use of the queue_job module. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Compassion CH + +Maintainers +----------- + +This module is part of the `CompassionCH/compassion-modules `_ project on GitHub. + +You are welcome to contribute. diff --git a/queue_job_optional/__init__.py b/queue_job_optional/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/queue_job_optional/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/queue_job_optional/__manifest__.py b/queue_job_optional/__manifest__.py new file mode 100644 index 000000000..9d53a2a59 --- /dev/null +++ b/queue_job_optional/__manifest__.py @@ -0,0 +1,48 @@ +############################################################################## +# +# ______ Releasing children from poverty _ +# / ____/___ ____ ___ ____ ____ ___________(_)___ ____ +# / / / __ \/ __ `__ \/ __ \/ __ `/ ___/ ___/ / __ \/ __ \ +# / /___/ /_/ / / / / / / /_/ / /_/ (__ |__ ) / /_/ / / / / +# \____/\____/_/ /_/ /_/ .___/\__,_/____/____/_/\____/_/ /_/ +# /_/ +# in Jesus' name +# +# Copyright (C) 2026 Compassion CH (http://www.compassion.ch) +# @author: Emanuel Cino +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +# pylint: disable=C8101 +{ + "name": "Sponsor to Participant communication", + "version": "18.0.1.0.0", + "category": "Compassion", + "summary": "Queue Job abstraction for Compassion", + "sequence": 150, + "author": "Compassion CH", + "license": "AGPL-3", + "website": "https://github.com/CompassionCH/compassion-modules", + "depends": [], + "external_dependencies": {}, + "data": [ + "security/res_group.xml", + "security/ir.model.access.csv", + "data/ir_cron.xml", + "views/queue_job_replacement_views.xml", + ], + "demo": [], + "installable": True, +} diff --git a/queue_job_optional/data/ir_cron.xml b/queue_job_optional/data/ir_cron.xml new file mode 100644 index 000000000..ebb060690 --- /dev/null +++ b/queue_job_optional/data/ir_cron.xml @@ -0,0 +1,10 @@ + + + Queue Job Replacement: Process Jobs + + code + model.cron_run_jobs() + 1 + minutes + + diff --git a/queue_job_optional/models/__init__.py b/queue_job_optional/models/__init__.py new file mode 100644 index 000000000..c10118fd4 --- /dev/null +++ b/queue_job_optional/models/__init__.py @@ -0,0 +1,2 @@ +from . import base +from . import queue_job_replacement diff --git a/queue_job_optional/models/base.py b/queue_job_optional/models/base.py new file mode 100644 index 000000000..8e84af160 --- /dev/null +++ b/queue_job_optional/models/base.py @@ -0,0 +1,96 @@ +from datetime import timedelta + +from odoo import fields, models + + +class Base(models.AbstractModel): + """The base model, which is implicitly inherited by all models. + + A new :meth:`~with_delay` method is added on all Odoo Models, allowing to + postpone the execution of a job method in an asynchronous process. + """ + + _inherit = "base" + + def with_delay_sh(self, job_function, *job_args, **delay_args): + """Returns a proxy object that will enqueue the method call instead of + executing it immediately. + + :param job_function: The name of the method to be executed asynchronously. + :param job_args: Positional arguments to be passed to the `job_function`. + These arguments will be serialized and passed to the + asynchronous job. + :param delay_args: Keyword arguments to control the job's execution. + Possible arguments include: + * **eta** (datetime or int/float): The earliest time at which the job + should be executed. If an int or float, it represents seconds from now. + Defaults to `fields.Datetime.now()` if not specified. + * **priority** (int): The priority of the job. Lower numbers indicate + higher priority (e.g., 0 is higher priority than 10). + * **channel** (str): The channel to which the job should be sent. + Jobs within the same channel are typically executed sequentially. + * **split** (int): If greater than 0, the current recordset (`self`) + will be split into chunks of this size, and a separate job will be + created for each chunk. Each job will then operate on its respective + chunk of records. + * **chain** (bool): If `split` is used and `chain` is True, the + generated jobs will be chained, meaning each job will only start + after the previous one in the chain has completed successfully. + * **parent_job_id** (queue.job record): An existing delayable object + (only applicable when the `queue_job` module is installed) to which + the new job will be linked as a child. + When queue_job is not installed, this should be the id of the parent + queue.job.replacement record. + The child job will be executed when the parent job is done. + """ + queue_job_installed = "queue.job" in self.env + split = delay_args.pop("split", 0) + chain = delay_args.pop("chain", False) + no_delay = self.env.context.get("queue_job__no_delay") + if queue_job_installed: + parent_job = delay_args.pop("parent_job_id", None) + if not no_delay and (split or parent_job is not None): + job = getattr(self.delayable(), job_function)(*job_args).set( + **delay_args + ) + if parent_job is not None: + parent_job.on_done(job) + if split: + job.split(split, chain=chain) + job.delay() + return job + else: + return getattr(self.with_delay(**delay_args), job_function)(*job_args) + else: + if no_delay: + return getattr(self, job_function)(*job_args) + eta = delay_args.pop("eta", None) + if isinstance(eta, (int | float)): + eta = fields.Datetime.now() + timedelta(seconds=eta) + elif eta is None: + eta = fields.Datetime.now() + create_vals = [ + { + "res_model": self._name, + "res_ids": str(self[i : i + max(split, 1)].ids), + "job_function": job_function, + "job_args": str(job_args), + "user_id": self.env.user.id, + "company_id": self.env.company.id, + "context": str(self.env.context), + "eta": eta, + **delay_args, + } + for i in range(0, max(len(self), 1), max(split, 1)) + ] + if chain: + job = self.env["queue.job.replacement"].sudo() + for vals in create_vals: + vals["parent_job_id"] = job.id + job = job.create(vals) + else: + job = self.env["queue.job.replacement"].sudo().create(create_vals) + self.env.ref( + "queue_job_optional.ir_cron_queue_job_replacement_process" + ).sudo()._trigger() + return job diff --git a/queue_job_optional/models/queue_job_replacement.py b/queue_job_optional/models/queue_job_replacement.py new file mode 100644 index 000000000..8407c182b --- /dev/null +++ b/queue_job_optional/models/queue_job_replacement.py @@ -0,0 +1,228 @@ +import logging +from ast import literal_eval +from contextlib import closing, contextmanager + +from odoo import _, api, fields, models +from odoo.exceptions import UserError, ValidationError + +_logger = logging.getLogger(__name__) + + +class QueueJobReplacement(models.Model): + _name = "queue.job.replacement" + _description = "Queued Job for later execution" + + res_model = fields.Char(required=True) + res_ids = fields.Char(required=True) + job_function = fields.Char(required=True) + user_id = fields.Many2one("res.users", default=lambda self: self.env.user) + company_id = fields.Many2one("res.company", default=lambda self: self.env.company) + context = fields.Char() + job_args = fields.Char() + eta = fields.Datetime(default=fields.Datetime.now, required=True, index=True) + description = fields.Char() + priority = fields.Integer(default=10, required=True, index=True) + channel = fields.Char() + identity_key = fields.Char() + job_result = fields.Text() + state = fields.Selection( + [ + ("pending", "Pending"), + ("processing", "Processing"), + ("done", "Done"), + ("failed", "Failed"), + ], + default="pending", + index=True, + required=True, + ) + parent_job_id = fields.Many2one("queue.job.replacement") + is_predecessor_complete = fields.Boolean( + compute="_compute_is_predecessor_complete", + search="_search_is_predecessor_complete", + ) + + def _compute_is_predecessor_complete(self): + for job in self: + job.is_predecessor_complete = ( + not job.parent_job_id or job.parent_job_id.state == "done" + ) + + def _search_is_predecessor_complete(self, operator, value): + if operator not in ["=", "!="]: + raise UserError( + _("Unsupported operator for is_predecessor_complete search.") + ) + if value: + domain = [ + "|", + ("parent_job_id", "=", False), + ("parent_job_id.state", "=", "done"), + ] + else: + domain = [ + ("parent_job_id", "!=", False), + ("parent_job_id.state", "!=", "done"), + ] + return domain + + @api.model_create_multi + def create(self, vals_list): + field_names = self._fields.keys() + res = self.browse() + to_create = [] + for vals in vals_list: + for key in list(vals.keys()): + if key not in field_names: + vals.pop(key) + if key == "parent_job_id" and not isinstance(vals[key], int): + vals[key] = vals[key].id + if identity_key := vals.get("identity_key"): + pending_job = self.search( + [ + ("identity_key", "=", identity_key), + ("state", "in", ["pending", "processing"]), + ] + ) + if len(pending_job) > 1: + raise ValidationError( + _("Another job with same identity key is already scheduled.") + ) + if pending_job: + if literal_eval(pending_job.job_args) != literal_eval( + vals["job_args"] + ): + raise ValidationError( + _( + "Another job with same identity key is already " + "scheduled with different arguments." + ) + ) + pending_res_ids = literal_eval(pending_job.res_ids) + current_res_ids = literal_eval(vals["res_ids"]) + pending_job.res_ids = str( + list(set(pending_res_ids + current_res_ids)) + ) + res |= pending_job + continue + to_create.append(vals) + res |= super().create(to_create) + return res + + def cron_run_jobs(self): + search_domain = [ + ("state", "=", "pending"), + ("eta", "<=", fields.Datetime.now()), + ] + total_jobs = self.search_count(search_domain) + jobs = self.search( + search_domain + [("is_predecessor_complete", "=", True)], + order="priority,eta", + limit=100, + ) + for job in jobs: + try: + with self._do_in_new_env(new_cr=True) as new_env: + job_new_env = new_env[self._name].browse(job.id) + job_new_env.state = "processing" + records = ( + new_env[job_new_env.res_model] + .with_user(job_new_env.user_id) + .with_company(job_new_env.company_id) + .with_context(**literal_eval(job_new_env.context)) + .browse(literal_eval(job_new_env.res_ids)) + ) + job_function = getattr(records, job_new_env.job_function) + job_result = job_function(*literal_eval(job_new_env.job_args)) + job_new_env.write({"state": "done", "job_result": str(job_result)}) + except Exception as e: + _logger.error("Error processing job", exc_info=True) + job.write({"state": "failed", "job_result": str(e)}) + if jobs and total_jobs > len(jobs): + self.env.ref( + "queue_job_optional.ir_cron_queue_job_replacement_process" + ).sudo()._trigger() + + def requeue(self): + self.write({"state": "pending"}) + + def set_done(self): + self.write({"state": "done"}) + + def drop_job(self, identity_key): + if "queue.job" in self.env: + job = ( + self.env["queue.job"] + .sudo() + .search( + [ + ("identity_key", "=", identity_key), + ("state", "not in", ["done", "cancelled"]), + ] + ) + ) + job.button_done() + job.unlink() + else: + job = self.search( + [("identity_key", "=", identity_key), ("state", "!=", "done")] + ) + job.set_done() + job.unlink() + return True + + def open_related(self): + self.ensure_one() + parsed_ids = literal_eval(self.res_ids) + if isinstance(parsed_ids, int): + record_ids = [parsed_ids] + elif isinstance(parsed_ids, (list | tuple | set)): + record_ids = [int(rec_id) for rec_id in parsed_ids] + else: + raise UserError(_("Related record ids are invalid.")) + + if not record_ids: + raise UserError(_("No related record ids are defined for this job.")) + + action = { + "name": _("Related records"), + "type": "ir.actions.act_window", + "res_model": self.res_model, + "target": "current", + } + if len(record_ids) == 1: + action.update( + { + "res_id": record_ids[0], + "view_mode": "form", + } + ) + else: + action.update( + { + "view_mode": "list,form", + "domain": [("id", "in", record_ids)], + } + ) + return action + + @contextmanager + def _do_in_new_env(self, new_cr=False): + """Context manager that yields a new environment + Copied from OCA storage/fs_attachment module + Using a new Odoo Environment thus a new PG transaction. + """ + if new_cr: + with closing(self.env.registry.cursor()) as cr: + try: + yield self.env(cr=cr) + except Exception: + cr.rollback() + raise + else: + # disable pylint error because this is a valid commit, + # we are in a new env + cr.commit() # pylint: disable=invalid-commit + else: + # make a copy + yield self.env() diff --git a/queue_job_optional/pyproject.toml b/queue_job_optional/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/queue_job_optional/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/queue_job_optional/readme/DESCRIPTION.md b/queue_job_optional/readme/DESCRIPTION.md new file mode 100644 index 000000000..2606423c9 --- /dev/null +++ b/queue_job_optional/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +Abstract the queue_job module from OCA to use it when available and fallback to a +dedicated CRON when the module is not installed. This is helpful for sharing code +between odoo.sh environment and dedicated environment because odoo.sh discourages +the use of the queue_job module. \ No newline at end of file diff --git a/queue_job_optional/security/ir.model.access.csv b/queue_job_optional/security/ir.model.access.csv new file mode 100644 index 000000000..8e2487cc2 --- /dev/null +++ b/queue_job_optional/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_queue_job_replacement_group,queue.job.replacement.all,model_queue_job_replacement,queue_job_optional.group_queue_job_replacement_user,1,1,1,1 +access_queue_job_replacement_system,queue.job.replacement.all,model_queue_job_replacement,base.group_system,1,1,1,1 diff --git a/queue_job_optional/security/res_group.xml b/queue_job_optional/security/res_group.xml new file mode 100644 index 000000000..ef9c2064d --- /dev/null +++ b/queue_job_optional/security/res_group.xml @@ -0,0 +1,7 @@ + + + Manage Queue Job Replacement + + + + diff --git a/queue_job_optional/static/description/index.html b/queue_job_optional/static/description/index.html new file mode 100644 index 000000000..3a545e0d0 --- /dev/null +++ b/queue_job_optional/static/description/index.html @@ -0,0 +1,412 @@ + + + + + +Sponsor to Participant communication + + + + + + diff --git a/queue_job_optional/views/queue_job_replacement_views.xml b/queue_job_optional/views/queue_job_replacement_views.xml new file mode 100644 index 000000000..540d31b74 --- /dev/null +++ b/queue_job_optional/views/queue_job_replacement_views.xml @@ -0,0 +1,121 @@ + + + + queue.job.replacement.view.form + queue.job.replacement + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + queue.job.replacement.view.search + queue.job.replacement + + + + + + + + + + + + + + + + + + + + + + + + queue.job.replacement.view.tree + queue.job.replacement + + + + + + + + + + + + + + + + Queue Job Replacements + queue.job.replacement + list,form + + + + + + + + +
diff --git a/sbc_compassion/__manifest__.py b/sbc_compassion/__manifest__.py index a2ccaa09a..d1f11147e 100644 --- a/sbc_compassion/__manifest__.py +++ b/sbc_compassion/__manifest__.py @@ -65,7 +65,6 @@ "data/child_layouts.xml", "data/correspondence_mappings.xml", "data/gmc_action.xml", - "data/queue_job.xml", "reports/correspondence_report.xml", "data/correspondence_cron.xml", ], diff --git a/sbc_compassion/data/queue_job.xml b/sbc_compassion/data/queue_job.xml deleted file mode 100644 index dbc9827ca..000000000 --- a/sbc_compassion/data/queue_job.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - sbc_compassion - - - - letter_import - - - - - - - generate_letters_job - - - - - run_analyze - - - diff --git a/sbc_compassion/models/correspondence.py b/sbc_compassion/models/correspondence.py index 983cd1378..140f2e281 100644 --- a/sbc_compassion/models/correspondence.py +++ b/sbc_compassion/models/correspondence.py @@ -1305,10 +1305,12 @@ def cron_download_old_correspondence(self): ], ) _logger.info("Downloading %d old letters", len(correspondences)) - correspondences.delayable()._download_old_correspondence().set( + correspondences.with_delay_sh( + "_download_old_correspondence", priority=500, channel="root.sbc_migration", - ).split(10).delay() + split=10, + ) def _download_old_correspondence(self): for correspondence in self: diff --git a/sbc_compassion/models/correspondence_s2b_generator.py b/sbc_compassion/models/correspondence_s2b_generator.py index 5140f09c0..deacbc5d1 100644 --- a/sbc_compassion/models/correspondence_s2b_generator.py +++ b/sbc_compassion/models/correspondence_s2b_generator.py @@ -131,9 +131,9 @@ def generate_letters(self): Launch S2B Creation job :return: True """ - self.with_delay( - identity_key="s2b_generator." + str(self.ids) - ).generate_letters_job() + self.with_delay_sh( + "generate_letters_job", identity_key="s2b_generator." + str(self.ids) + ) return { "type": "ir.actions.client", "tag": "display_notification", diff --git a/sbc_compassion/models/import_letters_history.py b/sbc_compassion/models/import_letters_history.py index 2e40f58fb..6cb67b85f 100644 --- a/sbc_compassion/models/import_letters_history.py +++ b/sbc_compassion/models/import_letters_history.py @@ -100,9 +100,15 @@ def create(self, vals_list): def button_import(self): self.ensure_one() self.state = "pending" - job = self.delayable().run_analyze() - after_job = self.delayable().write({"state": "open"}) - job.on_done(after_job).delay() + job = self.with_delay_sh( + "run_analyze", channel="root.sbc_compassion.letter_import" + ) + self.with_delay_sh( + "write", + {"state": "open"}, + channel="root.sbc_compassion.letter_import", + parent_job_id=job, + ) return { "type": "ir.actions.client", "tag": "display_notification", diff --git a/sbc_translation/__manifest__.py b/sbc_translation/__manifest__.py index 6f214f104..c6f96f2a1 100644 --- a/sbc_translation/__manifest__.py +++ b/sbc_translation/__manifest__.py @@ -45,7 +45,6 @@ "wizards/translation_letter_counting_view.xml", "data/mail_template.xml", "data/update_translation_priority_cron.xml", - "data/queue_job.xml", "views/translation_user_view.xml", "views/correspondence_view.xml", "views/translation_pool_view.xml", diff --git a/sbc_translation/data/queue_job.xml b/sbc_translation/data/queue_job.xml deleted file mode 100644 index 3e6e445f1..000000000 --- a/sbc_translation/data/queue_job.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - move_pool - - - diff --git a/sbc_translation/models/correspondence.py b/sbc_translation/models/correspondence.py index b1e2e0dfb..98143320f 100644 --- a/sbc_translation/models/correspondence.py +++ b/sbc_translation/models/correspondence.py @@ -630,12 +630,13 @@ def _post_process_translation(self): ) if is_s2b: # Send to GMC - self.with_user(SUPERUSER_ID).with_delay( + self.with_user(SUPERUSER_ID).with_delay_sh( + "create_commkit", channel="root.sbc_compassion", priority=100, description="Create Commkit", identity_key=f"sbc.create_commkit.{self.ids}", - ).create_commkit() + ) def list_letters(self): """API call to fetch letters to translate""" diff --git a/sponsorship_compassion/__manifest__.py b/sponsorship_compassion/__manifest__.py index c8ee1512c..a85f14315 100644 --- a/sponsorship_compassion/__manifest__.py +++ b/sponsorship_compassion/__manifest__.py @@ -72,7 +72,6 @@ "data/partner_category_data.xml", "data/utm_data.xml", "data/res_partner_sequence.xml", - "data/queue_job.xml", "data/sync_projects_from_gmc_cron.xml", "views/product_views.xml", "views/res_config_settings_view.xml", diff --git a/sponsorship_compassion/data/queue_job.xml b/sponsorship_compassion/data/queue_job.xml deleted file mode 100644 index 2b033e2da..000000000 --- a/sponsorship_compassion/data/queue_job.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - put_child_on_no_money_hold - - - - - cancel_old_invoices - - - diff --git a/sponsorship_compassion/models/contract_group.py b/sponsorship_compassion/models/contract_group.py index fa7aa3268..b16b22dac 100644 --- a/sponsorship_compassion/models/contract_group.py +++ b/sponsorship_compassion/models/contract_group.py @@ -33,11 +33,11 @@ def _compute_contains_sponsorship(self): and s.state not in ("terminated", "cancelled") ) - def _generate_invoices(self, invoicer): + def _generate_invoices(self, invoicer_id=False): # Exclude gifts from regular generation - super( + invoicer = super( ContractGroup, self.with_context(open_invoices_sponsorship_only=True) - )._generate_invoices(invoicer) + )._generate_invoices(invoicer_id) contracts = self.active_contract_ids if contracts: contracts._generate_gifts( @@ -46,7 +46,7 @@ def _generate_invoices(self, invoicer): contracts._generate_gifts( invoicer, self.env.ref("sponsorship_compassion.gift_type_christmas") ) - return True + return invoicer def build_inv_line_data( self, invoicing_date=False, gift_wizard=False, contract_line=False diff --git a/sponsorship_compassion/models/contracts.py b/sponsorship_compassion/models/contracts.py index c6c715752..b61db0253 100644 --- a/sponsorship_compassion/models/contracts.py +++ b/sponsorship_compassion/models/contracts.py @@ -811,11 +811,13 @@ def contract_active(self): # Cancel the old invoices if a contract is activated delay = datetime.now() + relativedelta(seconds=30) - self.with_delay( + self.with_delay_sh( + "cancel_old_invoices", + channel="root.accounting", eta=delay, priority=500, identity_key=self._name + ".cancel_old_invoices." + str(self.ids), - ).cancel_old_invoices() + ) con_line_obj = self.env["recurring.contract.line"] for contract in self.filtered(lambda c: c.type in SPONSORSHIP_TYPE_LIST): diff --git a/sponsorship_compassion/models/project_compassion.py b/sponsorship_compassion/models/project_compassion.py index 3167e07ba..494ecd4dd 100644 --- a/sponsorship_compassion/models/project_compassion.py +++ b/sponsorship_compassion/models/project_compassion.py @@ -108,12 +108,13 @@ def sync_projects_from_gmc( for i, p in enumerate(projects_to_sync): # Only synchronise projects for which we have sponsorships to speedup # execution and decrease remote server load - p.with_delay( + p.with_delay_sh( + "_sync_from_gmc", eta=requests_throttle_seconds * i, priority=500, channel="root.gmc_pool.fcp_compassion", description="Sync project from GMC", - )._sync_from_gmc() + ) def _sync_from_gmc(self): self.ensure_one() diff --git a/sponsorship_compassion/models/res_partner.py b/sponsorship_compassion/models/res_partner.py index 537aa252b..706711558 100644 --- a/sponsorship_compassion/models/res_partner.py +++ b/sponsorship_compassion/models/res_partner.py @@ -502,7 +502,7 @@ def _random_str(): [("res_model", "=", self._name), ("res_id", "=", partner.id)] ).unlink() partner.message_follower_ids.unlink() - partner.with_delay().clear_message_history() + partner.with_delay_sh("clear_message_history") return True def clear_message_history(self): diff --git a/thankyou_letters/__manifest__.py b/thankyou_letters/__manifest__.py index 61523c28e..3bb8b2860 100644 --- a/thankyou_letters/__manifest__.py +++ b/thankyou_letters/__manifest__.py @@ -48,7 +48,6 @@ "security/ir.model.access.csv", "data/email_template.xml", "data/communication_config.xml", - "data/queue_job.xml", "views/success_story_view.xml", "views/communication_job_view.xml", "views/account_invoice_view.xml", diff --git a/thankyou_letters/data/queue_job.xml b/thankyou_letters/data/queue_job.xml deleted file mode 100644 index bea4274ed..000000000 --- a/thankyou_letters/data/queue_job.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - generate_thank_you - - - diff --git a/thankyou_letters/models/account_move.py b/thankyou_letters/models/account_move.py index a33a2daf7..db6975f36 100644 --- a/thankyou_letters/models/account_move.py +++ b/thankyou_letters/models/account_move.py @@ -70,9 +70,11 @@ def _compute_amount(self): and new_payment_states[i] != "paid" and invoice.communication_id.state == "pending" ): - invoice.with_delay( - channel="root.thankyou_letters", priority=50 - ).cancel_thankyou_letter() + invoice.with_delay_sh( + "cancel_thankyou_letter", + channel="root.thankyou_letters", + priority=50, + ) return res def group_by_partner(self): @@ -101,11 +103,12 @@ def generate_thank_you(self): and line.product_id.requires_thankyou ) if move_lines: - move_lines.with_delay( + move_lines.with_delay_sh( + "generate_thank_you", channel="root.thankyou_letters", priority=50, identity_key=f"thank_you_for_lines_{move_lines.ids}", - ).generate_thank_you() + ) def cancel_thankyou_letter(self): for move in self: @@ -124,11 +127,12 @@ def cancel_thankyou_letter(self): remaining_lines = self.env["account.move.line"].browse( [int(i) for i in object_ids.split(",")] ) - remaining_lines.with_delay( + remaining_lines.with_delay_sh( + "generate_thank_you", channel="root.thankyou_letters", priority=50, identity_key=f"thank_you_for_lines_{remaining_lines.ids}", - ).generate_thank_you() + ) def _filter_move_to_thank(self, move_type=None): """ diff --git a/thankyou_letters/wizards/generate_communication_wizard.py b/thankyou_letters/wizards/generate_communication_wizard.py index eaa42cf22..f43adc78d 100644 --- a/thankyou_letters/wizards/generate_communication_wizard.py +++ b/thankyou_letters/wizards/generate_communication_wizard.py @@ -90,11 +90,15 @@ def generate_communications(self, async_mode=True): options = {"force_language": self.force_language} if async_mode or self.scheduled_date: - self.with_delay( + self.with_delay_sh( + "create_communication", + vals, + options, + channel="root.partner_communication", eta=self.scheduled_date, priority=50, identity_key=f"{self._name}.create_comm.invoice.{move_line.id}", - ).create_communication(vals, options) + ) else: self.create_communication(vals, options)