From 791e97f3220427f6b0d888bc8d80e1161345980e Mon Sep 17 00:00:00 2001 From: eLBati Date: Wed, 28 Aug 2019 15:00:27 +0200 Subject: [PATCH 01/43] IMP dbfilter_from_header allowing to use hostname (%h) or the first subdomain (%d) through which the system is being accessed, same feature as odoo itself. This can be useful when you have a virtualhost with "server_name *.domain.com" --- dbfilter_from_header/override.py | 9 +++++++++ dbfilter_from_header/readme/CONFIGURE.rst | 2 ++ dbfilter_from_header/readme/CONTRIBUTORS.rst | 1 + 3 files changed, 12 insertions(+) diff --git a/dbfilter_from_header/override.py b/dbfilter_from_header/override.py index 113acb6a004..069f9f07dd0 100644 --- a/dbfilter_from_header/override.py +++ b/dbfilter_from_header/override.py @@ -15,6 +15,15 @@ def db_filter(dbs, httprequest=None): dbs = db_filter_org(dbs, httprequest) httprequest = httprequest or http.request.httprequest db_filter_hdr = httprequest.environ.get('HTTP_X_ODOO_DBFILTER') + + # copied from original db_filter function, to support '%h' and '%d' + h = httprequest.environ.get('HTTP_HOST', '').split(':')[0] + d, _, r = h.partition('.') + if d == "www" and r: + d = r.partition('.')[0] + d, h = re.escape(d), re.escape(h) + db_filter_hdr = db_filter_hdr.replace('%h', h).replace('%d', d) + if db_filter_hdr: dbs = [db for db in dbs if re.match(db_filter_hdr, db)] return dbs diff --git a/dbfilter_from_header/readme/CONFIGURE.rst b/dbfilter_from_header/readme/CONFIGURE.rst index 11da21c49b4..2a61588ce2c 100644 --- a/dbfilter_from_header/readme/CONFIGURE.rst +++ b/dbfilter_from_header/readme/CONFIGURE.rst @@ -16,3 +16,5 @@ applied before looking at the regular expression in the header. And make sure that proxy mode is enabled in Odoo's configuration file: ``proxy_mode = True`` + +Your filter regex can contain dynamically injected hostname (%h) or the first subdomain (%d) through which the system is being accessed diff --git a/dbfilter_from_header/readme/CONTRIBUTORS.rst b/dbfilter_from_header/readme/CONTRIBUTORS.rst index 3ce1edb3a7c..0f02eb682d5 100644 --- a/dbfilter_from_header/readme/CONTRIBUTORS.rst +++ b/dbfilter_from_header/readme/CONTRIBUTORS.rst @@ -7,3 +7,4 @@ * Fabio Vilchez * Jos De Graeve * Lai Tim Siu (Quaritle Limited) +* Lorenzo Battistini From ca1728a951f5d7076b1c38db9c417fcd1db93d9e Mon Sep 17 00:00:00 2001 From: Dariusz Kubiak Date: Wed, 15 Dec 2021 20:25:17 +0100 Subject: [PATCH 02/43] [FIX] base_changeset: fix write return type --- base_changeset/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_changeset/models/base.py b/base_changeset/models/base.py index 907aedf5534..f32df7f4d4f 100644 --- a/base_changeset/models/base.py +++ b/base_changeset/models/base.py @@ -114,7 +114,7 @@ def write(self, values): for record in self: local_values = self.env["record.changeset"].add_changeset(record, values) super(Base, record).write(local_values) - return self + return True def _changeset_disabled(self): if self.env.context.get("__no_changeset") == disable_changeset: From 7df67b1d188cf192702218b4da777288a947b483 Mon Sep 17 00:00:00 2001 From: Sylvain LE GAL Date: Fri, 21 Sep 2018 10:55:50 +0200 Subject: [PATCH 03/43] [ADD] new module cron_inactivity_period (#1346) --- cron_inactivity_period/README.rst | 70 +++++++++++++ cron_inactivity_period/__init__.py | 2 + cron_inactivity_period/__openerp__.py | 26 +++++ cron_inactivity_period/demo/res_groups.xml | 13 +++ cron_inactivity_period/i18n/fr.po | 95 ++++++++++++++++++ cron_inactivity_period/models/__init__.py | 3 + cron_inactivity_period/models/ir_cron.py | 30 ++++++ .../models/ir_cron_inactivity_period.py | 63 ++++++++++++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/ir_cron_form.png | Bin 0 -> 26332 bytes cron_inactivity_period/tests/__init__.py | 2 + cron_inactivity_period/tests/test_module.py | 44 ++++++++ cron_inactivity_period/views/ir_cron.xml | 28 ++++++ 14 files changed, 379 insertions(+) create mode 100644 cron_inactivity_period/README.rst create mode 100644 cron_inactivity_period/__init__.py create mode 100644 cron_inactivity_period/__openerp__.py create mode 100644 cron_inactivity_period/demo/res_groups.xml create mode 100644 cron_inactivity_period/i18n/fr.po create mode 100644 cron_inactivity_period/models/__init__.py create mode 100644 cron_inactivity_period/models/ir_cron.py create mode 100644 cron_inactivity_period/models/ir_cron_inactivity_period.py create mode 100644 cron_inactivity_period/security/ir.model.access.csv create mode 100644 cron_inactivity_period/static/description/icon.png create mode 100644 cron_inactivity_period/static/description/ir_cron_form.png create mode 100644 cron_inactivity_period/tests/__init__.py create mode 100644 cron_inactivity_period/tests/test_module.py create mode 100644 cron_inactivity_period/views/ir_cron.xml diff --git a/cron_inactivity_period/README.rst b/cron_inactivity_period/README.rst new file mode 100644 index 00000000000..a15ebc3de0b --- /dev/null +++ b/cron_inactivity_period/README.rst @@ -0,0 +1,70 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +======================== +Cron - Inactivity Period +======================== + + +This module allows you to disable cron Jobs during periods. +It can be usefull if you want to disable your cron jobs during maintenance +period, or for other reasons. + +Note +---- + +If you have installed ``cron_run_manually`` module, it is still possible to run +your job, during inactivity periods. + +Configuration +============= + +To configure this module, you need to: + +#. Go to Settings > Technical > Automation > Scheduled Actions and select a + cron +#. Add new option inactivity periods + +.. figure:: https://raw.githubusercontent.com/OCA/server-tools/8.0/cron_inactivity_period/static/description/ir_cron_form.png + :alt: Inactivity Period Settings + :width: 80 % + :align: center + + +Known issues / Roadmap +====================== + +* For the time being, only one type of inactivity period is available. ('hour') + It should be great to add other options like 'week_day', to allow user to + disable cron jobs for given week days. + + +Credits +======= + +Authors +~~~~~~~ + +* GRAP, Groupement Régional Alimentaire de Proximité (http://www.grap.coop) + +Contributors +~~~~~~~~~~~~ + +* Sylvain LE GAL (https://www.twitter.com/legalsylvain) + +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. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. + diff --git a/cron_inactivity_period/__init__.py b/cron_inactivity_period/__init__.py new file mode 100644 index 00000000000..042e239ed16 --- /dev/null +++ b/cron_inactivity_period/__init__.py @@ -0,0 +1,2 @@ +# coding: utf-8 +from . import models diff --git a/cron_inactivity_period/__openerp__.py b/cron_inactivity_period/__openerp__.py new file mode 100644 index 00000000000..6d5fe9f3a84 --- /dev/null +++ b/cron_inactivity_period/__openerp__.py @@ -0,0 +1,26 @@ +# coding: utf-8 +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + 'name': 'Inactivity Periods for Cron Jobs', + 'version': '8.0.1.0.0', + 'author': "GRAP,Odoo Community Association (OCA)", + 'license': 'AGPL-3', + 'category': 'Tools', + 'depends': [ + 'base', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/ir_cron.xml', + ], + 'demo': [ + 'demo/res_groups.xml', + ], + 'images': [ + 'static/description/ir_cron_form.png', + ], + 'installable': True, +} diff --git a/cron_inactivity_period/demo/res_groups.xml b/cron_inactivity_period/demo/res_groups.xml new file mode 100644 index 00000000000..86277adc132 --- /dev/null +++ b/cron_inactivity_period/demo/res_groups.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/cron_inactivity_period/i18n/fr.po b/cron_inactivity_period/i18n/fr.po new file mode 100644 index 00000000000..fb424b5bb14 --- /dev/null +++ b/cron_inactivity_period/i18n/fr.po @@ -0,0 +1,95 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cron_inactivity_period +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-24 09:48+0000\n" +"PO-Revision-Date: 2018-08-24 09:48+0000\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: cron_inactivity_period +#: field:ir.cron.inactivity.period,inactivity_hour_begin:0 +msgid "Begin Hour" +msgstr "Heure de début" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,create_uid:0 +msgid "Created by" +msgstr "crééé par" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,create_date:0 +msgid "Created on" +msgstr "Créé le" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,cron_id:0 +msgid "Cron id" +msgstr "Cron id" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,display_name:0 +msgid "Display Name" +msgstr "Nom affiché" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,inactivity_hour_end:0 +msgid "End Hour" +msgstr "Heure de fin" + +#. module: cron_inactivity_period +#: selection:ir.cron.inactivity.period,type:0 +msgid "Hour" +msgstr "Heure" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,id:0 +msgid "ID" +msgstr "ID" + +#. module: cron_inactivity_period +#: view:ir.cron:cron_inactivity_period.view_ir_cron_form +#: field:ir.cron,inactivity_period_ids:0 +msgid "Inactivity Periods" +msgstr "Périodes d'inactivité" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,__last_update:0 +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,write_uid:0 +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,write_date:0 +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: cron_inactivity_period +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:36 +#, python-format +msgid "The End Hour should be greater than the Begin Hour" +msgstr "L'heure de fin doit être strictement supérieure à l'heure de début" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,type:0 +msgid "Type" +msgstr "Type" + +#. module: cron_inactivity_period +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:62 +#, python-format +msgid "Unimplemented Feature: Inactivity Period type '%s'" +msgstr "Fonctionnalité non implémentée : Période d'inactivité de type '%s'" + diff --git a/cron_inactivity_period/models/__init__.py b/cron_inactivity_period/models/__init__.py new file mode 100644 index 00000000000..c100f22be85 --- /dev/null +++ b/cron_inactivity_period/models/__init__.py @@ -0,0 +1,3 @@ +# coding: utf-8 +from . import ir_cron +from . import ir_cron_inactivity_period diff --git a/cron_inactivity_period/models/ir_cron.py b/cron_inactivity_period/models/ir_cron.py new file mode 100644 index 00000000000..1f86ab94dde --- /dev/null +++ b/cron_inactivity_period/models/ir_cron.py @@ -0,0 +1,30 @@ +# coding: utf-8 +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from openerp import api, fields, models + + +_logger = logging.getLogger(__name__) + + +class IrCron(models.Model): + _inherit = 'ir.cron' + + inactivity_period_ids = fields.One2many( + comodel_name='ir.cron.inactivity.period', string='Inactivity Periods', + inverse_name='cron_id') + + @api.model + def _callback(self, model_name, method_name, args, job_id): + job = self.browse(job_id) + if any(job.inactivity_period_ids._check_inactivity_period()): + _logger.info( + "Job %s skipped during inactivity period", + job.name) + return + return super(IrCron, self)._callback( + model_name, method_name, args, job_id) diff --git a/cron_inactivity_period/models/ir_cron_inactivity_period.py b/cron_inactivity_period/models/ir_cron_inactivity_period.py new file mode 100644 index 00000000000..a6a0ccbb484 --- /dev/null +++ b/cron_inactivity_period/models/ir_cron_inactivity_period.py @@ -0,0 +1,63 @@ +# coding: utf-8 +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime + +from openerp import _, api, fields, models +from openerp.exceptions import Warning as UserError + + +class IrCronInactivityPeriod(models.Model): + _name = 'ir.cron.inactivity.period' + + _SELECTION_TYPE = [ + ('hour', 'Hour'), + ] + + cron_id = fields.Many2one( + comodel_name='ir.cron', ondelete='cascade', required=True) + + type = fields.Selection( + string='Type', selection=_SELECTION_TYPE, + required=True, default='hour') + + inactivity_hour_begin = fields.Float( + string='Begin Hour', default=0) + + inactivity_hour_end = fields.Float( + string='End Hour', default=1) + + @api.constrains('inactivity_hour_begin', 'inactivity_hour_end') + def _check_activity_hour(self): + for period in self: + if period.inactivity_hour_begin >= period.inactivity_hour_end: + raise UserError(_( + "The End Hour should be greater than the Begin Hour")) + + @api.multi + def _check_inactivity_period(self): + res = [] + for period in self: + res.append(period._check_inactivity_period_one()) + return res + + @api.multi + def _check_inactivity_period_one(self): + self.ensure_one() + now = fields.Datetime.context_timestamp(self, datetime.now()) + if self.type == 'hour': + begin_inactivity = now.replace( + hour=int(self.inactivity_hour_begin), + minute=int((self.inactivity_hour_begin % 1) * 60), + second=0) + end_inactivity = now.replace( + hour=int(self.inactivity_hour_end), + minute=int((self.inactivity_hour_end % 1) * 60), + second=0) + return now >= begin_inactivity and now < end_inactivity + else: + raise UserError( + _("Unimplemented Feature: Inactivity Period type '%s'") % ( + self.type)) diff --git a/cron_inactivity_period/security/ir.model.access.csv b/cron_inactivity_period/security/ir.model.access.csv new file mode 100644 index 00000000000..d44bc01dd4f --- /dev/null +++ b/cron_inactivity_period/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_ir_cron_inactivity_period_user,access_ir_cron_inactivity_period_user,model_ir_cron_inactivity_period,base.group_user,1,,, +access_ir_cron_inactivity_period_manager,access_ir_cron_inactivity_period_manager,model_ir_cron_inactivity_period,base.group_system,1,1,1,1 diff --git a/cron_inactivity_period/static/description/icon.png b/cron_inactivity_period/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 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 z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 diff --git a/cron_inactivity_period/static/description/ir_cron_form.png b/cron_inactivity_period/static/description/ir_cron_form.png new file mode 100644 index 0000000000000000000000000000000000000000..bcc67389b70d235bf23c7da0ad8512ac4ddddfce GIT binary patch literal 26332 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�|^)Z#YNbtkq_@F_#{Uc;M}2C0HuEMk%J<$m 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 V2d)nhZlWOp=osCrz5D#_{{T>8YnuQ7 literal 0 HcmV?d00001 diff --git a/cron_inactivity_period/tests/__init__.py b/cron_inactivity_period/tests/__init__.py new file mode 100644 index 00000000000..17b82062c3d --- /dev/null +++ b/cron_inactivity_period/tests/__init__.py @@ -0,0 +1,2 @@ +# coding: utf-8 +from . import test_module diff --git a/cron_inactivity_period/tests/test_module.py b/cron_inactivity_period/tests/test_module.py new file mode 100644 index 00000000000..a54bae61cab --- /dev/null +++ b/cron_inactivity_period/tests/test_module.py @@ -0,0 +1,44 @@ +# coding: utf-8 +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.tests.common import TransactionCase + + +class TestModule(TransactionCase): + """Tests for 'Cron - Inactivity Period' Module""" + + def setUp(self): + super(TestModule, self).setUp() + self.config_obj = self.env['res.config'] + self.cron_obj = self.env['ir.cron'] + self.inactivity_period = self.env['ir.cron.inactivity.period'] + self.cron_job = self.env.ref('base.cronjob_osv_memory_autovacuum') + + # Test Section + def test_01_no_inactivity_period(self): + count = self._create_transient_model() + self.assertEqual( + count, 0, + "Calling a cron without inactivity period should run the cron") + + def test_02_no_activity_period(self): + self.inactivity_period.create({ + 'cron_id': self.cron_job.id, + 'type': 'hour', + 'inactivity_hour_begin': 0.0, + 'inactivity_hour_end': 23.59, + }) + count = self._create_transient_model() + self.assertEqual( + count, 1, + "Calling a cron with inactivity period should not run the cron") + + def _create_transient_model(self): + self.config_obj.search([]).unlink() + self.config_obj.create({}) + self.env.cr.execute("update res_config set write_date = '1970-01-01'") + self.cron_obj._callback( + 'osv_memory.autovacuum', 'power_on', (), self.cron_job.id) + return len(self.config_obj.search([])) diff --git a/cron_inactivity_period/views/ir_cron.xml b/cron_inactivity_period/views/ir_cron.xml new file mode 100644 index 00000000000..016461e4b99 --- /dev/null +++ b/cron_inactivity_period/views/ir_cron.xml @@ -0,0 +1,28 @@ + + + + + + ir.cron + + + + + + + + + + + + + + + + + + From a6ff903bd10faa46414aeee7308303a77048abba Mon Sep 17 00:00:00 2001 From: oca-travis Date: Fri, 21 Sep 2018 09:28:03 +0000 Subject: [PATCH 04/43] [UPD] Update cron_inactivity_period.pot --- .../i18n/cron_inactivity_period.pot | 93 +++++++++++++++++++ cron_inactivity_period/i18n/fr.po | 4 +- 2 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 cron_inactivity_period/i18n/cron_inactivity_period.pot diff --git a/cron_inactivity_period/i18n/cron_inactivity_period.pot b/cron_inactivity_period/i18n/cron_inactivity_period.pot new file mode 100644 index 00000000000..e23324009d7 --- /dev/null +++ b/cron_inactivity_period/i18n/cron_inactivity_period.pot @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * cron_inactivity_period +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 8.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: cron_inactivity_period +#: field:ir.cron.inactivity.period,inactivity_hour_begin:0 +msgid "Begin Hour" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,create_uid:0 +msgid "Created by" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,create_date:0 +msgid "Created on" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,cron_id:0 +msgid "Cron id" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,display_name:0 +msgid "Display Name" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,inactivity_hour_end:0 +msgid "End Hour" +msgstr "" + +#. module: cron_inactivity_period +#: selection:ir.cron.inactivity.period,type:0 +msgid "Hour" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,id:0 +msgid "ID" +msgstr "" + +#. module: cron_inactivity_period +#: view:ir.cron:cron_inactivity_period.view_ir_cron_form +#: field:ir.cron,inactivity_period_ids:0 +msgid "Inactivity Periods" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,__last_update:0 +msgid "Last Modified on" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,write_uid:0 +msgid "Last Updated by" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,write_date:0 +msgid "Last Updated on" +msgstr "" + +#. module: cron_inactivity_period +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:36 +#, python-format +msgid "The End Hour should be greater than the Begin Hour" +msgstr "" + +#. module: cron_inactivity_period +#: field:ir.cron.inactivity.period,type:0 +msgid "Type" +msgstr "" + +#. module: cron_inactivity_period +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:62 +#, python-format +msgid "Unimplemented Feature: Inactivity Period type '%s'" +msgstr "" + diff --git a/cron_inactivity_period/i18n/fr.po b/cron_inactivity_period/i18n/fr.po index fb424b5bb14..10ebab2fb72 100644 --- a/cron_inactivity_period/i18n/fr.po +++ b/cron_inactivity_period/i18n/fr.po @@ -1,6 +1,6 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * cron_inactivity_period +# * cron_inactivity_period # msgid "" msgstr "" @@ -10,6 +10,7 @@ msgstr "" "PO-Revision-Date: 2018-08-24 09:48+0000\n" "Last-Translator: <>\n" "Language-Team: \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" @@ -92,4 +93,3 @@ msgstr "Type" #, python-format msgid "Unimplemented Feature: Inactivity Period type '%s'" msgstr "Fonctionnalité non implémentée : Période d'inactivité de type '%s'" - From abbedfebe907ae4c2b0403a9be3837f3ebb4caf8 Mon Sep 17 00:00:00 2001 From: Ioan Date: Mon, 7 Nov 2022 17:27:34 +0100 Subject: [PATCH 05/43] [MIG] cron_inactivity_period: Migration to 12.0 --- cron_inactivity_period/README.rst | 70 ------------------- cron_inactivity_period/__init__.py | 1 - cron_inactivity_period/__manifest__.py | 17 +++++ cron_inactivity_period/__openerp__.py | 26 ------- cron_inactivity_period/demo/res_groups.xml | 6 +- cron_inactivity_period/models/__init__.py | 1 - cron_inactivity_period/models/ir_cron.py | 20 +++--- .../models/ir_cron_inactivity_period.py | 51 ++++++-------- cron_inactivity_period/readme/CONFIGURE.rst | 10 +++ .../readme/CONTRIBUTORS.rst | 2 + cron_inactivity_period/readme/DESCRIPTION.rst | 9 +++ cron_inactivity_period/readme/ROADMAP.rst | 3 + cron_inactivity_period/tests/__init__.py | 1 - cron_inactivity_period/tests/test_module.py | 38 +++++----- cron_inactivity_period/views/ir_cron.xml | 17 ++--- .../odoo/addons/cron_inactivity_period | 1 + setup/cron_inactivity_period/setup.py | 6 ++ 17 files changed, 110 insertions(+), 169 deletions(-) delete mode 100644 cron_inactivity_period/README.rst create mode 100644 cron_inactivity_period/__manifest__.py delete mode 100644 cron_inactivity_period/__openerp__.py create mode 100644 cron_inactivity_period/readme/CONFIGURE.rst create mode 100644 cron_inactivity_period/readme/CONTRIBUTORS.rst create mode 100644 cron_inactivity_period/readme/DESCRIPTION.rst create mode 100644 cron_inactivity_period/readme/ROADMAP.rst create mode 120000 setup/cron_inactivity_period/odoo/addons/cron_inactivity_period create mode 100644 setup/cron_inactivity_period/setup.py diff --git a/cron_inactivity_period/README.rst b/cron_inactivity_period/README.rst deleted file mode 100644 index a15ebc3de0b..00000000000 --- a/cron_inactivity_period/README.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: https://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - -======================== -Cron - Inactivity Period -======================== - - -This module allows you to disable cron Jobs during periods. -It can be usefull if you want to disable your cron jobs during maintenance -period, or for other reasons. - -Note ----- - -If you have installed ``cron_run_manually`` module, it is still possible to run -your job, during inactivity periods. - -Configuration -============= - -To configure this module, you need to: - -#. Go to Settings > Technical > Automation > Scheduled Actions and select a - cron -#. Add new option inactivity periods - -.. figure:: https://raw.githubusercontent.com/OCA/server-tools/8.0/cron_inactivity_period/static/description/ir_cron_form.png - :alt: Inactivity Period Settings - :width: 80 % - :align: center - - -Known issues / Roadmap -====================== - -* For the time being, only one type of inactivity period is available. ('hour') - It should be great to add other options like 'week_day', to allow user to - disable cron jobs for given week days. - - -Credits -======= - -Authors -~~~~~~~ - -* GRAP, Groupement Régional Alimentaire de Proximité (http://www.grap.coop) - -Contributors -~~~~~~~~~~~~ - -* Sylvain LE GAL (https://www.twitter.com/legalsylvain) - -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. - -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. - diff --git a/cron_inactivity_period/__init__.py b/cron_inactivity_period/__init__.py index 042e239ed16..0650744f6bc 100644 --- a/cron_inactivity_period/__init__.py +++ b/cron_inactivity_period/__init__.py @@ -1,2 +1 @@ -# coding: utf-8 from . import models diff --git a/cron_inactivity_period/__manifest__.py b/cron_inactivity_period/__manifest__.py new file mode 100644 index 00000000000..74165da4a09 --- /dev/null +++ b/cron_inactivity_period/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) +# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Inactivity Periods for Cron Jobs", + "version": "12.0.1.0.0", + "author": "GRAP,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/server-tools", + "license": "AGPL-3", + "category": "Tools", + "depends": ["base"], + "data": ["security/ir.model.access.csv", "views/ir_cron.xml"], + "demo": ["demo/res_groups.xml"], + "images": ["static/description/ir_cron_form.png"], + "installable": True, +} diff --git a/cron_inactivity_period/__openerp__.py b/cron_inactivity_period/__openerp__.py deleted file mode 100644 index 6d5fe9f3a84..00000000000 --- a/cron_inactivity_period/__openerp__.py +++ /dev/null @@ -1,26 +0,0 @@ -# coding: utf-8 -# Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) -# @author: Sylvain LE GAL (https://twitter.com/legalsylvain) -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - -{ - 'name': 'Inactivity Periods for Cron Jobs', - 'version': '8.0.1.0.0', - 'author': "GRAP,Odoo Community Association (OCA)", - 'license': 'AGPL-3', - 'category': 'Tools', - 'depends': [ - 'base', - ], - 'data': [ - 'security/ir.model.access.csv', - 'views/ir_cron.xml', - ], - 'demo': [ - 'demo/res_groups.xml', - ], - 'images': [ - 'static/description/ir_cron_form.png', - ], - 'installable': True, -} diff --git a/cron_inactivity_period/demo/res_groups.xml b/cron_inactivity_period/demo/res_groups.xml index 86277adc132..d5d22e492d9 100644 --- a/cron_inactivity_period/demo/res_groups.xml +++ b/cron_inactivity_period/demo/res_groups.xml @@ -4,10 +4,8 @@ Copyright (C) 2013 - Today: GRAP (http://www.grap.coop) @author: Sylvain LE GAL (https://twitter.com/legalsylvain) License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> - - + - - + diff --git a/cron_inactivity_period/models/__init__.py b/cron_inactivity_period/models/__init__.py index c100f22be85..bc0fa69635b 100644 --- a/cron_inactivity_period/models/__init__.py +++ b/cron_inactivity_period/models/__init__.py @@ -1,3 +1,2 @@ -# coding: utf-8 from . import ir_cron from . import ir_cron_inactivity_period diff --git a/cron_inactivity_period/models/ir_cron.py b/cron_inactivity_period/models/ir_cron.py index 1f86ab94dde..9fe1e36eb2a 100644 --- a/cron_inactivity_period/models/ir_cron.py +++ b/cron_inactivity_period/models/ir_cron.py @@ -1,30 +1,28 @@ -# coding: utf-8 # Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import logging -from openerp import api, fields, models +from odoo import api, fields, models _logger = logging.getLogger(__name__) class IrCron(models.Model): - _inherit = 'ir.cron' + _inherit = "ir.cron" inactivity_period_ids = fields.One2many( - comodel_name='ir.cron.inactivity.period', string='Inactivity Periods', - inverse_name='cron_id') + comodel_name="ir.cron.inactivity.period", + string="Inactivity Periods", + inverse_name="cron_id", + ) @api.model - def _callback(self, model_name, method_name, args, job_id): + def _callback(self, cron_name, server_action_id, job_id): job = self.browse(job_id) if any(job.inactivity_period_ids._check_inactivity_period()): - _logger.info( - "Job %s skipped during inactivity period", - job.name) + _logger.info("Job %s skipped during inactivity period", job.name) return - return super(IrCron, self)._callback( - model_name, method_name, args, job_id) + return super()._callback(cron_name, server_action_id, job_id) diff --git a/cron_inactivity_period/models/ir_cron_inactivity_period.py b/cron_inactivity_period/models/ir_cron_inactivity_period.py index a6a0ccbb484..e96be35f958 100644 --- a/cron_inactivity_period/models/ir_cron_inactivity_period.py +++ b/cron_inactivity_period/models/ir_cron_inactivity_period.py @@ -1,40 +1,33 @@ -# coding: utf-8 # Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from datetime import datetime -from openerp import _, api, fields, models -from openerp.exceptions import Warning as UserError +from odoo import _, api, fields, models +from odoo.exceptions import UserError class IrCronInactivityPeriod(models.Model): - _name = 'ir.cron.inactivity.period' - - _SELECTION_TYPE = [ - ('hour', 'Hour'), - ] - - cron_id = fields.Many2one( - comodel_name='ir.cron', ondelete='cascade', required=True) + _name = "ir.cron.inactivity.period" + cron_id = fields.Many2one(comodel_name="ir.cron", ondelete="cascade", required=True) type = fields.Selection( - string='Type', selection=_SELECTION_TYPE, - required=True, default='hour') - - inactivity_hour_begin = fields.Float( - string='Begin Hour', default=0) - - inactivity_hour_end = fields.Float( - string='End Hour', default=1) - - @api.constrains('inactivity_hour_begin', 'inactivity_hour_end') + string="Type", + selection=[ + ("hour", "Hour"), + ], + required=True, + default="hour", + ) + inactivity_hour_begin = fields.Float(string="Begin Hour", default=0) + inactivity_hour_end = fields.Float(string="End Hour", default=1) + + @api.constrains("inactivity_hour_begin", "inactivity_hour_end") def _check_activity_hour(self): for period in self: if period.inactivity_hour_begin >= period.inactivity_hour_end: - raise UserError(_( - "The End Hour should be greater than the Begin Hour")) + raise UserError(_("The End Hour should be greater than the Begin Hour")) @api.multi def _check_inactivity_period(self): @@ -47,17 +40,19 @@ def _check_inactivity_period(self): def _check_inactivity_period_one(self): self.ensure_one() now = fields.Datetime.context_timestamp(self, datetime.now()) - if self.type == 'hour': + if self.type == "hour": begin_inactivity = now.replace( hour=int(self.inactivity_hour_begin), minute=int((self.inactivity_hour_begin % 1) * 60), - second=0) + second=0, + ) end_inactivity = now.replace( hour=int(self.inactivity_hour_end), minute=int((self.inactivity_hour_end % 1) * 60), - second=0) + second=0, + ) return now >= begin_inactivity and now < end_inactivity else: raise UserError( - _("Unimplemented Feature: Inactivity Period type '%s'") % ( - self.type)) + _("Unimplemented Feature: Inactivity Period type '%s'") % (self.type) + ) diff --git a/cron_inactivity_period/readme/CONFIGURE.rst b/cron_inactivity_period/readme/CONFIGURE.rst new file mode 100644 index 00000000000..a330c775f0c --- /dev/null +++ b/cron_inactivity_period/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +To configure this module, you need to: + +#. Go to Settings > Technical > Automation > Scheduled Actions and select a + cron +#. Add new option inactivity periods + +.. figure:: https://raw.githubusercontent.com/OCA/server-tools/8.0/cron_inactivity_period/static/description/ir_cron_form.png + :alt: Inactivity Period Settings + :width: 80 % + :align: center diff --git a/cron_inactivity_period/readme/CONTRIBUTORS.rst b/cron_inactivity_period/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..75d65c98b1f --- /dev/null +++ b/cron_inactivity_period/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Sylvain LE GAL +* Ioan Galan diff --git a/cron_inactivity_period/readme/DESCRIPTION.rst b/cron_inactivity_period/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..7d816c5b4f7 --- /dev/null +++ b/cron_inactivity_period/readme/DESCRIPTION.rst @@ -0,0 +1,9 @@ +This module allows you to disable cron Jobs during periods. +It can be usefull if you want to disable your cron jobs during maintenance +period, or for other reasons. + +Note +---- + +If you have installed ``cron_run_manually`` module, it is still possible to run +your job, during inactivity periods. diff --git a/cron_inactivity_period/readme/ROADMAP.rst b/cron_inactivity_period/readme/ROADMAP.rst new file mode 100644 index 00000000000..699bda1cc24 --- /dev/null +++ b/cron_inactivity_period/readme/ROADMAP.rst @@ -0,0 +1,3 @@ +* For the time being, only one type of inactivity period is available. ('hour') + It should be great to add other options like 'week_day', to allow user to + disable cron jobs for given week days. diff --git a/cron_inactivity_period/tests/__init__.py b/cron_inactivity_period/tests/__init__.py index 17b82062c3d..d9b96c4fa5a 100644 --- a/cron_inactivity_period/tests/__init__.py +++ b/cron_inactivity_period/tests/__init__.py @@ -1,2 +1 @@ -# coding: utf-8 from . import test_module diff --git a/cron_inactivity_period/tests/test_module.py b/cron_inactivity_period/tests/test_module.py index a54bae61cab..2781d48ae26 100644 --- a/cron_inactivity_period/tests/test_module.py +++ b/cron_inactivity_period/tests/test_module.py @@ -1,9 +1,8 @@ -# coding: utf-8 # Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) # @author: Sylvain LE GAL (https://twitter.com/legalsylvain) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from openerp.tests.common import TransactionCase +from odoo.tests.common import TransactionCase class TestModule(TransactionCase): @@ -11,34 +10,39 @@ class TestModule(TransactionCase): def setUp(self): super(TestModule, self).setUp() - self.config_obj = self.env['res.config'] - self.cron_obj = self.env['ir.cron'] - self.inactivity_period = self.env['ir.cron.inactivity.period'] - self.cron_job = self.env.ref('base.cronjob_osv_memory_autovacuum') + self.config_obj = self.env["res.config"] + self.cron_obj = self.env["ir.cron"] + self.inactivity_period = self.env["ir.cron.inactivity.period"] + self.cron_job = self.env.ref("base.autovacuum_job") # Test Section def test_01_no_inactivity_period(self): count = self._create_transient_model() self.assertEqual( - count, 0, - "Calling a cron without inactivity period should run the cron") + count, 0, "Calling a cron without inactivity period should run the cron" + ) def test_02_no_activity_period(self): - self.inactivity_period.create({ - 'cron_id': self.cron_job.id, - 'type': 'hour', - 'inactivity_hour_begin': 0.0, - 'inactivity_hour_end': 23.59, - }) + self.inactivity_period.create( + { + "cron_id": self.cron_job.id, + "type": "hour", + "inactivity_hour_begin": 0.0, + "inactivity_hour_end": 23.59, + } + ) count = self._create_transient_model() self.assertEqual( - count, 1, - "Calling a cron with inactivity period should not run the cron") + count, 1, "Calling a cron with inactivity period should not run the cron" + ) def _create_transient_model(self): self.config_obj.search([]).unlink() self.config_obj.create({}) self.env.cr.execute("update res_config set write_date = '1970-01-01'") self.cron_obj._callback( - 'osv_memory.autovacuum', 'power_on', (), self.cron_job.id) + "osv_memory.autovacuum", + self.cron_job.ir_actions_server_id.id, + self.cron_job.id, + ) return len(self.config_obj.search([])) diff --git a/cron_inactivity_period/views/ir_cron.xml b/cron_inactivity_period/views/ir_cron.xml index 016461e4b99..7c8107c06df 100644 --- a/cron_inactivity_period/views/ir_cron.xml +++ b/cron_inactivity_period/views/ir_cron.xml @@ -4,15 +4,13 @@ Copyright (C) 2018 - Today: GRAP (http://www.grap.coop) @author: Sylvain LE GAL (https://twitter.com/legalsylvain) License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> - - + ir.cron - + - - - + + @@ -20,9 +18,8 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). - - + + - - + diff --git a/setup/cron_inactivity_period/odoo/addons/cron_inactivity_period b/setup/cron_inactivity_period/odoo/addons/cron_inactivity_period new file mode 120000 index 00000000000..1191b067134 --- /dev/null +++ b/setup/cron_inactivity_period/odoo/addons/cron_inactivity_period @@ -0,0 +1 @@ +../../../../cron_inactivity_period \ No newline at end of file diff --git a/setup/cron_inactivity_period/setup.py b/setup/cron_inactivity_period/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/cron_inactivity_period/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From 453bb53c8874ddcb50b0c2e7234fcd8b43d43959 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 12 Feb 2024 08:55:50 +0000 Subject: [PATCH 06/43] [UPD] Update autovacuum_message_attachment.pot --- .../i18n/autovacuum_message_attachment.pot | 1 + 1 file changed, 1 insertion(+) diff --git a/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot b/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot index 8fbd1a4f465..d6e43134195 100644 --- a/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot +++ b/autovacuum_message_attachment/i18n/autovacuum_message_attachment.pot @@ -188,6 +188,7 @@ msgstr "" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_autovacuum__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_config_parameter__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_cron__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_cron_inactivity_period__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_default__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_demo__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_demo_failure__assigned_attachment_ids From 98436e27524e32c70fabe0f91f3a5d4fdef429e4 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 12 Feb 2024 08:55:52 +0000 Subject: [PATCH 07/43] [UPD] Update base_changeset.pot --- base_changeset/i18n/base_changeset.pot | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base_changeset/i18n/base_changeset.pot b/base_changeset/i18n/base_changeset.pot index 8cfdc570002..57e293f9021 100644 --- a/base_changeset/i18n/base_changeset.pot +++ b/base_changeset/i18n/base_changeset.pot @@ -225,6 +225,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__changeset_change_ids +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_default__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__changeset_change_ids @@ -560,6 +561,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__changeset_ids +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_default__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__changeset_ids @@ -882,6 +884,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__count_pending_changeset_changes +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_default__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__count_pending_changeset_changes @@ -1197,6 +1200,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__count_pending_changesets +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_default__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__count_pending_changesets @@ -1914,6 +1918,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__user_can_see_changeset +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_default__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__user_can_see_changeset From e3e36d88db29b8e2d3624ccd46b14c48a8d14bce Mon Sep 17 00:00:00 2001 From: oca-ci Date: Mon, 12 Feb 2024 08:55:58 +0000 Subject: [PATCH 08/43] [UPD] Update cron_inactivity_period.pot --- .../i18n/cron_inactivity_period.pot | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/cron_inactivity_period/i18n/cron_inactivity_period.pot b/cron_inactivity_period/i18n/cron_inactivity_period.pot index e23324009d7..0e4cc1f8c0c 100644 --- a/cron_inactivity_period/i18n/cron_inactivity_period.pot +++ b/cron_inactivity_period/i18n/cron_inactivity_period.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: <>\n" "Language-Team: \n" @@ -14,32 +14,32 @@ msgstr "" "Plural-Forms: \n" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,inactivity_hour_begin:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__inactivity_hour_begin msgid "Begin Hour" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,create_uid:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__create_uid msgid "Created by" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,create_date:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__create_date msgid "Created on" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,cron_id:0 -msgid "Cron id" +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__cron_id +msgid "Cron" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,display_name:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__display_name msgid "Display Name" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,inactivity_hour_end:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__inactivity_hour_end msgid "End Hour" msgstr "" @@ -49,45 +49,55 @@ msgid "Hour" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,id:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__id msgid "ID" msgstr "" #. module: cron_inactivity_period -#: view:ir.cron:cron_inactivity_period.view_ir_cron_form -#: field:ir.cron,inactivity_period_ids:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron__inactivity_period_ids +#: model_terms:ir.ui.view,arch_db:cron_inactivity_period.view_ir_cron_form msgid "Inactivity Periods" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,__last_update:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period____last_update msgid "Last Modified on" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,write_uid:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__write_uid msgid "Last Updated by" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,write_date:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__write_date msgid "Last Updated on" msgstr "" #. module: cron_inactivity_period -#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:36 +#: model:ir.model,name:cron_inactivity_period.model_ir_cron +msgid "Scheduled Actions" +msgstr "" + +#. module: cron_inactivity_period +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:30 #, python-format msgid "The End Hour should be greater than the Begin Hour" msgstr "" #. module: cron_inactivity_period -#: field:ir.cron.inactivity.period,type:0 +#: model:ir.model.fields,field_description:cron_inactivity_period.field_ir_cron_inactivity_period__type msgid "Type" msgstr "" #. module: cron_inactivity_period -#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:62 +#: code:addons/cron_inactivity_period/models/ir_cron_inactivity_period.py:57 #, python-format msgid "Unimplemented Feature: Inactivity Period type '%s'" msgstr "" +#. module: cron_inactivity_period +#: model:ir.model,name:cron_inactivity_period.model_ir_cron_inactivity_period +msgid "ir.cron.inactivity.period" +msgstr "" + From fe6f8cbd228de9eb2205194abc8d35dd0aae688e Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 12 Feb 2024 09:17:17 +0000 Subject: [PATCH 09/43] [BOT] post-merge updates --- README.md | 1 + cron_inactivity_period/README.rst | 106 +++++ .../static/description/index.html | 450 ++++++++++++++++++ setup/_metapackage/VERSION.txt | 2 +- setup/_metapackage/setup.py | 1 + 5 files changed, 559 insertions(+), 1 deletion(-) create mode 100644 cron_inactivity_period/README.rst create mode 100644 cron_inactivity_period/static/description/index.html diff --git a/README.md b/README.md index d5e27390391..98b5f7b36ce 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ addon | version | maintainers | summary [bus_alt_connection](bus_alt_connection/) | 12.0.1.0.0 | | Needed when using PgBouncer as a connection pooler [company_country](company_country/) | 12.0.1.2.1 | [![moylop260](https://github.com/moylop260.png?size=30px)](https://github.com/moylop260) [![luisg123v](https://github.com/luisg123v.png?size=30px)](https://github.com/luisg123v) | Set country to main company [configuration_helper](configuration_helper/) | 12.0.1.0.0 | | Configuration Helper +[cron_inactivity_period](cron_inactivity_period/) | 12.0.1.0.0 | | Inactivity Periods for Cron Jobs [database_cleanup](database_cleanup/) | 12.0.1.2.1 | | Database cleanup [datetime_formatter](datetime_formatter/) | 12.0.1.0.0 | | Helper functions to give correct format to date[time] fields [dbfilter_from_header](dbfilter_from_header/) | 12.0.1.0.0 | | Filter databases with HTTP headers diff --git a/cron_inactivity_period/README.rst b/cron_inactivity_period/README.rst new file mode 100644 index 00000000000..ba341bdc637 --- /dev/null +++ b/cron_inactivity_period/README.rst @@ -0,0 +1,106 @@ +================================ +Inactivity Periods for Cron Jobs +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7a61e22fafb57fc9928e3f9fed782fcc3f77bed5fdb473a61f68554b8ab96a81 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/12.0/cron_inactivity_period + :alt: OCA/server-tools +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-cron_inactivity_period + :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/server-tools&target_branch=12.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows you to disable cron Jobs during periods. +It can be usefull if you want to disable your cron jobs during maintenance +period, or for other reasons. + +Note +---- + +If you have installed ``cron_run_manually`` module, it is still possible to run +your job, during inactivity periods. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +#. Go to Settings > Technical > Automation > Scheduled Actions and select a + cron +#. Add new option inactivity periods + +.. figure:: https://raw.githubusercontent.com/OCA/server-tools/8.0/cron_inactivity_period/static/description/ir_cron_form.png + :alt: Inactivity Period Settings + :width: 80 % + :align: center + +Known issues / Roadmap +====================== + +* For the time being, only one type of inactivity period is available. ('hour') + It should be great to add other options like 'week_day', to allow user to + disable cron jobs for given week days. + +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 +~~~~~~~ + +* GRAP + +Contributors +~~~~~~~~~~~~ + +* Sylvain LE GAL +* Ioan Galan + +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/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/cron_inactivity_period/static/description/index.html b/cron_inactivity_period/static/description/index.html new file mode 100644 index 00000000000..dc399accf79 --- /dev/null +++ b/cron_inactivity_period/static/description/index.html @@ -0,0 +1,450 @@ + + + + + +Inactivity Periods for Cron Jobs + + + +
+

