From 8d49d9a96923ec6a7e8686a3884f76f7eb91d929 Mon Sep 17 00:00:00 2001 From: Yannick Vaucher Date: Thu, 18 Jun 2015 17:30:36 +0200 Subject: [PATCH 01/12] Add module rusty_record_reaper_runner --- rusty_record_reaper_runner/__init__.py | 1 + rusty_record_reaper_runner/__openerp__.py | 44 +++++++ rusty_record_reaper_runner/data/cron.xml | 19 +++ rusty_record_reaper_runner/models/__init__.py | 3 + rusty_record_reaper_runner/models/company.py | 30 +++++ .../models/record_lifespan.py | 112 ++++++++++++++++++ .../models/res_config.py | 70 +++++++++++ .../static/src/img/icon.png | Bin 0 -> 5881 bytes .../views/res_config.xml | 58 +++++++++ 9 files changed, 337 insertions(+) create mode 100644 rusty_record_reaper_runner/__init__.py create mode 100644 rusty_record_reaper_runner/__openerp__.py create mode 100644 rusty_record_reaper_runner/data/cron.xml create mode 100644 rusty_record_reaper_runner/models/__init__.py create mode 100644 rusty_record_reaper_runner/models/company.py create mode 100644 rusty_record_reaper_runner/models/record_lifespan.py create mode 100644 rusty_record_reaper_runner/models/res_config.py create mode 100644 rusty_record_reaper_runner/static/src/img/icon.png create mode 100644 rusty_record_reaper_runner/views/res_config.xml diff --git a/rusty_record_reaper_runner/__init__.py b/rusty_record_reaper_runner/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/rusty_record_reaper_runner/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/rusty_record_reaper_runner/__openerp__.py b/rusty_record_reaper_runner/__openerp__.py new file mode 100644 index 000000000..22bcfce39 --- /dev/null +++ b/rusty_record_reaper_runner/__openerp__.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Author: Yannick Vaucher +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +{'name': 'Rusty Record Reaper Runner', + 'version': '0.1', + 'description': """ + Define a cron job to deactivate old records in order to optimize performances. + + Records are deactivated base on last activity on them (write_date). + + You can configure lifespan of each type of record in + Settings -> Configuration -> Rusty Record Reaper Runner + + Lifespan is defined per record per company. + """, + 'author': 'Camptocamp', + 'maintainer': 'Camptocamp', + 'license': 'AGPL-3', + 'category': 'misc', + 'complexity': "easy", # easy, normal, expert + 'depends': ['base'], + 'website': 'www.camptocamp.com', + 'data': ['views/res_config.xml', + 'data/cron.xml'], + 'test': [], + 'installable': True, + 'auto_install': False, + } diff --git a/rusty_record_reaper_runner/data/cron.xml b/rusty_record_reaper_runner/data/cron.xml new file mode 100644 index 000000000..286623bac --- /dev/null +++ b/rusty_record_reaper_runner/data/cron.xml @@ -0,0 +1,19 @@ + + + + + + Rusty Record Reaper Runner + + + 1 + months + -1 + + + + + + + + diff --git a/rusty_record_reaper_runner/models/__init__.py b/rusty_record_reaper_runner/models/__init__.py new file mode 100644 index 000000000..17ec9c568 --- /dev/null +++ b/rusty_record_reaper_runner/models/__init__.py @@ -0,0 +1,3 @@ +from . import company +from . import res_config +from . import record_lifespan diff --git a/rusty_record_reaper_runner/models/company.py b/rusty_record_reaper_runner/models/company.py new file mode 100644 index 000000000..2b3c410a3 --- /dev/null +++ b/rusty_record_reaper_runner/models/company.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# +# Author: Yannick Vaucher +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +from openerp.osv import orm, fields + + +class Company(orm.Model): + _inherit = 'res.company' + + _columns = { + 'lifespan_ids': fields.one2many( + 'record.lifespan', + 'company_id', + string="Record Lifespans"), + } diff --git a/rusty_record_reaper_runner/models/record_lifespan.py b/rusty_record_reaper_runner/models/record_lifespan.py new file mode 100644 index 000000000..c4271e660 --- /dev/null +++ b/rusty_record_reaper_runner/models/record_lifespan.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# +# Author: Yannick Vaucher +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import logging + +from datetime import datetime +from dateutil.relativedelta import relativedelta + +from openerp.osv import orm, fields, osv +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT +from openerp.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class RecordLifespan(orm.Model): + """Instead of writing this info on ir.model + here is a new object to be able to configure rec lifespan + per company + """ + _name = 'record.lifespan' + + _order = 'model' + + _columns = { + 'model': fields.char( + "Model", + required=True), + 'months': fields.integer( + "Months", + required=True, + help="Number of month after which the records will be set to " + "inactive based on write date"), + 'company_id': fields.many2one( + 'res.company', + string="Company", + ondelete="cascade", + required=True), + } + + _sql_constraints = [ + ('model_uniq', 'unique(model, company_id)', + "A model can only have 1 lifespan per company"), + ('months_gt_0', 'check (months > 0)', + "Months must be a value greater than 0"), + ] + + def _scheduler_rusty_record_reaper(self, cr, uid, context=None): + lifespan_ids = self.search(cr, uid, [], context=context) + _logger.info('Record Reaper starts harvesting rusty records') + for lifespan_id in lifespan_ids: + try: + self.harvest_rusty_records( + cr, uid, [lifespan_id], context=context) + except osv.except_osv as e: + _logger.error("Reaper error:\n%s", e[1]) + _logger.info('Rusty Records now rest in peace') + return True + + def harvest_rusty_records(self, cr, uid, ids, context=None): + """ Search and deactivate old records for each configured lifespan + + Only done and cancelled records will be deactivated. + """ + if context is None: + context = {} + lifespans = self.browse(cr, uid, ids, context=context) + today = datetime.today() + for lifespan in lifespans: + + model = self.pool.get(lifespan.model) + if not model: + raise osv.except_osv( + _('Error'), + _('Model %s not found') % lifespan.model) + if 'active' not in model._columns.keys(): + raise osv.except_osv( + _('Error'), + _('Model %s has no active field') % lifespan.model) + delta = relativedelta(months=lifespan.months) + expiration_date = (today - delta).strftime(DATE_FORMAT) + domain = [('write_date', '<', expiration_date), + ('company_id', '=', lifespan.company_id.id)] + if 'state' in model._columns.keys(): + domain += [('state', 'in', ('done', 'cancel'))] + rec_ids = model.search(cr, uid, domain, context=context) + + if not rec_ids: + continue + # use a SQL query to bypass tracking always messages on write for + # object inheriting mail.thread + query = ("UPDATE %s SET active = FALSE WHERE id in %%s" + ) % model._table + cr.execute(query, (tuple(rec_ids),)) + _logger.info( + 'Archived %s %s older than %s', + len(rec_ids), lifespan.model, expiration_date) diff --git a/rusty_record_reaper_runner/models/res_config.py b/rusty_record_reaper_runner/models/res_config.py new file mode 100644 index 000000000..92c47c219 --- /dev/null +++ b/rusty_record_reaper_runner/models/res_config.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# +# Author: Yannick Vaucher +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +import logging + +from openerp.osv import orm, fields + +_logger = logging.getLogger(__name__) + + +class RustyRecordReaperRunnerConfigSettings(orm.TransientModel): + _name = 'rusty.record.reaper.runner.config.settings' + _inherit = 'res.config.settings' + + _columns = { + 'company_id': fields.many2one('res.company', 'Company', required=True), + 'lifespan_ids': fields.related( + 'company_id', 'lifespan_ids', + string='Record Lifespans', + type='one2many', + relation='record.lifespan'), + } + + def _default_company(self, cr, uid, context=None): + user = self.pool.get('res.users').browse(cr, uid, uid, context=context) + return user.company_id.id + + _defaults = { + 'company_id': _default_company, + } + + def create(self, cr, uid, values, context=None): + rec_id = super(RustyRecordReaperRunnerConfigSettings, self + ).create(cr, uid, values, context=context) + # Hack: to avoid some nasty bug, related fields are not written upon + # record creation. + # Hence we write on those fields here. + vals = {} + for fname, field in self._columns.iteritems(): + if isinstance(field, fields.related) and fname in values: + vals[fname] = values[fname] + self.write(cr, uid, [rec_id], vals, context=context) + return id + + def onchange_company_id(self, cr, uid, ids, company_id, context=None): + # update related fields + if not company_id: + return {'value': {}} + company = self.pool.get('res.company' + ).browse(cr, uid, company_id, context=context) + lifespan_ids = [l.id for l in company.lifespan_ids] + values = { + 'lifespan_ids': lifespan_ids, + } + return {'value': values} diff --git a/rusty_record_reaper_runner/static/src/img/icon.png b/rusty_record_reaper_runner/static/src/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0959854c1c44a9efcb8ca7c86173c5c6d7bc2ff8 GIT binary patch literal 5881 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 literal 0 HcmV?d00001 diff --git a/rusty_record_reaper_runner/views/res_config.xml b/rusty_record_reaper_runner/views/res_config.xml new file mode 100644 index 000000000..7a5a02e74 --- /dev/null +++ b/rusty_record_reaper_runner/views/res_config.xml @@ -0,0 +1,58 @@ + + + + + + rusty record reaper runner settings + rusty.record.reaper.runner.config.settings + +
+
+
+ + +
+
+
+
+
+ + All following type of record will be harvested by the cron based on write_date. If a record has made his time, it will be deactivated. + +
+
+ + + + + + +
+
+
+ +
+
+ + + Configure Rusty Record Reaper Runner + ir.actions.act_window + rusty.record.reaper.runner.config.settings + form + inline + + + + +
+
From 4239e84ea665c924170ae40927f604f88d13b1fc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 10:33:31 +0200 Subject: [PATCH 02/12] Rename module From rusty_record_reaper_runner to record_archiver. Not that I dislike the old funny name, but I guess it will be easier to get what it does with the new (maybe too serious) name. --- .../__init__.py | 0 .../__openerp__.py | 13 +++++----- .../data/cron.xml | 6 ++--- .../models/__init__.py | 0 .../models/company.py | 0 .../models/record_lifespan.py | 24 ++++++++---------- .../models/res_config.py | 8 +++--- .../static/src/img/icon.png | Bin .../views/res_config.xml | 24 ++++++++++-------- 9 files changed, 38 insertions(+), 37 deletions(-) rename {rusty_record_reaper_runner => record_archiver}/__init__.py (100%) rename {rusty_record_reaper_runner => record_archiver}/__openerp__.py (76%) rename {rusty_record_reaper_runner => record_archiver}/data/cron.xml (68%) rename {rusty_record_reaper_runner => record_archiver}/models/__init__.py (100%) rename {rusty_record_reaper_runner => record_archiver}/models/company.py (100%) rename {rusty_record_reaper_runner => record_archiver}/models/record_lifespan.py (84%) rename {rusty_record_reaper_runner => record_archiver}/models/res_config.py (90%) rename {rusty_record_reaper_runner => record_archiver}/static/src/img/icon.png (100%) rename {rusty_record_reaper_runner => record_archiver}/views/res_config.xml (61%) diff --git a/rusty_record_reaper_runner/__init__.py b/record_archiver/__init__.py similarity index 100% rename from rusty_record_reaper_runner/__init__.py rename to record_archiver/__init__.py diff --git a/rusty_record_reaper_runner/__openerp__.py b/record_archiver/__openerp__.py similarity index 76% rename from rusty_record_reaper_runner/__openerp__.py rename to record_archiver/__openerp__.py index 22bcfce39..2ac5c486b 100644 --- a/rusty_record_reaper_runner/__openerp__.py +++ b/record_archiver/__openerp__.py @@ -17,17 +17,18 @@ # along with this program. If not, see . # -{'name': 'Rusty Record Reaper Runner', +{'name': 'Records Archiver', 'version': '0.1', 'description': """ - Define a cron job to deactivate old records in order to optimize performances. +Create a cron job that deactivates old records in order to optimize +performance. - Records are deactivated base on last activity on them (write_date). +Records are deactivated based on their last activity (write_date). - You can configure lifespan of each type of record in - Settings -> Configuration -> Rusty Record Reaper Runner +You can configure lifespan of each type of record in +`Settings -> Configuration -> Records Archiver` - Lifespan is defined per record per company. +Lifespan is defined per record per company. """, 'author': 'Camptocamp', 'maintainer': 'Camptocamp', diff --git a/rusty_record_reaper_runner/data/cron.xml b/record_archiver/data/cron.xml similarity index 68% rename from rusty_record_reaper_runner/data/cron.xml rename to record_archiver/data/cron.xml index 286623bac..f98f39ed0 100644 --- a/rusty_record_reaper_runner/data/cron.xml +++ b/record_archiver/data/cron.xml @@ -2,8 +2,8 @@ - - Rusty Record Reaper Runner + + Records Archiver 1 @@ -11,7 +11,7 @@ -1 - + diff --git a/rusty_record_reaper_runner/models/__init__.py b/record_archiver/models/__init__.py similarity index 100% rename from rusty_record_reaper_runner/models/__init__.py rename to record_archiver/models/__init__.py diff --git a/rusty_record_reaper_runner/models/company.py b/record_archiver/models/company.py similarity index 100% rename from rusty_record_reaper_runner/models/company.py rename to record_archiver/models/company.py diff --git a/rusty_record_reaper_runner/models/record_lifespan.py b/record_archiver/models/record_lifespan.py similarity index 84% rename from rusty_record_reaper_runner/models/record_lifespan.py rename to record_archiver/models/record_lifespan.py index c4271e660..1a75ee229 100644 --- a/rusty_record_reaper_runner/models/record_lifespan.py +++ b/record_archiver/models/record_lifespan.py @@ -29,9 +29,10 @@ _logger = logging.getLogger(__name__) class RecordLifespan(orm.Model): - """Instead of writing this info on ir.model - here is a new object to be able to configure rec lifespan - per company + """ Configure records lifespans per model + + After the lifespan is expired (compared to the `write_date` of the + records), the records are deactivated. """ _name = 'record.lifespan' @@ -45,7 +46,7 @@ class RecordLifespan(orm.Model): "Months", required=True, help="Number of month after which the records will be set to " - "inactive based on write date"), + "inactive based on their write date"), 'company_id': fields.many2one( 'res.company', string="Company", @@ -60,30 +61,27 @@ class RecordLifespan(orm.Model): "Months must be a value greater than 0"), ] - def _scheduler_rusty_record_reaper(self, cr, uid, context=None): + def _scheduler_record_archiver(self, cr, uid, context=None): lifespan_ids = self.search(cr, uid, [], context=context) - _logger.info('Record Reaper starts harvesting rusty records') + _logger.info('Records archiver starts archiving records') for lifespan_id in lifespan_ids: try: - self.harvest_rusty_records( - cr, uid, [lifespan_id], context=context) + self.archive_records(cr, uid, [lifespan_id], context=context) except osv.except_osv as e: - _logger.error("Reaper error:\n%s", e[1]) + _logger.error("Archiver error:\n%s", e[1]) _logger.info('Rusty Records now rest in peace') return True - def harvest_rusty_records(self, cr, uid, ids, context=None): + def archive_records(self, cr, uid, ids, context=None): """ Search and deactivate old records for each configured lifespan Only done and cancelled records will be deactivated. """ - if context is None: - context = {} lifespans = self.browse(cr, uid, ids, context=context) today = datetime.today() for lifespan in lifespans: - model = self.pool.get(lifespan.model) + model = self.pool[lifespan.model] if not model: raise osv.except_osv( _('Error'), diff --git a/rusty_record_reaper_runner/models/res_config.py b/record_archiver/models/res_config.py similarity index 90% rename from rusty_record_reaper_runner/models/res_config.py rename to record_archiver/models/res_config.py index 92c47c219..fb6c2bf61 100644 --- a/rusty_record_reaper_runner/models/res_config.py +++ b/record_archiver/models/res_config.py @@ -23,8 +23,8 @@ from openerp.osv import orm, fields _logger = logging.getLogger(__name__) -class RustyRecordReaperRunnerConfigSettings(orm.TransientModel): - _name = 'rusty.record.reaper.runner.config.settings' +class RecordArchiverConfigSettings(orm.TransientModel): + _name = 'record.archiver.config.settings' _inherit = 'res.config.settings' _columns = { @@ -45,8 +45,8 @@ class RustyRecordReaperRunnerConfigSettings(orm.TransientModel): } def create(self, cr, uid, values, context=None): - rec_id = super(RustyRecordReaperRunnerConfigSettings, self - ).create(cr, uid, values, context=context) + _super = super(RecordArchiverConfigSettings, self) + rec_id = _super.create(cr, uid, values, context=context) # Hack: to avoid some nasty bug, related fields are not written upon # record creation. # Hence we write on those fields here. diff --git a/rusty_record_reaper_runner/static/src/img/icon.png b/record_archiver/static/src/img/icon.png similarity index 100% rename from rusty_record_reaper_runner/static/src/img/icon.png rename to record_archiver/static/src/img/icon.png diff --git a/rusty_record_reaper_runner/views/res_config.xml b/record_archiver/views/res_config.xml similarity index 61% rename from rusty_record_reaper_runner/views/res_config.xml rename to record_archiver/views/res_config.xml index 7a5a02e74..0642b4671 100644 --- a/rusty_record_reaper_runner/views/res_config.xml +++ b/record_archiver/views/res_config.xml @@ -2,11 +2,11 @@ - - rusty record reaper runner settings - rusty.record.reaper.runner.config.settings + + record archiver settings + record.archiver.config.settings -
+
- - -
-
-
-
-
- - All following type of record will be harvested by the cron - based on write_date. If a record has made his time, it will be - deactivated. - -
-
- - - - - - -
-
-
- -
-
- - - Configure Records Archiver - ir.actions.act_window - record.archiver.config.settings - form - inline - - - - -
-
From 386341d4ff97c4d182f7ebeac57a117293f2fc30 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:14:31 +0200 Subject: [PATCH 07/12] Add tests an fix a condition On the search of active models --- record_archiver/models/ir_model.py | 4 +- record_archiver/tests/__init__.py | 8 +++ record_archiver/tests/test_active_search.py | 56 +++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 record_archiver/tests/__init__.py create mode 100644 record_archiver/tests/test_active_search.py diff --git a/record_archiver/models/ir_model.py b/record_archiver/models/ir_model.py index a9162d1eb..8122c43a1 100644 --- a/record_archiver/models/ir_model.py +++ b/record_archiver/models/ir_model.py @@ -54,9 +54,9 @@ class IrModel(orm.Model): load='_classic_write', context=context) model_ids = [field['model_id'] for field in active_fields] - if operator == '=': + if operator == '=' or not value: domain.append(('id', 'in', model_ids)) - elif operator == '!=': + elif operator == '!=' or value: domain.append(('id', 'not in', model_ids)) else: raise AssertionError('operator %s not allowed' % operator) diff --git a/record_archiver/tests/__init__.py b/record_archiver/tests/__init__.py new file mode 100644 index 000000000..1cc4cf843 --- /dev/null +++ b/record_archiver/tests/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from . import test_active_search + + +checks = [ + test_active_search, +] diff --git a/record_archiver/tests/test_active_search.py b/record_archiver/tests/test_active_search.py new file mode 100644 index 000000000..f96bb7365 --- /dev/null +++ b/record_archiver/tests/test_active_search.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# +# Authors: Guewen Baconnier +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + + +import openerp.tests.common as common + + +class TestActiveSearch(common.TransactionCase): + + def test_model_with_active_field(self): + cr, uid = self.cr, self.uid + IrModel = self.registry('ir.model') + partner_model_id = IrModel.search(cr, uid, + [('model', '=', 'res.partner')], + limit=1)[0] + partner_model = IrModel.browse(cr, uid, partner_model_id) + self.assertTrue(partner_model.has_an_active_field) + self.assertIn(partner_model_id, + IrModel.search(cr, uid, + [('has_an_active_field', '=', True)])) + self.assertIn(partner_model_id, + IrModel.search(cr, uid, + [('has_an_active_field', '!=', False)])) + + def test_model_without_active_field(self): + cr, uid = self.cr, self.uid + IrModel = self.registry('ir.model') + country_model_id = IrModel.search(cr, uid, + [('model', '=', 'res.country')], + limit=1) + country_model = IrModel.browse(cr, uid, country_model_id[0]) + self.assertFalse(country_model.has_an_active_field) + self.assertNotIn(country_model_id, + IrModel.search(cr, uid, + [('has_an_active_field', '=', False)])) + self.assertNotIn(country_model_id, + IrModel.search(cr, uid, + [('has_an_active_field', '!=', True)])) From 723ddbc8a5cd1beb2ffaeeda830956a9b48542db Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:19:37 +0200 Subject: [PATCH 08/12] Add access rights --- record_archiver/__openerp__.py | 4 +++- record_archiver/security/ir.model.access.csv | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 record_archiver/security/ir.model.access.csv diff --git a/record_archiver/__openerp__.py b/record_archiver/__openerp__.py index e24ae22f6..146313735 100644 --- a/record_archiver/__openerp__.py +++ b/record_archiver/__openerp__.py @@ -38,7 +38,9 @@ Lifespan is defined per record per company. 'depends': ['base'], 'website': 'www.camptocamp.com', 'data': ['views/record_lifespan_view.xml', - 'data/cron.xml'], + 'data/cron.xml', + 'security/ir.model.access.csv', + ], 'test': [], 'installable': True, 'auto_install': False, diff --git a/record_archiver/security/ir.model.access.csv b/record_archiver/security/ir.model.access.csv new file mode 100644 index 000000000..73b9057af --- /dev/null +++ b/record_archiver/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_record_lifespan,Record Lifespan,record_archiver.model_record_lifespan,base.group_no_one,1,1,1,1 From a07535d5767b8e3a9808a678bf5e72ee1da0a5ea Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:33:09 +0200 Subject: [PATCH 09/12] Add tests for the archiving of records --- record_archiver/tests/__init__.py | 2 + record_archiver/tests/test_archive.py | 68 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 record_archiver/tests/test_archive.py diff --git a/record_archiver/tests/__init__.py b/record_archiver/tests/__init__.py index 1cc4cf843..9ea217c5d 100644 --- a/record_archiver/tests/__init__.py +++ b/record_archiver/tests/__init__.py @@ -1,8 +1,10 @@ # -*- coding: utf-8 -*- from . import test_active_search +from . import test_archive checks = [ test_active_search, + test_archive, ] diff --git a/record_archiver/tests/test_archive.py b/record_archiver/tests/test_archive.py new file mode 100644 index 000000000..93ba06772 --- /dev/null +++ b/record_archiver/tests/test_archive.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# +# +# Authors: Guewen Baconnier +# Copyright 2015 Camptocamp SA +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# + +from datetime import datetime, timedelta +import openerp.tests.common as common + + +class TestArchive(common.TransactionCase): + + def setUp(self): + super(TestArchive, self).setUp() + self.Partner = self.registry('res.partner') + cr, uid = self.cr, self.uid + self.partner1_id = self.Partner.create(cr, uid, + {'name': 'test user 1'}) + self.partner2_id = self.Partner.create(cr, uid, + {'name': 'test user 2'}) + self.partner3_id = self.Partner.create(cr, uid, + {'name': 'test user 3'}) + old_date = datetime.now() - timedelta(days=365) + self.cr.execute('UPDATE res_partner SET write_date = %s ' + 'WHERE id IN %s', (old_date, tuple([self.partner2_id, + self.partner3_id])) + ) + self.Lifespan = self.registry('record.lifespan') + self.model_id = self.ref('base.model_res_partner') + + def test_lifespan(self): + cr, uid = self.cr, self.uid + lifespan_id = self.Lifespan.create( + cr, uid, + {'model_id': self.model_id, + 'months': 3, + }) + self.Lifespan.archive_records(cr, uid, [lifespan_id]) + self.assertTrue(self.Partner.browse(cr, uid, self.partner1_id).active) + self.assertFalse(self.Partner.browse(cr, uid, self.partner2_id).active) + self.assertFalse(self.Partner.browse(cr, uid, self.partner3_id).active) + + def test_scheduler(self): + cr, uid = self.cr, self.uid + self.Lifespan.create( + cr, uid, + {'model_id': self.model_id, + 'months': 3, + }) + self.Lifespan._scheduler_archive_records(cr, uid) + self.assertTrue(self.Partner.browse(cr, uid, self.partner1_id).active) + self.assertFalse(self.Partner.browse(cr, uid, self.partner2_id).active) + self.assertFalse(self.Partner.browse(cr, uid, self.partner3_id).active) From 366726a41ae175448a974351f22b7173329cf5a3 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:35:51 +0200 Subject: [PATCH 10/12] Add translation pot --- record_archiver/i18n/record_archiver.pot | 99 ++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 record_archiver/i18n/record_archiver.pot diff --git a/record_archiver/i18n/record_archiver.pot b/record_archiver/i18n/record_archiver.pot new file mode 100644 index 000000000..68ed9a609 --- /dev/null +++ b/record_archiver/i18n/record_archiver.pot @@ -0,0 +1,99 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * record_archiver +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 7.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-07-01 12:35+0000\n" +"PO-Revision-Date: 2015-07-01 12:35+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: record_archiver +#: model:ir.actions.act_window,help:record_archiver.action_record_lifespan_view +msgid "

