From 9336636b3b10a1645830f642b79e8227f0ecc145 Mon Sep 17 00:00:00 2001 From: len Date: Thu, 10 Apr 2025 23:45:10 +0200 Subject: [PATCH 1/8] [FIX] account_credit_control: put the credit lines in error if mail was blacklisted Before, if the contact email is balcklisted, the control lines just appear as "queued" forever, even if the mail was canceled. This is due to postprocess not being called. --- account_credit_control/models/mail_mail.py | 35 +++++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) 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, + ) From 4914a0339a31c07658890369837e4bb52053baa4 Mon Sep 17 00:00:00 2001 From: len Date: Wed, 9 Jul 2025 22:41:18 +0200 Subject: [PATCH 2/8] [FIX] account_credit_control: correctly update control lines status --- .../models/credit_control_communication.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/account_credit_control/models/credit_control_communication.py b/account_credit_control/models/credit_control_communication.py index 00ca84288..c75d4b239 100644 --- a/account_credit_control/models/credit_control_communication.py +++ b/account_credit_control/models/credit_control_communication.py @@ -254,15 +254,14 @@ def send_mails_threaded(self, record_ids): 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") From 5bbd147377da578f83f6f1b12fbb77d017dc4046 Mon Sep 17 00:00:00 2001 From: len Date: Wed, 9 Jul 2025 23:48:44 +0200 Subject: [PATCH 3/8] [FIX] account_credit_control: more flexible tests The root tag for the message could be a span in certain cases, so these are excluded from the check. The email_from is defined in the template, so it should be set on the company to be able to send it. --- .../tests/test_credit_control_run.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/account_credit_control/tests/test_credit_control_run.py b/account_credit_control/tests/test_credit_control_run.py index 266b4e8bd..6bd71e7b5 100644 --- a/account_credit_control/tests/test_credit_control_run.py +++ b/account_credit_control/tests/test_credit_control_run.py @@ -127,10 +127,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 +213,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 +248,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 @@ -293,6 +293,7 @@ def test_sent_email_invoice_detail(self): ) wiz_emailer = emailer_obj.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() new_communication = capture.records From 5a4224319bab89196b3171437263910eea2f370c Mon Sep 17 00:00:00 2001 From: len Date: Tue, 15 Jul 2025 14:09:25 +0200 Subject: [PATCH 4/8] Revert "[IMP] account_credit_control: Send communication emails in background" This reverts commit 834e7ade44908ef4a1c3d456b80b4be8330a708b. Commit: https://github.com/odoo/odoo/commit/543a3ad204232cc67ae9e099f43960b664e8de19 Contains the comment that ``` ``` So a thread is generally assumed to take at most 2 cursors. Because of this, it is essentially a very bad idea to open an unlimited number of cursors. (On a decently sized DB, about 400 lines would be processed in parallel...) Because things are launched in their own cursor, this doesn't impact the main request, so the run is marked as done and all lines are marked as queued, despite the emails never being scheduled. There is already a way to manage this kind of cases in the OCA that is called `queue_job`. Therefore this is the module that should handle this. If for some reason one would still want the threaded code nonetheless, it should be put in a custom project module, as it relies on some environment hypothesis that can't be tested from within the code. Note that one test is split into two; although it is better to avoid code duplication, the test was cheating the setup, and therefore not representative of the real user flow is when users click on the interface buttons. --- .../models/credit_control_communication.py | 24 ++++--------------- .../models/credit_control_run.py | 10 -------- .../tests/test_credit_control_run.py | 23 +++++++++++++----- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/account_credit_control/models/credit_control_communication.py b/account_credit_control/models/credit_control_communication.py index c75d4b239..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,24 +232,9 @@ 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: 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/tests/test_credit_control_run.py b/account_credit_control/tests/test_credit_control_run.py index 6bd71e7b5..9deb38fa9 100644 --- a/account_credit_control/tests/test_credit_control_run.py +++ b/account_credit_control/tests/test_credit_control_run.py @@ -288,10 +288,7 @@ 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: @@ -302,14 +299,28 @@ def test_sent_email_invoice_detail(self): # 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() From 3315c4049ef727bef5acbd9e920b2ae7186661ca Mon Sep 17 00:00:00 2001 From: len Date: Tue, 15 Jul 2025 14:47:41 +0200 Subject: [PATCH 5/8] [IMP] account_credit_control: make test class inheritable --- account_credit_control/tests/test_credit_control_run.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/account_credit_control/tests/test_credit_control_run.py b/account_credit_control/tests/test_credit_control_run.py index 9deb38fa9..ebe63f2ee 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 From 75e63c5c7333b8e29fd83e3d6719d223e59aab2d Mon Sep 17 00:00:00 2001 From: len Date: Tue, 15 Jul 2025 14:49:18 +0200 Subject: [PATCH 6/8] [ADD] account_credit_control_queue_job --- account_credit_control_queue_job/README.rst | 72 +++ account_credit_control_queue_job/__init__.py | 3 + .../__manifest__.py | 19 + .../models/__init__.py | 3 + .../models/credit_control_communication.py | 23 + .../models/credit_control_run.py | 19 + .../pyproject.toml | 3 + .../readme/DESCRIPTION.md | 1 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 417 ++++++++++++++++++ .../tests/__init__.py | 2 + .../tests/test_credit_control_run.py | 35 ++ .../wizard/__init__.py | 4 + .../wizard/res_config_settings.py | 14 + .../wizard/res_config_settings.xml | 23 + 15 files changed, 638 insertions(+) create mode 100644 account_credit_control_queue_job/README.rst create mode 100644 account_credit_control_queue_job/__init__.py create mode 100644 account_credit_control_queue_job/__manifest__.py create mode 100644 account_credit_control_queue_job/models/__init__.py create mode 100644 account_credit_control_queue_job/models/credit_control_communication.py create mode 100644 account_credit_control_queue_job/models/credit_control_run.py create mode 100644 account_credit_control_queue_job/pyproject.toml create mode 100644 account_credit_control_queue_job/readme/DESCRIPTION.md create mode 100644 account_credit_control_queue_job/static/description/icon.png create mode 100644 account_credit_control_queue_job/static/description/index.html create mode 100644 account_credit_control_queue_job/tests/__init__.py create mode 100644 account_credit_control_queue_job/tests/test_credit_control_run.py create mode 100644 account_credit_control_queue_job/wizard/__init__.py create mode 100644 account_credit_control_queue_job/wizard/res_config_settings.py create mode 100644 account_credit_control_queue_job/wizard/res_config_settings.xml 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..1885ef2a2 --- /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.0", + "author": "360 ERP, Odoo Community Association (OCA)", + "category": "Finance", + "depends": [ + "account_credit_control", + "queue_job", + ], + "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..1b2362d13 --- /dev/null +++ b/account_credit_control_queue_job/models/credit_control_communication.py @@ -0,0 +1,23 @@ +# 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 + 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_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 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 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 + + + + + + + + + + From f839e48a96e49818ae2af47406ee59d75f13a952 Mon Sep 17 00:00:00 2001 From: len Date: Tue, 15 Jul 2025 15:17:12 +0200 Subject: [PATCH 7/8] [FIXTEST] account_credit_control: make tests work with account_credit_control_queue_job initialized Since tests first initialize modules then run the tests, this module needs to pass the context key queue_job__no_delay --- account_credit_control/tests/test_credit_control_run.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account_credit_control/tests/test_credit_control_run.py b/account_credit_control/tests/test_credit_control_run.py index ebe63f2ee..758faca75 100644 --- a/account_credit_control/tests/test_credit_control_run.py +++ b/account_credit_control/tests/test_credit_control_run.py @@ -294,7 +294,7 @@ def test_sent_email_invoice_detail(self): 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) @@ -325,7 +325,7 @@ def test_sent_email_no_invoice_detail(self): 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) From a27f79031fa841817fd41fd3cbadd257082057ec Mon Sep 17 00:00:00 2001 From: len Date: Sat, 26 Jul 2025 11:30:10 +0200 Subject: [PATCH 8/8] [IMP] account_credit_control_queue_job: put jobs in batch --- account_credit_control_queue_job/__manifest__.py | 4 ++-- .../models/credit_control_communication.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/account_credit_control_queue_job/__manifest__.py b/account_credit_control_queue_job/__manifest__.py index 1885ef2a2..a4504959a 100644 --- a/account_credit_control_queue_job/__manifest__.py +++ b/account_credit_control_queue_job/__manifest__.py @@ -3,12 +3,12 @@ { "name": "Account Credit Control", - "version": "18.0.1.0.0", + "version": "18.0.1.0.1", "author": "360 ERP, Odoo Community Association (OCA)", "category": "Finance", "depends": [ "account_credit_control", - "queue_job", + "queue_job_batch", ], "website": "https://github.com/OCA/credit-control", "data": ["wizard/res_config_settings.xml"], diff --git a/account_credit_control_queue_job/models/credit_control_communication.py b/account_credit_control_queue_job/models/credit_control_communication.py index 1b2362d13..01e98a4c9 100644 --- a/account_credit_control_queue_job/models/credit_control_communication.py +++ b/account_credit_control_queue_job/models/credit_control_communication.py @@ -15,9 +15,13 @@ def _send_mails(self): 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_delay(description=desc)._send_communications_by_email() + comms.with_context(job_batch=batch).with_delay( + description=desc + )._send_communications_by_email()