From 75616d2eb5cc4e8198df8bf62f09eaf361998264 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 21 Nov 2019 17:10:21 +0100 Subject: [PATCH 01/23] Add export_async_schedule Schedule exports based on an export list. Susbcribed users will receive an email at the configured interval. --- export_async_schedule/__init__.py | 3 + export_async_schedule/__manifest__.py | 24 +++ export_async_schedule/data/ir_cron.xml | 17 ++ export_async_schedule/models/__init__.py | 3 + .../models/export_async_schedule.py | 140 ++++++++++++++ export_async_schedule/readme/CONFIGURE.rst | 15 ++ export_async_schedule/readme/CONTRIBUTORS.rst | 1 + export_async_schedule/readme/DESCRIPTION.rst | 3 + export_async_schedule/readme/ROADMAP.rst | 1 + export_async_schedule/readme/USAGE.rst | 6 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 9455 bytes export_async_schedule/tests/__init__.py | 1 + .../tests/test_export_async_schedule.py | 180 ++++++++++++++++++ .../views/export_async_schedule_views.xml | 93 +++++++++ 15 files changed, 489 insertions(+) create mode 100644 export_async_schedule/__init__.py create mode 100644 export_async_schedule/__manifest__.py create mode 100644 export_async_schedule/data/ir_cron.xml create mode 100644 export_async_schedule/models/__init__.py create mode 100644 export_async_schedule/models/export_async_schedule.py create mode 100644 export_async_schedule/readme/CONFIGURE.rst create mode 100644 export_async_schedule/readme/CONTRIBUTORS.rst create mode 100644 export_async_schedule/readme/DESCRIPTION.rst create mode 100644 export_async_schedule/readme/ROADMAP.rst create mode 100644 export_async_schedule/readme/USAGE.rst create mode 100644 export_async_schedule/security/ir.model.access.csv create mode 100644 export_async_schedule/static/description/icon.png create mode 100644 export_async_schedule/tests/__init__.py create mode 100644 export_async_schedule/tests/test_export_async_schedule.py create mode 100644 export_async_schedule/views/export_async_schedule_views.xml diff --git a/export_async_schedule/__init__.py b/export_async_schedule/__init__.py new file mode 100644 index 0000000000..31660d6a96 --- /dev/null +++ b/export_async_schedule/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/export_async_schedule/__manifest__.py b/export_async_schedule/__manifest__.py new file mode 100644 index 0000000000..e23a602ac3 --- /dev/null +++ b/export_async_schedule/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2019 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + 'name': 'Scheduled Asynchronous Export', + 'summary': 'Generate and send exports by emails on a schedule', + 'version': '12.0.1.0.0', + 'author': 'Camptocamp, Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://github.com/OCA/queue', + 'category': 'Generic Modules', + 'depends': [ + 'base_export_async', + 'queue_job', + ], + 'data': [ + 'data/ir_cron.xml', + 'views/export_async_schedule_views.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'maintainers': ['guewen'], + 'development_status': 'Beta', +} diff --git a/export_async_schedule/data/ir_cron.xml b/export_async_schedule/data/ir_cron.xml new file mode 100644 index 0000000000..171480ee44 --- /dev/null +++ b/export_async_schedule/data/ir_cron.xml @@ -0,0 +1,17 @@ + + + + + Send Scheduled Exports + + + + 1 + hours + -1 + + code + model.search([('next_execution', '<=', datetime.datetime.now())]).run_schedule() + + + diff --git a/export_async_schedule/models/__init__.py b/export_async_schedule/models/__init__.py new file mode 100644 index 0000000000..cb2b01369e --- /dev/null +++ b/export_async_schedule/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import export_async_schedule diff --git a/export_async_schedule/models/export_async_schedule.py b/export_async_schedule/models/export_async_schedule.py new file mode 100644 index 0000000000..6737910687 --- /dev/null +++ b/export_async_schedule/models/export_async_schedule.py @@ -0,0 +1,140 @@ +# Copyright 2019 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.tools.safe_eval import safe_eval +from odoo.addons.base.models.res_partner import _lang_get + + +class ExportAsyncSchedule(models.Model): + _name = "export.async.schedule" + _description = "Export Async Schedule" + + active = fields.Boolean(default=True) + + # Export configuration + model_id = fields.Many2one( + comodel_name="ir.model", required=True, ondelete="cascade" + ) + model_name = fields.Char(related="model_id.model", string="Model Name") + user_ids = fields.Many2many( + string="Recipients", comodel_name="res.users", required=True + ) + domain = fields.Char(string="Export Domain", default=[]) + ir_export_id = fields.Many2one( + comodel_name="ir.exports", + string="Export List", + required=True, + domain="[('resource', '=', model_name)]", + ondelete="restrict", + ) + export_format = fields.Selection( + selection=[("csv", "CSV"), ("excel", "Excel")], + default="csv", + required=True, + ) + import_compat = fields.Boolean(string="Import-compatible Export") + lang = fields.Selection( + _lang_get, + string="Language", + default=lambda self: self.env.lang, + help="Exports will be translated in this language.", + ) + + # Scheduling + next_execution = fields.Datetime( + default=fields.Datetime.now, required=True + ) + interval = fields.Integer(default=1, required=True) + interval_unit = fields.Selection( + selection=[ + ("hours", "Hour(s)"), + ("days", "Day(s)"), + ("weeks", "Week(s)"), + ("months", "Month(s)"), + ], + string="Unit", + default="months", + required=True, + ) + + def name_get(self): + result = [] + for record in self: + name = "{}: {}".format( + record.model_id.name, record.ir_export_id.name + ) + result.append((record.id, name)) + return result + + def run_schedule(self): + for record in self: + if record.next_execution > datetime.now(): + continue + record.action_export() + record.next_execution = record._compute_next_date() + + def _compute_next_date(self): + args = {self.interval_unit: self.interval} + return self.next_execution + relativedelta(**args) + + @api.model + def _get_fields_with_labels(self, model_name, export_fields): + self_fields = self.env[model_name]._fields + result = [] + for field_name in export_fields: + if "/" in field_name: + # The ir.exports.line model contains only the name of the + # field, and when we follow relations, the name of the fields + # joined by /. example: 'bank_ids/acc_number' + # Here, we follow the relations to get the labels + parts = field_name.split("/") + model_fields = self_fields + label_parts = [] + for cur_field_name in parts: + cur_field = model_fields[cur_field_name] + label_parts.append(cur_field._description_string(self.env)) + comodel_name = cur_field.comodel_name + if comodel_name: + model_fields = self.env[cur_field.comodel_name]._fields + label = "/".join(label_parts) + else: + label = self_fields[field_name]._description_string(self.env) + result.append({"label": label, "name": field_name}) + return result + + def _prepare_export_params(self): + export_fields = [ + export_field.name + for export_field in self.ir_export_id.export_fields + ] + if self.import_compat: + export_fields = [ + {"label": export_field, "name": export_field} + for export_field in export_fields + ] + else: + export_fields = self._get_fields_with_labels( + self.model_name, + [export_field for export_field in export_fields], + ) + + return { + "format": self.export_format, + "model": self.model_name, + "fields": export_fields, + "ids": False, + "domain": safe_eval(self.domain), + "context": self.env.context, + "import_compat": self.import_compat, + "user_ids": self.user_ids.ids, + } + + def action_export(self): + for record in self: + record = record.with_context(lang=record.lang) + params = record._prepare_export_params() + record.env["delay.export"].with_delay().export(params) diff --git a/export_async_schedule/readme/CONFIGURE.rst b/export_async_schedule/readme/CONFIGURE.rst new file mode 100644 index 0000000000..8ecdc16397 --- /dev/null +++ b/export_async_schedule/readme/CONFIGURE.rst @@ -0,0 +1,15 @@ +The configuration of a scheduled export is based on export lists. + +To create an export list: + +* open the list view of the model to export +* select at least one record, and open "Action → Export" +* select the fields to export and save using "Save fields list". + +To configure a scheduled export: + +* open "Settings → Technical → Automation → Scheduled Exports" +* create a scheduled export by filling the form + +A Scheduled Action named "Send Scheduled Exports" checks every hour +if Scheduled Exports have to be executed. diff --git a/export_async_schedule/readme/CONTRIBUTORS.rst b/export_async_schedule/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..446e16bcaf --- /dev/null +++ b/export_async_schedule/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Guewen Baconnier (Camptocamp) diff --git a/export_async_schedule/readme/DESCRIPTION.rst b/export_async_schedule/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..8eadbd00cb --- /dev/null +++ b/export_async_schedule/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +Add a new Automation feature: Scheduled Exports. +Based on an export list and a domain, an email is sent every X +hours/days/weeks/months to a selection of users. diff --git a/export_async_schedule/readme/ROADMAP.rst b/export_async_schedule/readme/ROADMAP.rst new file mode 100644 index 0000000000..2b43d3086e --- /dev/null +++ b/export_async_schedule/readme/ROADMAP.rst @@ -0,0 +1 @@ +* We could configure a custom TTL (time-to-live) for each scheduled export diff --git a/export_async_schedule/readme/USAGE.rst b/export_async_schedule/readme/USAGE.rst new file mode 100644 index 0000000000..f3a75213c1 --- /dev/null +++ b/export_async_schedule/readme/USAGE.rst @@ -0,0 +1,6 @@ +When the configuration of a Scheduled Export is done, their execution +is automatic. + +Users will receive an email containing a link to download the exported file at +the specified frequency. The attachments stay in the database for 7 days by +default (it can be changed with the system parameter ``attachment.ttl``. diff --git a/export_async_schedule/security/ir.model.access.csv b/export_async_schedule/security/ir.model.access.csv new file mode 100644 index 0000000000..4e3a697a18 --- /dev/null +++ b/export_async_schedule/security/ir.model.access.csv @@ -0,0 +1,2 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"access_export_async_schedule","export_async_schedule","model_export_async_schedule","base.group_system",1,1,1,1 diff --git a/export_async_schedule/static/description/icon.png b/export_async_schedule/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/export_async_schedule/tests/__init__.py b/export_async_schedule/tests/__init__.py new file mode 100644 index 0000000000..69f905f9cf --- /dev/null +++ b/export_async_schedule/tests/__init__.py @@ -0,0 +1 @@ +from . import test_export_async_schedule diff --git a/export_async_schedule/tests/test_export_async_schedule.py b/export_async_schedule/tests/test_export_async_schedule.py new file mode 100644 index 0000000000..8cfb507b75 --- /dev/null +++ b/export_async_schedule/tests/test_export_async_schedule.py @@ -0,0 +1,180 @@ +# Copyright 2019 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from odoo.tests import common +from odoo.addons.queue_job.tests.common import mock_with_delay + + +class TestExportAsyncSchedule(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.Schedule = cls.env["export.async.schedule"] + cls._create_schedule() + + @classmethod + def _create_schedule(cls): + cls.ir_export = cls.env["ir.exports"].create( + { + "name": "test", + "resource": "res.partner", + "export_fields": [ + (0, 0, {"name": "display_name"}), + (0, 0, {"name": "email"}), + (0, 0, {"name": "phone"}), + (0, 0, {"name": "title/shortcut"}), + ], + } + ) + model = cls.env["ir.model"].search([("model", "=", "res.partner")]) + user = cls.env.ref("base.user_admin") + cls.schedule = cls.Schedule.create( + { + "model_id": model.id, + "user_ids": [(4, user.id)], + "domain": '[("is_company", "=", True)]', + "ir_export_id": cls.ir_export.id, + "export_format": "csv", + "import_compat": True, + "lang": "en_US", + } + ) + + def test_fields_with_labels(self): + export_fields = [ + "display_name", + "email", + "phone", + "title/shortcut", + "parent_id/customer", + "parent_id/company_id/name", + ] + result = self.env["export.async.schedule"]._get_fields_with_labels( + "res.partner", export_fields + ) + expected = [ + {"label": "Display Name", "name": "display_name"}, + {"label": "Email", "name": "email"}, + {"label": "Phone", "name": "phone"}, + {"label": "Title/Abbreviation", "name": "title/shortcut"}, + { + "label": "Related Company/Is a Customer", + "name": "parent_id/customer", + }, + { + "label": "Related Company/Company/Company Name", + "name": "parent_id/company_id/name", + }, + ] + self.assertEqual(result, expected) + + def test_prepare_export_params_compatible(self): + prepared = self.schedule._prepare_export_params() + expected = { + "context": {}, + "domain": [("is_company", "=", True)], + # in 'import compatible' mode, the header (label) + # is equal to the field name + "fields": [ + {"label": "display_name", "name": "display_name"}, + {"label": "email", "name": "email"}, + {"label": "phone", "name": "phone"}, + {"label": "title/shortcut", "name": "title/shortcut"}, + ], + "format": "csv", + "ids": False, + "import_compat": True, + "model": "res.partner", + "user_ids": [self.env.ref("base.user_admin").id], + } + self.assertDictEqual(prepared, expected) + + def test_prepare_export_params_friendly(self): + self.schedule.import_compat = False + prepared = self.schedule._prepare_export_params() + expected = { + "context": {}, + "domain": [("is_company", "=", True)], + # in 'import compatible' mode, the header (label) + # is equal to the field name + "fields": [ + {"label": "Display Name", "name": "display_name"}, + {"label": "Email", "name": "email"}, + {"label": "Phone", "name": "phone"}, + {"label": "Title/Abbreviation", "name": "title/shortcut"}, + ], + "format": "csv", + "ids": False, + "import_compat": False, + "model": "res.partner", + "user_ids": [self.env.ref("base.user_admin").id], + } + self.assertDictEqual(prepared, expected) + + def test_schedule_next_date(self): + start_date = datetime(2019, 11, 23, 12, 0, 0) + + def assert_next_schedule(interval, unit, expected): + self.schedule.next_execution = start_date + self.schedule.interval = interval + self.schedule.interval_unit = unit + + self.assertEqual(self.schedule._compute_next_date(), expected) + + assert_next_schedule(1, "hours", start_date + relativedelta(hours=1)) + assert_next_schedule(2, "hours", start_date + relativedelta(hours=2)) + assert_next_schedule(1, "days", start_date + relativedelta(days=1)) + assert_next_schedule(2, "days", start_date + relativedelta(days=2)) + assert_next_schedule(1, "weeks", start_date + relativedelta(weeks=1)) + assert_next_schedule(2, "weeks", start_date + relativedelta(weeks=2)) + assert_next_schedule(1, "months", start_date + relativedelta(months=1)) + assert_next_schedule(2, "months", start_date + relativedelta(months=2)) + + def test_run_schedule(self): + in_future = datetime.now() + relativedelta(minutes=1) + self.schedule.next_execution = in_future + self.schedule.run_schedule() + # nothing happened because we have not reached the next execution + self.assertEqual(self.schedule.next_execution, in_future) + + in_past = datetime.now() - relativedelta(minutes=1) + self.schedule.next_execution = in_past + self.schedule.run_schedule() + # it has been executed and the date changed to the next execution + self.assertGreater(self.schedule.next_execution, in_past) + + def test_delay_job(self): + with mock_with_delay() as (delayable_cls, delayable): + self.schedule.action_export() + + # check 'with_delay()' part: + self.assertEqual(delayable_cls.call_count, 1) + # arguments passed in 'with_delay()' + delay_args, __ = delayable_cls.call_args + self.assertEqual((self.env["delay.export"],), delay_args) + + # check what's passed to the job method 'export' + self.assertEqual(delayable.export.call_count, 1) + delay_args, delay_kwargs = delayable.export.call_args + expected_params = ( + { + "context": {"lang": "en_US"}, + "domain": [("is_company", "=", True)], + "fields": [ + {"label": "display_name", "name": "display_name"}, + {"label": "email", "name": "email"}, + {"label": "phone", "name": "phone"}, + {"label": "title/shortcut", "name": "title/shortcut"}, + ], + "format": "csv", + "ids": False, + "import_compat": True, + "model": "res.partner", + "user_ids": [2], + }, + ) + + self.assertEqual(delay_args, expected_params) diff --git a/export_async_schedule/views/export_async_schedule_views.xml b/export_async_schedule/views/export_async_schedule_views.xml new file mode 100644 index 0000000000..dbb293638a --- /dev/null +++ b/export_async_schedule/views/export_async_schedule_views.xml @@ -0,0 +1,93 @@ + + + + + export.async.schedule.tree + export.async.schedule + + + + + + + + + + + + + export.async.schedule.form + export.async.schedule + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + export.async.schedule.search + export.async.schedule + + + + + + + + + + + + + + + + + + + + Scheduled Exports + ir.actions.act_window + export.async.schedule + form + Schedule Exports to send by email + + + + +
From 466a5dfb419d757d68c228723efa3a18de478490 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 22 Nov 2019 13:36:17 +0100 Subject: [PATCH 02/23] [UPD] README.rst --- export_async_schedule/README.rst | 117 +++++ .../static/description/index.html | 457 ++++++++++++++++++ 2 files changed, 574 insertions(+) create mode 100644 export_async_schedule/README.rst create mode 100644 export_async_schedule/static/description/index.html diff --git a/export_async_schedule/README.rst b/export_async_schedule/README.rst new file mode 100644 index 0000000000..bcb9420462 --- /dev/null +++ b/export_async_schedule/README.rst @@ -0,0 +1,117 @@ +============================= +Scheduled Asynchronous Export +============================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fqueue-lightgray.png?logo=github + :target: https://github.com/OCA/queue/tree/12.0/export_async_schedule + :alt: OCA/queue +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/queue-12-0/queue-12-0-export_async_schedule + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/230/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add a new Automation feature: Scheduled Exports. +Based on an export list and a domain, an email is sent every X +hours/days/weeks/months to a selection of users. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +The configuration of a scheduled export is based on export lists. + +To create an export list: + +* open the list view of the model to export +* select at least one record, and open "Action → Export" +* select the fields to export and save using "Save fields list". + +To configure a scheduled export: + +* open "Settings → Technical → Automation → Scheduled Exports" +* create a scheduled export by filling the form + +A Scheduled Action named "Send Scheduled Exports" checks every hour +if Scheduled Exports have to be executed. + +Usage +===== + +When the configuration of a Scheduled Export is done, their execution +is automatic. + +Users will receive an email containing a link to download the exported file at +the specified frequency. The attachments stay in the database for 7 days by +default (it can be changed with the system parameter ``attachment.ttl``. + +Known issues / Roadmap +====================== + +* We could configure a custom TTL (time-to-live) for each scheduled export + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier (Camptocamp) + +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. + +.. |maintainer-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen + +Current `maintainer `__: + +|maintainer-guewen| + +This module is part of the `OCA/queue `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/export_async_schedule/static/description/index.html b/export_async_schedule/static/description/index.html new file mode 100644 index 0000000000..caa67a6395 --- /dev/null +++ b/export_async_schedule/static/description/index.html @@ -0,0 +1,457 @@ + + + + + + +Scheduled Asynchronous Export + + + +
+

Scheduled Asynchronous Export

+ + +

Beta License: AGPL-3 OCA/queue Translate me on Weblate Try me on Runbot

+

Add a new Automation feature: Scheduled Exports. +Based on an export list and a domain, an email is sent every X +hours/days/weeks/months to a selection of users.

+

Table of contents

+ +
+

Configuration

+

The configuration of a scheduled export is based on export lists.

+

To create an export list:

+
    +
  • open the list view of the model to export
  • +
  • select at least one record, and open “Action → Export”
  • +
  • select the fields to export and save using “Save fields list”.
  • +
+

To configure a scheduled export:

+
    +
  • open “Settings → Technical → Automation → Scheduled Exports”
  • +
  • create a scheduled export by filling the form
  • +
+

A Scheduled Action named “Send Scheduled Exports” checks every hour +if Scheduled Exports have to be executed.

+
+
+

Usage

+

When the configuration of a Scheduled Export is done, their execution +is automatic.

+

Users will receive an email containing a link to download the exported file at +the specified frequency. The attachments stay in the database for 7 days by +default (it can be changed with the system parameter attachment.ttl.

+
+
+

Known issues / Roadmap

+
    +
  • We could configure a custom TTL (time-to-live) for each scheduled export
  • +
+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+
    +
  • Guewen Baconnier (Camptocamp)
  • +
+
+
+

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.

+

Current maintainer:

+

guewen

+

This module is part of the OCA/queue project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From d406458c8d8c0f85f6d39c0a76b530ef14ba08eb Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 22 Nov 2019 17:40:15 +0100 Subject: [PATCH 03/23] Add end of month option for the schedules --- .../models/export_async_schedule.py | 12 ++++++++++++ .../tests/test_export_async_schedule.py | 14 ++++++++++++++ .../views/export_async_schedule_views.xml | 3 +++ 3 files changed, 29 insertions(+) diff --git a/export_async_schedule/models/export_async_schedule.py b/export_async_schedule/models/export_async_schedule.py index 6737910687..f4ad93d790 100644 --- a/export_async_schedule/models/export_async_schedule.py +++ b/export_async_schedule/models/export_async_schedule.py @@ -60,6 +60,7 @@ class ExportAsyncSchedule(models.Model): default="months", required=True, ) + end_of_month = fields.Boolean() def name_get(self): result = [] @@ -79,8 +80,19 @@ def run_schedule(self): def _compute_next_date(self): args = {self.interval_unit: self.interval} + if self.interval_unit == "months" and self.end_of_month: + # dateutil knows how to deal with variable days of months, + # it will put the latest possible day + args.update({"day": 31, "hour": 23, "minute": 59, "second": 59}) return self.next_execution + relativedelta(**args) + @api.onchange("end_of_month") + def onchange_end_of_month(self): + if self.end_of_month: + self.next_execution = self.next_execution + relativedelta( + day=31, hour=23, minute=59, second=59 + ) + @api.model def _get_fields_with_labels(self, model_name, export_fields): self_fields = self.env[model_name]._fields diff --git a/export_async_schedule/tests/test_export_async_schedule.py b/export_async_schedule/tests/test_export_async_schedule.py index 8cfb507b75..1bd6b38a00 100644 --- a/export_async_schedule/tests/test_export_async_schedule.py +++ b/export_async_schedule/tests/test_export_async_schedule.py @@ -133,6 +133,20 @@ def assert_next_schedule(interval, unit, expected): assert_next_schedule(1, "months", start_date + relativedelta(months=1)) assert_next_schedule(2, "months", start_date + relativedelta(months=2)) + self.schedule.end_of_month = True + assert_next_schedule( + 1, + "months", + start_date + + relativedelta(months=1, day=31, hour=23, minute=59, second=59), + ) + assert_next_schedule( + 2, + "months", + start_date + + relativedelta(months=2, day=31, hour=23, minute=59, second=59), + ) + def test_run_schedule(self): in_future = datetime.now() + relativedelta(minutes=1) self.schedule.next_execution = in_future diff --git a/export_async_schedule/views/export_async_schedule_views.xml b/export_async_schedule/views/export_async_schedule_views.xml index dbb293638a..34e4f4146a 100644 --- a/export_async_schedule/views/export_async_schedule_views.xml +++ b/export_async_schedule/views/export_async_schedule_views.xml @@ -50,6 +50,9 @@ + From 35039466381a74780097a21616b2b992fb8b0607 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 11 Jun 2020 19:00:51 +0000 Subject: [PATCH 04/23] [UPD] Update export_async_schedule.pot --- .../i18n/export_async_schedule.pot | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 export_async_schedule/i18n/export_async_schedule.pot diff --git a/export_async_schedule/i18n/export_async_schedule.pot b/export_async_schedule/i18n/export_async_schedule.pot new file mode 100644 index 0000000000..5167dc57cd --- /dev/null +++ b/export_async_schedule/i18n/export_async_schedule.pot @@ -0,0 +1,202 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * export_async_schedule +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__active +msgid "Active" +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_search +msgid "All" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,export_format:0 +msgid "CSV" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__create_uid +msgid "Created by" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__create_date +msgid "Created on" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,interval_unit:0 +msgid "Day(s)" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__display_name +msgid "Display Name" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__end_of_month +msgid "End Of Month" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,export_format:0 +msgid "Excel" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model,name:export_async_schedule.model_export_async_schedule +msgid "Export Async Schedule" +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_form +msgid "Export Configuration" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__domain +msgid "Export Domain" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__export_format +msgid "Export Format" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__ir_export_id +msgid "Export List" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,help:export_async_schedule.field_export_async_schedule__lang +msgid "Exports will be translated in this language." +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_search +msgid "Group By" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,interval_unit:0 +msgid "Hour(s)" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__id +msgid "ID" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__import_compat +msgid "Import-compatible Export" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__interval +msgid "Interval" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__lang +msgid "Language" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule____last_update +msgid "Last Modified on" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__write_date +msgid "Last Updated on" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__model_id +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_search +msgid "Model" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__model_name +msgid "Model Name" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,interval_unit:0 +msgid "Month(s)" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__next_execution +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_search +msgid "Next Execution" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__user_ids +msgid "Recipients" +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.actions.act_window,help:export_async_schedule.action_export_async_schedule +msgid "Schedule Exports to send by email" +msgstr "" + +#. module: export_async_schedule +#: model:ir.actions.act_window,name:export_async_schedule.action_export_async_schedule +#: model:ir.ui.menu,name:export_async_schedule.menu_export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_search +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_tree +msgid "Scheduled Exports" +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_form +msgid "Scheduling" +msgstr "" + +#. module: export_async_schedule +#: model:ir.actions.server,name:export_async_schedule.ir_cron_export_async_schedule_ir_actions_server +#: model:ir.cron,cron_name:export_async_schedule.ir_cron_export_async_schedule +#: model:ir.cron,name:export_async_schedule.ir_cron_export_async_schedule +msgid "Send Scheduled Exports" +msgstr "" + +#. module: export_async_schedule +#: model_terms:ir.ui.view,arch_db:export_async_schedule.view_export_async_schedule_form +msgid "Test Export" +msgstr "" + +#. module: export_async_schedule +#: model:ir.model.fields,field_description:export_async_schedule.field_export_async_schedule__interval_unit +msgid "Unit" +msgstr "" + +#. module: export_async_schedule +#: selection:export.async.schedule,interval_unit:0 +msgid "Week(s)" +msgstr "" + From 49e860fee686e3edb27bd7ceddd712cbe7c8006f Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 11 Jun 2020 19:22:27 +0000 Subject: [PATCH 05/23] [UPD] README.rst --- export_async_schedule/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/export_async_schedule/static/description/index.html b/export_async_schedule/static/description/index.html index caa67a6395..8dd8ef6787 100644 --- a/export_async_schedule/static/description/index.html +++ b/export_async_schedule/static/description/index.html @@ -3,7 +3,7 @@ - + Scheduled Asynchronous Export -
-

Scheduled Asynchronous Export

+
+ + +Odoo Community Association + +
+

Scheduled Asynchronous Export

-

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

+

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

Add a new Automation feature: Scheduled Exports. Based on an export list and a domain, an email is sent every X hours/days/weeks/months to a selection of users.

@@ -390,7 +395,7 @@

Scheduled Asynchronous Export

-

Configuration

+

Configuration

The configuration of a scheduled export is based on export lists.

To create an export list:

    @@ -407,7 +412,7 @@

    Configuration

    Scheduled Exports have to be executed.

-

Usage

+

Usage

When the configuration of a Scheduled Export is done, their execution is automatic.

Users will receive an email containing a link to download the exported @@ -416,31 +421,31 @@

Usage

attachment.ttl.

-

Known issues / Roadmap

+

Known issues / Roadmap

  • We could configure a custom TTL (time-to-live) for each scheduled export
-

Bug Tracker

+

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.

+feedback.

Do not contact contributors directly about support or help with technical issues.

-

Credits

+

Credits

-

Authors

+

Authors

  • Camptocamp
  • ACSONE SA/NV
-

Contributors

+

Contributors

-

Other credits

+

Other credits

The migration of this module from 14.0 to 17.0 was financially supported by:

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -475,10 +480,11 @@

Maintainers

promote its widespread use.

Current maintainer:

guewen

-

This module is part of the OCA/queue project on GitHub.

+

This module is part of the OCA/queue project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
diff --git a/export_async_schedule/views/export_async_schedule_views.xml b/export_async_schedule/views/export_async_schedule_views.xml index 3fffd23940..09db539306 100644 --- a/export_async_schedule/views/export_async_schedule_views.xml +++ b/export_async_schedule/views/export_async_schedule_views.xml @@ -1,120 +1,123 @@ - - export.async.schedule.tree - export.async.schedule - - - - - - - - - - + + export.async.schedule.tree + export.async.schedule + + + + + + + + + + - - export.async.schedule.form - export.async.schedule - -
-
-
-
- -
- - - + + + + - - + - - + - - - - - - - - - - - - - -
+ + + + + + + + + + + + + - - export.async.schedule.search - export.async.schedule - - - - - - - - - - + export.async.schedule.search + export.async.schedule + + + + + + + + + + - - + - - - - - + + + + - - Scheduled Exports - ir.actions.act_window - export.async.schedule - Schedule Exports to send by email - + + Scheduled Exports + ir.actions.act_window + export.async.schedule + Schedule Exports to send by email + - Date: Tue, 25 Aug 2026 12:50:12 +0200 Subject: [PATCH 23/23] [MIG] export_async_schedule: Migration to 18.0 --- export_async_schedule/__manifest__.py | 2 +- export_async_schedule/data/ir_cron.xml | 2 -- export_async_schedule/models/export_async_schedule.py | 2 +- export_async_schedule/views/export_async_schedule_views.xml | 4 ++-- test-requirements.txt | 1 + 5 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 test-requirements.txt diff --git a/export_async_schedule/__manifest__.py b/export_async_schedule/__manifest__.py index c14ba8a5c3..639d6df7b0 100644 --- a/export_async_schedule/__manifest__.py +++ b/export_async_schedule/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Scheduled Asynchronous Export", "summary": "Generate and send exports by emails on a schedule", - "version": "17.0.1.0.0", + "version": "18.0.1.0.0", "author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)", "license": "AGPL-3", "website": "https://github.com/OCA/queue", diff --git a/export_async_schedule/data/ir_cron.xml b/export_async_schedule/data/ir_cron.xml index 4a442bd26b..7b738aea97 100644 --- a/export_async_schedule/data/ir_cron.xml +++ b/export_async_schedule/data/ir_cron.xml @@ -7,8 +7,6 @@ 1 hours - -1 - code export.async.schedule.tree export.async.schedule - + - + diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000000..b1466d33b6 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +odoo-addon-base_export_async @ git+https://github.com/OCA/queue.git@refs/pull/849/head#subdirectory=base_export_async