\n" +" Click to define a new lifespan for a type of records.\n" +"

\n" +" Every record of model with a lifespan will be set to inactive\n" +" after the the defined months are elapsed. The lifespan is\n" +" based on the last write on a record.\n" +"

\n" +" " +msgstr "" + +#. module: record_archiver +#: code:addons/record_archiver/models/record_lifespan.py:104 +#: code:addons/record_archiver/models/record_lifespan.py:108 +#, python-format +msgid "Error" +msgstr "" + +#. module: record_archiver +#: field:ir.model,has_an_active_field:0 +msgid "Has an active field" +msgstr "" + +#. module: record_archiver +#: field:record.lifespan,model_id:0 +msgid "Model" +msgstr "" + +#. module: record_archiver +#: code:addons/record_archiver/models/record_lifespan.py:109 +#, python-format +msgid "Model %s has no active field" +msgstr "" + +#. module: record_archiver +#: code:addons/record_archiver/models/record_lifespan.py:105 +#, python-format +msgid "Model %s not found" +msgstr "" + +#. module: record_archiver +#: field:record.lifespan,model:0 +msgid "Model Name" +msgstr "" + +#. module: record_archiver +#: code:_description:0 +#: model:ir.model,name:record_archiver.model_ir_model +#, python-format +msgid "Models" +msgstr "" + +#. module: record_archiver +#: field:record.lifespan,months:0 +msgid "Months" +msgstr "" + +#. module: record_archiver +#: sql_constraint:record.lifespan:0 +msgid "Months must be a value greater than 0" +msgstr "" + +#. module: record_archiver +#: help:record.lifespan,months:0 +msgid "Number of month after which the records will be set to inactive based on their write date" +msgstr "" + +#. module: record_archiver +#: model:ir.actions.act_window,name:record_archiver.action_record_lifespan_view +#: model:ir.ui.menu,name:record_archiver.menu_record_lifespan_config +#: view:record.lifespan:0 +msgid "Records Archiver Lifespans" +msgstr "" + +#. module: record_archiver +#: code:_description:0 +#: model:ir.model,name:record_archiver.model_record_lifespan +#, python-format +msgid "record.lifespan" +msgstr "" + From b5e5d9bbfacfd60815ddef229e203519920a784b Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 17 Aug 2015 16:06:49 +0200 Subject: [PATCH 11/12] Use the OCA template in __openerp__.py --- record_archiver/__openerp__.py | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/record_archiver/__openerp__.py b/record_archiver/__openerp__.py index 146313735..f76b83f4c 100644 --- a/record_archiver/__openerp__.py +++ b/record_archiver/__openerp__.py @@ -20,28 +20,80 @@ {'name': 'Records Archiver', 'version': '0.1', 'description': """ +Records Archiver +================ + Create a cron job that deactivates old records in order to optimize performance. Records are deactivated based on their last activity (write_date). +Configuration +============= + You can configure lifespan of each type of record in `Settings -> Configuration -> Records Archiver` -Lifespan is defined per record per company. +A different lifespan can be configured for each model. + +Usage +===== + +Once the lifespans are configured, the cron will automatically +deactivate the old records. + +Known issues / Roadmap +====================== + +The default behavior is to archive all records having a ``write_date`` < +lifespan and with a state being ``done`` or ``cancel``. If these rules +need to be modified for a model (e.g. change the states to archive), the +hook ``RecordLifespan._archive_domain`` can be extended. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, +please check there if your issue has already been reported. If you +spotted it first, help us smashing it by providing a detailed and +welcomed feedback `here +`_. + + +Credits +======= + +Contributors +------------ + +* Yannick Vaucher +* Guewen Baconnier + +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 http://odoo-community.org. """, - 'author': 'Camptocamp', - 'maintainer': 'Camptocamp', + 'author': 'Camptocamp,Odoo Community Association (OCA)', 'license': 'AGPL-3', 'category': 'misc', - 'complexity': "easy", # easy, normal, expert 'depends': ['base'], 'website': 'www.camptocamp.com', 'data': ['views/record_lifespan_view.xml', 'data/cron.xml', 'security/ir.model.access.csv', ], - 'test': [], 'installable': True, 'auto_install': False, } From 85146900a2e0cbe45b3508729a0345fc693d2103 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 17 Aug 2015 17:11:33 +0200 Subject: [PATCH 12/12] Exclude record_archiver from the one-by-one tests Because an error prevent creation of res.partner records in the tests due the 'mail' addon adding a required 'notification_email_send' field. This addon does not depend on 'mail' so the test violates the not-null constraint. The tests are passing in the normal builds anyway. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c4ceed2f7..c3378649c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ env: - VERSION="7.0" LINT_CHECK=1 - VERSION="7.0" ODOO_REPO="odoo/odoo" LINT_CHECK=0 - VERSION="7.0" ODOO_REPO="OCA/OCB" LINT_CHECK=0 - - VERSION="7.0" UNIT_TEST="1" LINT_CHECK=0 + - VERSION="7.0" UNIT_TEST="1" LINT_CHECK=0 EXCLUDE=record_archiver virtualenv: system_site_packages: true