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 000000000..0959854c1 Binary files /dev/null and b/rusty_record_reaper_runner/static/src/img/icon.png differ 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 + + + + +
+