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

+ + +

Beta License: AGPL-3 OCA/credit-control Translate me on Weblate Try me on Runboat

+

Once this module is installed, the emails will be sent in individual +jobs.

+

Table of contents

+ +
+

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.

+ +Odoo Community Association + +

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 + + + + + + + + + +