Inactivity Periods for Cron Jobs

+ + +

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

+

This module allows you to disable cron Jobs during periods. +It can be usefull if you want to disable your cron jobs during maintenance +period, or for other reasons.

+
+

Note

+

If you have installed cron_run_manually module, it is still possible to run +your job, during inactivity periods.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+
    +
  1. Go to Settings > Technical > Automation > Scheduled Actions and select a +cron
  2. +
  3. Add new option inactivity periods
  4. +
+
+Inactivity Period Settings +
+
+
+

Known issues / Roadmap

+
    +
  • For the time being, only one type of inactivity period is available. (‘hour’) +It should be great to add other options like ‘week_day’, to allow user to +disable cron jobs for given week days.
  • +
+
+
+

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

+
    +
  • GRAP
  • +
+
+ +
+

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/server-tools project on GitHub.

+

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

+
+
+
+
+ + diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt index 3d85bcf71d1..39e2b03b232 100644 --- a/setup/_metapackage/VERSION.txt +++ b/setup/_metapackage/VERSION.txt @@ -1 +1 @@ -12.0.20221001.0 \ No newline at end of file +12.0.20240212.0 \ No newline at end of file diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py index b6179378c5c..0642df2227c 100644 --- a/setup/_metapackage/setup.py +++ b/setup/_metapackage/setup.py @@ -37,6 +37,7 @@ 'odoo12-addon-bus_alt_connection', 'odoo12-addon-company_country', 'odoo12-addon-configuration_helper', + 'odoo12-addon-cron_inactivity_period', 'odoo12-addon-database_cleanup', 'odoo12-addon-datetime_formatter', 'odoo12-addon-dbfilter_from_header', From 7c2fe754c36ddf4d0f3bb15eeb670155d40f9c39 Mon Sep 17 00:00:00 2001 From: mymage Date: Mon, 12 Feb 2024 08:22:59 +0000 Subject: [PATCH 10/43] Translated using Weblate (Italian) Currently translated at 84.8% (151 of 178 strings) Translation: server-tools-12.0/server-tools-12.0-excel_import_export Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export/it/ --- excel_import_export/i18n/it.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/excel_import_export/i18n/it.po b/excel_import_export/i18n/it.po index 4516b50439e..392a344442e 100644 --- a/excel_import_export/i18n/it.po +++ b/excel_import_export/i18n/it.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2024-02-05 09:36+0000\n" +"PO-Revision-Date: 2024-02-12 09:17+0000\n" "Last-Translator: mymage \n" "Language-Team: none\n" "Language: it\n" @@ -869,7 +869,7 @@ msgstr "" #: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export____last_update #: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import____last_update msgid "Last Modified on" -msgstr "Ultima Modifica il" +msgstr "Ultima modifica il" #. module: excel_import_export #: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__write_uid From 1c5602197ba7bc80b02dd2cdf757781d545ef999 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 12 Feb 2024 09:17:55 +0000 Subject: [PATCH 11/43] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-12.0/server-tools-12.0-base_changeset Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-base_changeset/ --- base_changeset/i18n/es.po | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/base_changeset/i18n/es.po b/base_changeset/i18n/es.po index 599ebce0bec..54c1e8a2f49 100644 --- a/base_changeset/i18n/es.po +++ b/base_changeset/i18n/es.po @@ -227,6 +227,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__changeset_change_ids +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_default__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__changeset_change_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__changeset_change_ids @@ -562,6 +563,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__changeset_ids +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_default__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__changeset_ids #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__changeset_ids @@ -884,6 +886,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__count_pending_changeset_changes +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_default__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__count_pending_changeset_changes #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__count_pending_changeset_changes @@ -1199,6 +1202,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__count_pending_changesets +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_default__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__count_pending_changesets #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__count_pending_changesets @@ -1924,6 +1928,7 @@ msgstr "" #: model:ir.model.fields,field_description:base_changeset.field_ir_autovacuum__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_config_parameter__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_cron__user_can_see_changeset +#: model:ir.model.fields,field_description:base_changeset.field_ir_cron_inactivity_period__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_default__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_demo__user_can_see_changeset #: model:ir.model.fields,field_description:base_changeset.field_ir_demo_failure__user_can_see_changeset From 74bee5ff8678ef01db56e6d93aa5734f82f8d9b1 Mon Sep 17 00:00:00 2001 From: Weblate Date: Mon, 12 Feb 2024 09:17:55 +0000 Subject: [PATCH 12/43] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-12.0/server-tools-12.0-autovacuum_message_attachment Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-autovacuum_message_attachment/ --- autovacuum_message_attachment/i18n/fr.po | 1 + 1 file changed, 1 insertion(+) diff --git a/autovacuum_message_attachment/i18n/fr.po b/autovacuum_message_attachment/i18n/fr.po index 5084a1e11d2..25088cc1c3c 100644 --- a/autovacuum_message_attachment/i18n/fr.po +++ b/autovacuum_message_attachment/i18n/fr.po @@ -191,6 +191,7 @@ msgstr "Tous" #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_autovacuum__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_config_parameter__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_cron__assigned_attachment_ids +#: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_cron_inactivity_period__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_default__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_demo__assigned_attachment_ids #: model:ir.model.fields,field_description:autovacuum_message_attachment.field_ir_demo_failure__assigned_attachment_ids From 9e31b52214b20f8c4185c17e9318f7939da91271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Thu, 27 Sep 2018 12:09:41 +0200 Subject: [PATCH 13/43] [ADD] profiler: Integration of python cprofile and postgresql logging collector for Odoo --- profiler/README.rst | 33 ++ profiler/__init__.py | 3 + profiler/__manifest__.py | 15 + profiler/hooks.py | 45 +++ profiler/models/__init__.py | 2 + profiler/models/profiler_profile.py | 468 +++++++++++++++++++++++ profiler/security/ir.model.access.csv | 3 + profiler/tests/__init__.py | 3 + profiler/tests/test_profiling.py | 13 + profiler/views/profiler_profile_view.xml | 103 +++++ 10 files changed, 688 insertions(+) create mode 100644 profiler/README.rst create mode 100644 profiler/__init__.py create mode 100644 profiler/__manifest__.py create mode 100644 profiler/hooks.py create mode 100644 profiler/models/__init__.py create mode 100644 profiler/models/profiler_profile.py create mode 100644 profiler/security/ir.model.access.csv create mode 100644 profiler/tests/__init__.py create mode 100644 profiler/tests/test_profiling.py create mode 100644 profiler/views/profiler_profile_view.xml diff --git a/profiler/README.rst b/profiler/README.rst new file mode 100644 index 00000000000..30f52081e8e --- /dev/null +++ b/profiler/README.rst @@ -0,0 +1,33 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============= +Odoo Profiler +============= + +Integration of python cprofile and postgresql logging collector for Odoo +Check the Profiler menu in admin menu + +Credits +======= + +Contributors +------------ + +* Moisés López + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/profiler/__init__.py b/profiler/__init__.py new file mode 100644 index 00000000000..92b8b3593a9 --- /dev/null +++ b/profiler/__init__.py @@ -0,0 +1,3 @@ + +from . import models +from .hooks import post_load diff --git a/profiler/__manifest__.py b/profiler/__manifest__.py new file mode 100644 index 00000000000..819819c6c36 --- /dev/null +++ b/profiler/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "profiler", + 'author': "Vauxoo", + 'website': "http://www.vauxoo.com", + 'category': 'Tests', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'depends': ["document"], + 'data': [ + 'security/ir.model.access.csv', + 'views/profiler_profile_view.xml', + ], + 'post_load': 'post_load', + 'installable': True, +} diff --git a/profiler/hooks.py b/profiler/hooks.py new file mode 100644 index 00000000000..33408ff9357 --- /dev/null +++ b/profiler/hooks.py @@ -0,0 +1,45 @@ + +import logging + +from openerp.http import WebRequest +from openerp.sql_db import Cursor + +from .models.profiler_profile import ProfilerProfile + +_logger = logging.getLogger(__name__) + + +def patch_web_request_call_function(): + """Modify Odoo entry points so that profile can record. + + Odoo is a multi-threaded program. Therefore, the :data:`profile` object + needs to be enabled/disabled each in each thread to capture all the + execution. + + For instance, Odoo spawns a new thread for each request. + """ + _logger.info('Patching http.WebRequest._call_function') + webreq_f_origin = WebRequest._call_function + + def webreq_f(*args, **kwargs): + with ProfilerProfile.profiling(): + return webreq_f_origin(*args, **kwargs) + WebRequest._call_function = webreq_f + + +def patch_cursor_init(): + _logger.info('Patching sql_dp.Cursor.__init__') + cursor_f_origin = Cursor.__init__ + + def init_f(self, *args, **kwargs): + cursor_f_origin(self, *args, **kwargs) + enable = ProfilerProfile.activate_deactivate_pglogs + if enable is not None: + self._obj.execute('SET log_min_duration_statement TO "%s"' % + ((not enable) * -1,)) + Cursor.__init__ = init_f + + +def post_load(): + patch_web_request_call_function() + patch_cursor_init() diff --git a/profiler/models/__init__.py b/profiler/models/__init__.py new file mode 100644 index 00000000000..7744d42134c --- /dev/null +++ b/profiler/models/__init__.py @@ -0,0 +1,2 @@ + +from . import profiler_profile diff --git a/profiler/models/profiler_profile.py b/profiler/models/profiler_profile.py new file mode 100644 index 00000000000..eadeb1e77c9 --- /dev/null +++ b/profiler/models/profiler_profile.py @@ -0,0 +1,468 @@ + +import base64 +import logging +import os +import pstats +import re +import subprocess +import lxml.html +from contextlib import contextmanager +from cProfile import Profile +from io import StringIO + +from psycopg2 import OperationalError, ProgrammingError + +from odoo import api, exceptions, fields, models, sql_db, tools, _ + +DATETIME_FORMAT_FILE = "%Y%m%d_%H%M%S" +CPROFILE_EMPTY_CHARS = b"{0" +PGOPTIONS = { + 'log_min_duration_statement': '0', + 'client_min_messages': 'notice', + 'log_min_messages': 'warning', + 'log_min_error_statement': 'error', + 'log_duration': 'off', + 'log_error_verbosity': 'verbose', + 'log_lock_waits': 'on', + 'log_statement': 'none', + 'log_temp_files': '0', +} +PGOPTIONS_ENV = ' '.join(["-c %s=%s" % (param, value) + for param, value in PGOPTIONS.items()]) +PY_STATS_FIELDS = [ + 'ncalls', + 'tottime', 'tt_percall', + 'cumtime', 'ct_percall', + 'file', 'lineno', 'method', +] +LINE_STATS_RE = re.compile( + r'(?P<%s>\d+/?\d+|\d+)\s+(?P<%s>\d+\.?\d+)\s+(?P<%s>\d+\.?\d+)\s+' + r'(?P<%s>\d+\.?\d+)\s+(?P<%s>\d+\.?\d+)\s+(?P<%s>.*):(?P<%s>\d+)' + r'\((?P<%s>.*)\)' % tuple(PY_STATS_FIELDS)) + +_logger = logging.getLogger(__name__) + + +class ProfilerProfilePythonLine(models.Model): + _name = 'profiler.profile.python.line' + _description = 'Profiler Python Line to save cProfiling results' + _rec_name = 'cprof_fname' + _order = 'cprof_cumtime DESC' + + profile_id = fields.Many2one('profiler.profile', required=True, + ondelete='cascade') + cprof_tottime = fields.Float("Total time") + cprof_ncalls = fields.Float("Calls") + cprof_nrcalls = fields.Float("Recursive Calls") + cprof_ttpercall = fields.Float("Time per call") + cprof_cumtime = fields.Float("Cumulative time") + cprof_ctpercall = fields.Float("CT per call") + cprof_fname = fields.Char("Filename:lineno(method)") + + +class ProfilerProfile(models.Model): + _name = 'profiler.profile' + _description = 'Profiler Profile' + + @api.model + def _find_loggers_path(self): + self.env.cr.execute("SHOW log_directory") + log_directory = self.env.cr.fetchone()[0] + self.env.cr.execute("SHOW log_filename") + log_filename = self.env.cr.fetchone()[0] + log_path = os.path.join(log_directory, log_filename) + if not os.path.isabs(log_path): + # It is relative path then join data_directory + self.env.cr.execute("SHOW data_directory") + data_dir = self.env.cr.fetchone()[0] + log_path = os.path.join(data_dir, log_path) + return log_path + + name = fields.Char(required=True) + enable_python = fields.Boolean(default=True) + enable_postgresql = fields.Boolean( + default=False, + help="It requires postgresql server logs seudo-enabled") + use_py_index = fields.Boolean( + name="Get cProfiling report", default=False, + help="Index human-readable cProfile attachment." + "\nTo access this report, you must open the cprofile attachment view " + "using debug mode.\nWarning: Uses more resources.") + date_started = fields.Char(readonly=True) + date_finished = fields.Char(readonly=True) + state = fields.Selection([ + ('enabled', 'Enabled'), + ('disabled', 'Disabled'), + ], default='disabled', readonly=True, required=True) + description = fields.Text(readonly=True) + attachment_count = fields.Integer(compute="_compute_attachment_count") + pg_log_path = fields.Char(help="Getting the path to the logger", + default=_find_loggers_path) + pg_remote = fields.Char() + pg_stats_slowest_html = fields.Html( + "PostgreSQL Stats - Slowest", readonly=True) + pg_stats_time_consuming_html = fields.Html( + "PostgreSQL Stats - Time Consuming", readonly=True) + pg_stats_most_frequent_html = fields.Html( + "PostgreSQL Stats - Most Frequent", readonly=True) + py_stats_lines = fields.One2many( + "profiler.profile.python.line", "profile_id", "PY Stats Lines") + + @api.multi + def _compute_attachment_count(self): + for record in self: + self.attachment_count = self.env['ir.attachment'].search_count([ + ('res_model', '=', self._name), ('res_id', '=', record.id)]) + + @api.onchange('enable_postgresql') + def onchange_enable_postgresql(self): + if not self.enable_postgresql: + return + self.env.cr.execute("SHOW config_file") + pg_config_file = self.env.cr.fetchone()[0] + db_host = tools.config.get('db_host') + if db_host == 'localhost' or db_host == '127.0.0.1': + db_host = False + if db_host: + pg_config_file = 'postgres@%s:%s' % (db_host, pg_config_file) + self.pg_remote = db_host + + self.description = ( + "You need seudo-enable logs from your " + "postgresql-server configuration file.\n" + "Common paths:\n\t- %s\n" + "or your can looking for the service using: " + "'ps aux | grep postgres'\n\n" + ) % pg_config_file + self.description += """Adds the following parameters: +# Pre-enable logs +logging_collector=on +log_destination='stderr' +log_directory='pg_log' +log_filename='postgresql.log' +log_rotation_age=0 +log_checkpoints=on +log_hostname=on +log_line_prefix='%t [%p]: [%l-1] db=%d,user=%u ' +log_connections=on +log_disconnections=on + +Reload configuration using the following query: + - select pg_reload_conf() +Or restart the postgresql server service. + +FYI This module will enable the following parameter from the client + It's not needed added them to configuration file if database user is + superuser or use PGOPTIONS environment variable in the terminal + that you start your odoo server. + If you don't add these parameters or PGOPTIONS this module will try do it. +# Enable logs from postgresql.conf +log_min_duration_statement=0 +client_min_messages=notice +log_min_messages=warning +log_min_error_statement=error +log_duration=off +log_error_verbosity=verbose +log_lock_waits=on +log_statement=none +log_temp_files=0 + +# Or enable logs from PGOPTIONS environment variable before to start odoo +# server +export PGOPTIONS="-c log_min_duration_statement=0 \\ +-c client_min_messages=notice -c log_min_messages=warning \\ +-c log_min_error_statement=error -c log_connections=on \\ +-c log_disconnections=on -c log_duration=off -c log_error_verbosity=verbose \\ +-c log_lock_waits=on -c log_statement=none -c log_temp_files=0" +~/odoo_path/odoo-bin ... +""" + + profile = Profile() + enabled = None + pglogs_enabled = None + + # True to activate it False to inactivate None to do nothing + activate_deactivate_pglogs = None + + # Params dict with values before to change it. + psql_params_original = {} + + @api.model + def now_utc(self): + self.env.cr.execute("SELECT to_char(now(), 'YYYY-MM-DD HH24:MI:SS')") + now = self.env.cr.fetchall()[0][0] + # now = fields.Datetime.to_string( + # fields.Datetime.context_timestamp(self, datetime.now())) + return now + + @api.multi + def enable(self): + self.ensure_one() + if tools.config.get('workers'): + raise exceptions.UserError( + _("Start the odoo server using the parameter '--workers=0'")) + _logger.info("Enabling profiler") + self.write(dict( + date_started=self.now_utc(), + state='enabled' + )) + ProfilerProfile.enabled = self.enable_python + self._reset_postgresql() + + @api.multi + def _reset_postgresql(self): + if not self.enable_postgresql: + return + if ProfilerProfile.pglogs_enabled: + _logger.info("Using postgresql.conf or PGOPTIONS predefined.") + return + os.environ['PGOPTIONS'] = ( + PGOPTIONS_ENV if self.state == 'enabled' else '') + self._reset_connection(self.state == 'enabled') + + def _reset_connection(self, enable): + for connection in sql_db._Pool._connections: + with connection[0].cursor() as pool_cr: + params = (PGOPTIONS if enable + else ProfilerProfile.psql_params_original) + for param, value in params.items(): + try: + pool_cr.execute('SET %s TO %s' % (param, value)) + except (OperationalError, ProgrammingError) as oe: + pool_cr.connection.rollback() + raise exceptions.UserError( + _("It's not possible change parameter.\n%s\n" + "Please, disable postgresql or re-enable it " + "in order to read the instructions") % str(oe)) + ProfilerProfile.activate_deactivate_pglogs = enable + + def get_stats_string(self, cprofile_path): + pstats_stream = StringIO() + pstats_obj = pstats.Stats(cprofile_path, stream=pstats_stream) + pstats_obj.sort_stats('cumulative') + pstats_obj.print_stats() + pstats_stream.seek(0) + stats_string = pstats_stream.read() + pstats_stream = None + return stats_string + + @api.multi + def dump_postgresql_logs(self, indexed=None): + self.ensure_one() + self.description = '' + pgbadger_cmd = self._get_pgbadger_command() + if pgbadger_cmd is None: + return + pgbadger_cmd_str = subprocess.list2cmdline(pgbadger_cmd) + self.description += ( + '\nRunning the command: %s') % pgbadger_cmd_str + result = tools.exec_command_pipe(*pgbadger_cmd) + datas = result[1].read() + if not datas: + self.description += "\nPgbadger output is empty!" + return + fname = self._get_attachment_name("pg_stats", ".html") + self.env['ir.attachment'].create({ + 'name': fname, + 'res_id': self.id, + 'res_model': self._name, + 'datas': base64.b64encode(datas), + 'datas_fname': fname, + 'description': 'pgbadger html output', + }) + xpaths = [ + '//*[@id="slowest-individual-queries"]', + '//*[@id="time-consuming-queries"]', + '//*[@id="most-frequent-queries"]', + ] + # pylint: disable=unbalanced-tuple-unpacking + self.pg_stats_slowest_html, self.pg_stats_time_consuming_html, \ + self.pg_stats_most_frequent_html = self._compute_pgbadger_html( + datas, xpaths) + + @staticmethod + def _compute_pgbadger_html(html_doc, xpaths): + html = lxml.html.document_fromstring(html_doc) + result = [] + for this_xpath in xpaths: + this_result = html.xpath(this_xpath) + result.append( + tools.html_sanitize(lxml.html.tostring(this_result[0]))) + return result + + @api.multi + def _get_pgbadger_command(self): + self.ensure_one() + # TODO: Catch early the following errors. + try: + pgbadger_bin = tools.find_in_path('pgbadger') + except IOError: + self.description += ( + "\nInstall 'apt-get install pgbadger'") + try: + with open(self.pg_log_path, "r"): + pass + except IOError: + self.description += ( + "\nCheck if exists and has permission to read the log file." + "\nMaybe running: chmod 604 '%s'" + ) % self.pg_log_path + + pgbadger_cmd = [ + pgbadger_bin, '-f', 'stderr', '--sample', '15', + '-o', '-', '-x', 'html', '--quiet', + '-T', self.name, + '-d', self.env.cr.dbname, + '-b', self.date_started, + '-e', self.date_finished, + self.pg_log_path, + ] + return pgbadger_cmd + + def _get_attachment_name(self, prefix, suffix): + started = fields.Datetime.from_string( + self.date_started).strftime(DATETIME_FORMAT_FILE) + finished = fields.Datetime.from_string( + self.date_finished).strftime(DATETIME_FORMAT_FILE) + fname = '%s_%d_%s_to_%s%s' % ( + prefix, self.id, started, finished, suffix) + return fname + + @api.model + def dump_stats(self): + attachment = None + with tools.osutil.tempdir() as dump_dir: + cprofile_fname = self._get_attachment_name("py_stats", ".cprofile") + cprofile_path = os.path.join(dump_dir, cprofile_fname) + _logger.info("Dumping cProfile '%s'", cprofile_path) + ProfilerProfile.profile.dump_stats(cprofile_path) + with open(cprofile_path, "rb") as f_cprofile: + datas = f_cprofile.read() + if datas and datas != CPROFILE_EMPTY_CHARS: + attachment = self.env['ir.attachment'].create({ + 'name': cprofile_fname, + 'res_id': self.id, + 'res_model': self._name, + 'datas': base64.b64encode(datas), + 'datas_fname': cprofile_fname, + 'description': 'cProfile dump stats', + }) + _logger.info("A datas was saved, here %s", attachment.name) + try: + if self.use_py_index: + py_stats = self.get_stats_string(cprofile_path) + self.env['profiler.profile.python.line'].search([ + ('profile_id', '=', self.id)]).unlink() + for py_stat_line in py_stats.splitlines(): + py_stat_line = py_stat_line.strip('\r\n ') + py_stat_line_match = LINE_STATS_RE.match( + py_stat_line) if py_stat_line else None + if not py_stat_line_match: + continue + data = dict(( + field, py_stat_line_match.group(field)) + for field in PY_STATS_FIELDS) + data['rcalls'], data['calls'] = ( + "%(ncalls)s/%(ncalls)s" % data).split('/')[:2] + self.env['profiler.profile.python.line'].create({ + 'cprof_tottime': data['tottime'], + 'cprof_ncalls': data['calls'], + 'cprof_nrcalls': data['rcalls'], + 'cprof_ttpercall': data['tt_percall'], + 'cprof_cumtime': data['cumtime'], + 'cprof_ctpercall': data['ct_percall'], + 'cprof_fname': ( + "%(file)s:%(lineno)s (%(method)s)" % data), + 'profile_id': self.id, + }) + attachment.index_content = py_stats + except IOError: + # Fancy feature but not stop process if fails + _logger.info("There was an error while getting the stats" + "from the cprofile_path") + # pylint: disable=unnecessary-pass + pass + self.dump_postgresql_logs() + _logger.info("cProfile stats stored.") + else: + _logger.info("cProfile stats empty.") + return attachment + + @api.multi + def clear(self, reset_date=True): + self.ensure_one() + _logger.info("Clear profiler") + if reset_date: + self.date_started = self.now_utc() + ProfilerProfile.profile.clear() + + @api.multi + def disable(self): + self.ensure_one() + _logger.info("Disabling profiler") + ProfilerProfile.enabled = False + self.state = 'disabled' + self.date_finished = self.now_utc() + self.dump_stats() + self.clear(reset_date=False) + self._reset_postgresql() + + @staticmethod + @contextmanager + def profiling(): + """Thread local profile management, according to the shared "enabled" + """ + if ProfilerProfile.enabled: + _logger.debug("Catching profiling") + ProfilerProfile.profile.enable() + try: + yield + finally: + if ProfilerProfile.enabled: + ProfilerProfile.profile.disable() + + @api.multi + def action_view_attachment(self): + attachments = self.env['ir.attachment'].search([ + ('res_model', '=', self._name), ('res_id', '=', self.id)]) + action = self.env.ref("base.action_attachment").read()[0] + action['domain'] = [('id', 'in', attachments.ids)] + return action + + @api.model + def set_pgoptions_enabled(self): + """Verify if postgresql has configured the parameters for logging""" + ProfilerProfile.pglogs_enabled = True + pgoptions_enabled = bool(os.environ.get('PGOPTIONS')) + _logger.info('Logging enabled from environment ' + 'variable PGOPTIONS? %s', pgoptions_enabled) + if pgoptions_enabled: + return + pgparams_required = { + 'log_min_duration_statement': '0', + } + for param, value in pgparams_required.items(): + # pylint: disable=sql-injection + self.env.cr.execute("SHOW %s" % param) + db_value = self.env.cr.fetchone()[0].lower() + if value.lower() != db_value: + ProfilerProfile.pglogs_enabled = False + break + ProfilerProfile.psql_params_original = self.get_psql_params( + self.env.cr, PGOPTIONS.keys()) + _logger.info('Logging enabled from postgresql.conf? %s', + ProfilerProfile.pglogs_enabled) + + @staticmethod + def get_psql_params(cr, params): + result = {} + for param in set(params): + # pylint: disable=sql-injection + cr.execute('SHOW %s' % param) + result.update(cr.dictfetchone()) + return result + + @api.model + def _setup_complete(self): + self.set_pgoptions_enabled() + return super(ProfilerProfile, self)._setup_complete() diff --git a/profiler/security/ir.model.access.csv b/profiler/security/ir.model.access.csv new file mode 100644 index 00000000000..494b5f10674 --- /dev/null +++ b/profiler/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_profiler_profile_admin,profiler profile admin,model_profiler_profile,base.group_system,1,1,1,1 +access_profiler_profile_line_admin,profiler profile line admin,model_profiler_profile_python_line,base.group_system,1,1,1,1 diff --git a/profiler/tests/__init__.py b/profiler/tests/__init__.py new file mode 100644 index 00000000000..bd6191a5103 --- /dev/null +++ b/profiler/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Vauxoo (https://www.vauxoo.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import test_profiling diff --git a/profiler/tests/test_profiling.py b/profiler/tests/test_profiling.py new file mode 100644 index 00000000000..7cd0819c8b1 --- /dev/null +++ b/profiler/tests/test_profiling.py @@ -0,0 +1,13 @@ +# Copyright 2018 Vauxoo (https://www.vauxoo.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openerp.tests.common import TransactionCase + + +class TestProfiling(TransactionCase): + + def test_profile_creation(self): + """We are testing the creation of a profile.""" + prof_obj = self.env['profiler.profile'] + profile = prof_obj.create({'name': 'this_profiler'}) + profile.enable() + profile.disable() diff --git a/profiler/views/profiler_profile_view.xml b/profiler/views/profiler_profile_view.xml new file mode 100644 index 00000000000..1a025760df6 --- /dev/null +++ b/profiler/views/profiler_profile_view.xml @@ -0,0 +1,103 @@ + + + view profile list + profiler.profile + + + + + + + + + + + + + + view profile form + profiler.profile + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + Profiler + profiler.profile + tree,form + + + + +
From 1e51f60371755658796867ac1c6d5607ee66a03e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Thu, 27 Sep 2018 12:09:41 +0200 Subject: [PATCH 14/43] [ADD] profiler: Integration of python cprofile and postgresql logging collector for Odoo --- profiler/__manifest__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profiler/__manifest__.py b/profiler/__manifest__.py index 819819c6c36..2bc4de39efa 100644 --- a/profiler/__manifest__.py +++ b/profiler/__manifest__.py @@ -1,9 +1,9 @@ { 'name': "profiler", - 'author': "Vauxoo", - 'website': "http://www.vauxoo.com", + 'author': "Vauxoo, Odoo Community Association (OCA)", + 'website': "https://github.com/OCA/server-tools/tree/12.0/profiler", 'category': 'Tests', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'license': 'AGPL-3', 'depends': ["document"], 'data': [ From a366dff8d8a30030f3bbf3f769f41b69550b669c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Mon, 1 Oct 2018 13:35:39 +0000 Subject: [PATCH 15/43] [REF] profiler: Forcing english log otuput and UTC time --- profiler/models/profiler_profile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/profiler/models/profiler_profile.py b/profiler/models/profiler_profile.py index eadeb1e77c9..7f69280092d 100644 --- a/profiler/models/profiler_profile.py +++ b/profiler/models/profiler_profile.py @@ -146,6 +146,7 @@ def onchange_enable_postgresql(self): log_line_prefix='%t [%p]: [%l-1] db=%d,user=%u ' log_connections=on log_disconnections=on +lc_messages='en_US.UTF-8' Reload configuration using the following query: - select pg_reload_conf() @@ -189,7 +190,8 @@ def onchange_enable_postgresql(self): @api.model def now_utc(self): - self.env.cr.execute("SELECT to_char(now(), 'YYYY-MM-DD HH24:MI:SS')") + self.env.cr.execute("SELECT to_char(current_timestamp AT TIME " + "ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')") now = self.env.cr.fetchall()[0][0] # now = fields.Datetime.to_string( # fields.Datetime.context_timestamp(self, datetime.now())) From 2464610108d64c0f15d9e04b7395048f9a814ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Mon, 1 Oct 2018 13:40:29 +0000 Subject: [PATCH 16/43] [REF] profiler: Uses standard unix log_directory --- profiler/models/profiler_profile.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/profiler/models/profiler_profile.py b/profiler/models/profiler_profile.py index 7f69280092d..2a4d19c5ab1 100644 --- a/profiler/models/profiler_profile.py +++ b/profiler/models/profiler_profile.py @@ -138,7 +138,7 @@ def onchange_enable_postgresql(self): # Pre-enable logs logging_collector=on log_destination='stderr' -log_directory='pg_log' +log_directory='/var/log/postgresql' log_filename='postgresql.log' log_rotation_age=0 log_checkpoints=on @@ -147,6 +147,7 @@ def onchange_enable_postgresql(self): log_connections=on log_disconnections=on lc_messages='en_US.UTF-8' +log_timezone='UTC' Reload configuration using the following query: - select pg_reload_conf() @@ -190,8 +191,10 @@ def onchange_enable_postgresql(self): @api.model def now_utc(self): + self.env.cr.execute("SHOW log_timezone") + zone = self.env.cr.fetchone()[0] self.env.cr.execute("SELECT to_char(current_timestamp AT TIME " - "ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS')") + "ZONE %s, 'YYYY-MM-DD HH24:MI:SS')", (zone,)) now = self.env.cr.fetchall()[0][0] # now = fields.Datetime.to_string( # fields.Datetime.context_timestamp(self, datetime.now())) From 3975ac24300f3eedd50424f8c4220d87d8659010 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Fri, 12 Oct 2018 22:21:02 -0500 Subject: [PATCH 17/43] [REF] Add patch to kw fc --- profiler/hooks.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/profiler/hooks.py b/profiler/hooks.py index 33408ff9357..f8fbce7d541 100644 --- a/profiler/hooks.py +++ b/profiler/hooks.py @@ -1,8 +1,9 @@ import logging -from openerp.http import WebRequest -from openerp.sql_db import Cursor +from odoo.http import WebRequest +from odoo.sql_db import Cursor +from odoo import api from .models.profiler_profile import ProfilerProfile @@ -27,6 +28,16 @@ def webreq_f(*args, **kwargs): WebRequest._call_function = webreq_f +def patch_call_kw_function(): + _logger.info('Patching odoo.api.call_kw') + call_kw_f_origin = api.call_kw + + def call_kw_f(*args, **kwargs): + with ProfilerProfile.profiling(): + return call_kw_f_origin(*args, **kwargs) + api.call_kw = call_kw_f + + def patch_cursor_init(): _logger.info('Patching sql_dp.Cursor.__init__') cursor_f_origin = Cursor.__init__ @@ -42,4 +53,5 @@ def init_f(self, *args, **kwargs): def post_load(): patch_web_request_call_function() + patch_call_kw_function() patch_cursor_init() From 3eb4c6975fac4f1058c976424dad5522460573a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Fri, 12 Oct 2018 22:35:34 -0500 Subject: [PATCH 18/43] Revert "[REF] Add patch to kw fc" This reverts commit a023ae24ab9c50762841b9213350802b35734a6b. --- profiler/hooks.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/profiler/hooks.py b/profiler/hooks.py index f8fbce7d541..33408ff9357 100644 --- a/profiler/hooks.py +++ b/profiler/hooks.py @@ -1,9 +1,8 @@ import logging -from odoo.http import WebRequest -from odoo.sql_db import Cursor -from odoo import api +from openerp.http import WebRequest +from openerp.sql_db import Cursor from .models.profiler_profile import ProfilerProfile @@ -28,16 +27,6 @@ def webreq_f(*args, **kwargs): WebRequest._call_function = webreq_f -def patch_call_kw_function(): - _logger.info('Patching odoo.api.call_kw') - call_kw_f_origin = api.call_kw - - def call_kw_f(*args, **kwargs): - with ProfilerProfile.profiling(): - return call_kw_f_origin(*args, **kwargs) - api.call_kw = call_kw_f - - def patch_cursor_init(): _logger.info('Patching sql_dp.Cursor.__init__') cursor_f_origin = Cursor.__init__ @@ -53,5 +42,4 @@ def init_f(self, *args, **kwargs): def post_load(): patch_web_request_call_function() - patch_call_kw_function() patch_cursor_init() From 0b6502e4c13e49adf39082e09229876ebdffe7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mois=C3=A9s=20L=C3=B3pez?= Date: Fri, 12 Oct 2018 23:13:30 -0500 Subject: [PATCH 19/43] [REF] add view TODO: context is not working --- profiler/views/profiler_profile_view.xml | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/profiler/views/profiler_profile_view.xml b/profiler/views/profiler_profile_view.xml index 1a025760df6..b5cc20025e5 100644 --- a/profiler/views/profiler_profile_view.xml +++ b/profiler/views/profiler_profile_view.xml @@ -14,6 +14,32 @@ + + + view profiling_lines + profiler.profile.python.line + + + + + + + + + + + + + + Profiling lines + profiler.profile.python.line + form + tree,form + + [] + {} + + view profile form profiler.profile @@ -30,6 +56,14 @@
+ +

Usage

@@ -446,6 +446,7 @@

Contributors

  • Fabio Vilchez <fabio.vilchez@clearcorp.co.cr>
  • Jos De Graeve <Jos.DeGraeve@apertoso.be>
  • Lai Tim Siu (Quaritle Limited) <info@quartile.co>
  • +
  • Lorenzo Battistini <https://github.com/eLBati>