diff --git a/account_credit_control/models/credit_control_communication.py b/account_credit_control/models/credit_control_communication.py
index 00ca84288..abe545a7d 100644
--- a/account_credit_control/models/credit_control_communication.py
+++ b/account_credit_control/models/credit_control_communication.py
@@ -4,9 +4,8 @@
# Copyright 2020 Manuel Calero - Tecnativa
# Copyright 2023 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-import threading
-from odoo import _, api, fields, models, modules, registry, tools
+from odoo import _, api, fields, models
from odoo.tools.misc import format_amount, format_date
@@ -233,36 +232,20 @@ def _generate_emails(self):
comm._send_mails()
def _send_mails(self):
- # Launch process in new thread to improve the user speedup
- if not tools.config["test_enable"] and not modules.module.current_test:
-
- @self.env.cr.postcommit.add
- def _launch_print_thread():
- threaded_calculation = threading.Thread(
- target=self.send_mails_threaded,
- args=self.ids,
- )
- threaded_calculation.start()
- else:
- self._send_communications_by_email()
-
- def send_mails_threaded(self, record_ids):
- with registry(self._cr.dbname).cursor() as cr:
- self = self.with_env(self.env(cr=cr))
- communications = self.browse(record_ids)
- communications._send_communications_by_email()
+ # in account_credit_control_queue_job, override this method
+ # to loop over self and call _send_communications_by_email with delay
+ self._send_communications_by_email()
def _send_communications_by_email(self):
for comm in self:
- comm.message_mail_with_source(
+ # in mass_mail mode, the subtype is dropped, which is used by the
+ # postprocessing that marks control lines as sent.lines
+ comm.message_post_with_source(
comm.policy_level_id.email_template_id,
subtype_id=self.env["ir.model.data"]._xmlid_to_res_id(
"account_credit_control.mt_request"
),
)
- comm.credit_control_line_ids.filtered(
- lambda line: line.state == "queued"
- ).state = "sent"
def _mark_credit_line_as_sent(self):
lines = self.mapped("credit_control_line_ids")
diff --git a/account_credit_control/models/credit_control_run.py b/account_credit_control/models/credit_control_run.py
index e44d9bcd2..77816fb7b 100644
--- a/account_credit_control/models/credit_control_run.py
+++ b/account_credit_control/models/credit_control_run.py
@@ -208,16 +208,6 @@ def run_channel_action(self):
comm_obj = self.env["credit.control.communication"]
comms = comm_obj._generate_comm_from_credit_lines(email_lines)
comms._generate_emails()
- # Notify user that the emails will be sent in background
- self.env["bus.bus"]._sendone(
- self.env.user.partner_id,
- "simple_notification",
- {
- "type": "info",
- "title": _("Notifications"),
- "message": _("The emails will be sent in the background"),
- },
- )
if letter_lines:
wiz = self.env["credit.control.printer"].create(
{"line_ids": letter_lines.ids}
diff --git a/account_credit_control/models/mail_mail.py b/account_credit_control/models/mail_mail.py
index 938c2d345..c7902c976 100644
--- a/account_credit_control/models/mail_mail.py
+++ b/account_credit_control/models/mail_mail.py
@@ -8,10 +8,7 @@
class Mail(models.Model):
_inherit = "mail.mail"
- def _postprocess_sent_message(
- self, success_pids, failure_reason=False, failure_type=None
- ):
- """Mark credit control lines states."""
+ def _update_control_line_status(self):
for mail in self:
msg = mail.mail_message_id
if msg.model != "credit.control.communication":
@@ -23,8 +20,38 @@ def _postprocess_sent_message(
)
new_state = "sent" if mail.state == "sent" else "email_error"
lines.write({"state": new_state})
+
+ def _postprocess_sent_message(
+ self, success_pids, failure_reason=False, failure_type=None
+ ):
+ """Mark credit control lines states."""
+ self._update_control_line_status()
return super()._postprocess_sent_message(
success_pids=success_pids,
failure_reason=failure_reason,
failure_type=failure_type,
)
+
+ def _send(
+ self,
+ auto_commit=False,
+ raise_exception=False,
+ smtp_session=None,
+ alias_domain_id=False,
+ mail_server=False,
+ post_send_callback=None,
+ ):
+ # because of
+ # https://github.com/odoo/odoo/blob/bcba6c0dda4818e67a9023beb26593a7d74ff6a6/
+ # addons/mail/models/mail_mail.py#L606-L607
+ # we don't go through _postprocess_sent_message if the address is blacklisted
+ no_postprocess = self.filtered(lambda m: m.state != "outgoing")
+ no_postprocess._update_control_line_status()
+ return super()._send(
+ auto_commit=auto_commit,
+ raise_exception=raise_exception,
+ smtp_session=smtp_session,
+ alias_domain_id=alias_domain_id,
+ mail_server=mail_server,
+ post_send_callback=post_send_callback,
+ )
diff --git a/account_credit_control/tests/test_credit_control_run.py b/account_credit_control/tests/test_credit_control_run.py
index 266b4e8bd..758faca75 100644
--- a/account_credit_control/tests/test_credit_control_run.py
+++ b/account_credit_control/tests/test_credit_control_run.py
@@ -14,8 +14,7 @@
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
-@tagged("post_install", "-at_install")
-class TestCreditControlRun(AccountTestInvoicingCommon):
+class TestCreditControlRunCase(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -79,6 +78,9 @@ def setUpClass(cls):
cls.invoice = invoice_form.save()
cls.invoice.action_post()
+
+@tagged("post_install", "-at_install")
+class TestCreditControlRun(TestCreditControlRunCase):
def test_check_run_date(self):
"""
Create a control run older than the last control run
@@ -127,10 +129,10 @@ def test_generate_credit_lines(self):
)
report_regex = (
- rf'
Policy "{self.policy.name}" has generated '
- r"\d+ Credit Control Lines.
"
+ rf'Policy "{self.policy.name}" has generated '
+ r"\d+ Credit Control Lines.
"
)
- regex_result = re.match(report_regex, control_run.report)
+ regex_result = re.search(report_regex, control_run.report)
self.assertIsNotNone(regex_result)
def test_generate_credit_lines_with_max_level(self):
@@ -213,10 +215,10 @@ def test_wiz_print_lines(self):
self.assertEqual(control_run.state, "done")
report_regex = (
- rf'Policy "{self.policy.name}" has generated '
- r"\d+ Credit Control Lines.
"
+ rf'Policy "{self.policy.name}" has generated '
+ r"\d+ Credit Control Lines.
"
)
- regex_result = re.match(report_regex, control_run.report)
+ regex_result = re.search(report_regex, control_run.report)
self.assertIsNotNone(regex_result)
# Mark lines to be send
@@ -248,10 +250,10 @@ def test_wiz_credit_control_emailer(self):
self.assertEqual(control_run.state, "done")
report_regex = (
- rf'Policy "{self.policy.name}" has generated '
- r"\d+ Credit Control Lines.
"
+ rf'Policy "{self.policy.name}" has generated '
+ r"\d+ Credit Control Lines.
"
)
- regex_result = re.match(report_regex, control_run.report)
+ regex_result = re.search(report_regex, control_run.report)
self.assertIsNotNone(regex_result)
# Mark lines to be send
@@ -288,30 +290,42 @@ def test_sent_email_invoice_detail(self):
{"name": "to_be_sent", "line_ids": [(6, 0, control_lines.ids)]}
)
marker.mark_lines()
- emailer_obj = self.env["credit.control.emailer"].with_context(
- domain_notifications_email="test@example.com"
- )
- wiz_emailer = emailer_obj.create({})
+ wiz_emailer = self.env["credit.control.emailer"].create({})
wiz_emailer.line_ids = control_lines
+ self.env.user.company_id.email = "test@example.com"
with RecordCapturer(self.env["credit.control.communication"], []) as capture:
- wiz_emailer.email_lines()
+ wiz_emailer.with_context(queue_job__no_delay=True).email_lines()
new_communication = capture.records
self.assertEqual(len(new_communication), 1)
self.assertEqual(len(new_communication.message_ids), 1)
# Verify that the email include the invoice details.
self.assertIn("Invoices summary", new_communication.message_ids.body)
self.assertIn(self.invoice.name, new_communication.message_ids.body)
+
+ def test_sent_email_no_invoice_detail(self):
+ """
+ Verify that the email is sent and does not include the invoice details
+ """
+ policy_level_expected = self.env.ref("account_credit_control.3_time_1")
+ self.invoice.partner_id.email = "test@test.com"
+ self.env.user.company_id.email = "test@example.com"
+ control_run = self.env["credit.control.run"].create(
+ {"date": fields.Date.today(), "policy_ids": [(6, 0, [self.policy.id])]}
+ )
+ control_run.with_context(lang="en_US").generate_credit_lines()
+ self.assertTrue(len(self.invoice.credit_control_line_ids), 1)
+ control_lines = self.invoice.credit_control_line_ids
+ self.assertEqual(control_lines.policy_level_id, policy_level_expected)
# CASE 2: set the policy level to show invoice details = False
control_lines.policy_level_id.mail_show_invoice_detail = False
- control_lines.state = "to_be_sent"
marker = self.env["credit.control.marker"].create(
{"name": "to_be_sent", "line_ids": [(6, 0, control_lines.ids)]}
)
marker.mark_lines()
- wiz_emailer = emailer_obj.create({})
+ wiz_emailer = self.env["credit.control.emailer"].create({})
wiz_emailer.line_ids = control_lines
with RecordCapturer(self.env["credit.control.communication"], []) as capture:
- wiz_emailer.email_lines()
+ wiz_emailer.with_context(queue_job__no_delay=True).email_lines()
new_communication = capture.records
self.assertEqual(len(new_communication), 1)
self.assertEqual(len(new_communication.message_ids), 1)
diff --git a/account_credit_control_queue_job/README.rst b/account_credit_control_queue_job/README.rst
new file mode 100644
index 000000000..e9a4ecf11
--- /dev/null
+++ b/account_credit_control_queue_job/README.rst
@@ -0,0 +1,72 @@
+======================
+Account Credit Control
+======================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:e19d48059d05f79acf49652dc0b98548511fb71b832aa80fa9164df08895de55
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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-OCA%2Fcredit--control-lightgray.png?logo=github
+ :target: https://github.com/OCA/credit-control/tree/18.0/account_credit_control_queue_job
+ :alt: OCA/credit-control
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/credit-control-18-0/credit-control-18-0-account_credit_control_queue_job
+ :alt: Translate me on Weblate
+.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/credit-control&target_branch=18.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+Once this module is installed, the emails will be sent in individual
+jobs.
+
+**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
+-------
+
+* 360 ERP
+
+Maintainers
+-----------
+
+This module is maintained by the OCA.
+
+.. image:: https://odoo-community.org/logo.png
+ :alt: Odoo Community Association
+ :target: https://odoo-community.org
+
+OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
+This module is part of the `OCA/credit-control `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/account_credit_control_queue_job/__init__.py b/account_credit_control_queue_job/__init__.py
new file mode 100644
index 000000000..d6c56a95f
--- /dev/null
+++ b/account_credit_control_queue_job/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import models
+from . import wizard
diff --git a/account_credit_control_queue_job/__manifest__.py b/account_credit_control_queue_job/__manifest__.py
new file mode 100644
index 000000000..a4504959a
--- /dev/null
+++ b/account_credit_control_queue_job/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2025 360ERP ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+{
+ "name": "Account Credit Control",
+ "version": "18.0.1.0.1",
+ "author": "360 ERP, Odoo Community Association (OCA)",
+ "category": "Finance",
+ "depends": [
+ "account_credit_control",
+ "queue_job_batch",
+ ],
+ "website": "https://github.com/OCA/credit-control",
+ "data": ["wizard/res_config_settings.xml"],
+ "installable": True,
+ "auto_install": False,
+ "license": "AGPL-3",
+ "application": True,
+}
diff --git a/account_credit_control_queue_job/models/__init__.py b/account_credit_control_queue_job/models/__init__.py
new file mode 100644
index 000000000..7d082e568
--- /dev/null
+++ b/account_credit_control_queue_job/models/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import credit_control_communication
+from . import credit_control_run
diff --git a/account_credit_control_queue_job/models/credit_control_communication.py b/account_credit_control_queue_job/models/credit_control_communication.py
new file mode 100644
index 000000000..01e98a4c9
--- /dev/null
+++ b/account_credit_control_queue_job/models/credit_control_communication.py
@@ -0,0 +1,27 @@
+# Copyright 2025 360ERP ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, models
+from odoo.tools import split_every
+
+
+class CreditControlCommunication(models.Model):
+ _inherit = "credit.control.communication"
+
+ def _send_mails(self):
+ key = "account_credit_control_queue_job.batch_size"
+ batch_size = self.env["ir.config_parameter"].sudo().get_param(key)
+ try:
+ batch_size = max(1, int(batch_size))
+ except Exception: # pylint: disable=broad-except
+ batch_size = 1
+ batch_name = _("Credit Control Emails")
+ batch = self.env["queue.job.batch"].get_new_batch(batch_name)
+ for comms in split_every(batch_size, self.ids, self.browse):
+ if batch_size > 1:
+ desc = _("Sending credit control emails for ids: %s") % comms.ids
+ else:
+ desc = _("Sending credit control email for %s") % comms.partner_id.name
+ comms.with_context(job_batch=batch).with_delay(
+ description=desc
+ )._send_communications_by_email()
diff --git a/account_credit_control_queue_job/models/credit_control_run.py b/account_credit_control_queue_job/models/credit_control_run.py
new file mode 100644
index 000000000..145616f56
--- /dev/null
+++ b/account_credit_control_queue_job/models/credit_control_run.py
@@ -0,0 +1,19 @@
+# Copyright 2025 360ERP ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import _, models
+
+
+class CreditControlRun(models.Model):
+ _inherit = "credit.control.run"
+
+ def run_channel_action(self):
+ res = super().run_channel_action()
+ target = self.env.user.partner_id
+ msg = {
+ "type": "info",
+ "title": _("Jobs enqueued"),
+ "message": _("The emails will be sent in the background"),
+ }
+ self.env["bus.bus"]._sendone(target, "simple_notification", msg)
+ return res
diff --git a/account_credit_control_queue_job/pyproject.toml b/account_credit_control_queue_job/pyproject.toml
new file mode 100644
index 000000000..4231d0ccc
--- /dev/null
+++ b/account_credit_control_queue_job/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["whool"]
+build-backend = "whool.buildapi"
diff --git a/account_credit_control_queue_job/readme/DESCRIPTION.md b/account_credit_control_queue_job/readme/DESCRIPTION.md
new file mode 100644
index 000000000..384b23854
--- /dev/null
+++ b/account_credit_control_queue_job/readme/DESCRIPTION.md
@@ -0,0 +1 @@
+Once this module is installed, the emails will be sent in individual jobs.
diff --git a/account_credit_control_queue_job/static/description/icon.png b/account_credit_control_queue_job/static/description/icon.png
new file mode 100644
index 000000000..3a0328b51
Binary files /dev/null and b/account_credit_control_queue_job/static/description/icon.png differ
diff --git a/account_credit_control_queue_job/static/description/index.html b/account_credit_control_queue_job/static/description/index.html
new file mode 100644
index 000000000..bb76fa5a1
--- /dev/null
+++ b/account_credit_control_queue_job/static/description/index.html
@@ -0,0 +1,417 @@
+
+
+
+
+
+Account Credit Control
+
+
+
+
+
Account Credit Control
+
+
+

+
Once this module is installed, the emails will be sent in individual
+jobs.
+
Table of contents
+
+
+
+
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.
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
This module is part of the OCA/credit-control project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/account_credit_control_queue_job/tests/__init__.py b/account_credit_control_queue_job/tests/__init__.py
new file mode 100644
index 000000000..7dde1d5ae
--- /dev/null
+++ b/account_credit_control_queue_job/tests/__init__.py
@@ -0,0 +1,2 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+from . import test_credit_control_run
diff --git a/account_credit_control_queue_job/tests/test_credit_control_run.py b/account_credit_control_queue_job/tests/test_credit_control_run.py
new file mode 100644
index 000000000..6ed1967e7
--- /dev/null
+++ b/account_credit_control_queue_job/tests/test_credit_control_run.py
@@ -0,0 +1,35 @@
+# Copyright 2025 360ERP ()
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import fields
+from odoo.tests import tagged
+
+from odoo.addons.account_credit_control.tests.test_credit_control_run import (
+ TestCreditControlRunCase,
+)
+
+
+@tagged("post_install", "-at_install")
+class TestCreditControlRun(TestCreditControlRunCase):
+ def test_sent_email_in_job(self):
+ self.invoice.partner_id.email = "test@test.com"
+ control_run = self.env["credit.control.run"].create(
+ {"date": fields.Date.today(), "policy_ids": [(6, 0, [self.policy.id])]}
+ )
+ control_run.generate_credit_lines()
+ self.assertTrue(len(self.invoice.credit_control_line_ids), 1)
+ control_lines = self.invoice.credit_control_line_ids
+ marker = self.env["credit.control.marker"].create(
+ {"name": "to_be_sent", "line_ids": [(6, 0, control_lines.ids)]}
+ )
+ marker.mark_lines()
+ wiz_emailer = self.env["credit.control.emailer"].create({})
+ wiz_emailer.line_ids = control_lines
+
+ wiz_emailer.email_lines()
+
+ communications = control_lines.communication_id
+ self.assertTrue(communications)
+ domain = [("method_name", "=", "_send_communications_by_email")]
+ jobs = self.env["queue.job"].sudo().search(domain)
+ self.assertEqual(len(jobs), len(communications))
diff --git a/account_credit_control_queue_job/wizard/__init__.py b/account_credit_control_queue_job/wizard/__init__.py
new file mode 100644
index 000000000..90017c158
--- /dev/null
+++ b/account_credit_control_queue_job/wizard/__init__.py
@@ -0,0 +1,4 @@
+# Copyright 2025 360ERP ()
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from . import res_config_settings
diff --git a/account_credit_control_queue_job/wizard/res_config_settings.py b/account_credit_control_queue_job/wizard/res_config_settings.py
new file mode 100644
index 000000000..9cba83800
--- /dev/null
+++ b/account_credit_control_queue_job/wizard/res_config_settings.py
@@ -0,0 +1,14 @@
+# Copyright 2025 360ERP (https://360erp.com)
+# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).
+
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ credit_control_batch_size = fields.Integer(
+ "Batch Size for Credit Control",
+ default=1,
+ config_parameter="account_credit_control_queue_job.batch_size",
+ )
diff --git a/account_credit_control_queue_job/wizard/res_config_settings.xml b/account_credit_control_queue_job/wizard/res_config_settings.xml
new file mode 100644
index 000000000..18c865e5a
--- /dev/null
+++ b/account_credit_control_queue_job/wizard/res_config_settings.xml
@@ -0,0 +1,23 @@
+
+
+
+ res.config.settings.view.account_credit_control_queue_job
+ res.config.settings
+
+
+
+
+
+
+
+
+
+