From 1f63b3e13539ed4e19b7f8cd016f063ec54ffe35 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 10:33:31 +0200 Subject: [PATCH] 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. --- record_archiver/__init__.py | 1 + record_archiver/__openerp__.py | 45 +++++++++ record_archiver/data/cron.xml | 19 ++++ record_archiver/models/__init__.py | 3 + record_archiver/models/company.py | 30 ++++++ record_archiver/models/record_lifespan.py | 110 ++++++++++++++++++++++ record_archiver/models/res_config.py | 70 ++++++++++++++ record_archiver/static/src/img/icon.png | Bin 0 -> 5881 bytes record_archiver/views/res_config.xml | 60 ++++++++++++ 9 files changed, 338 insertions(+) create mode 100644 record_archiver/__init__.py create mode 100644 record_archiver/__openerp__.py create mode 100644 record_archiver/data/cron.xml create mode 100644 record_archiver/models/__init__.py create mode 100644 record_archiver/models/company.py create mode 100644 record_archiver/models/record_lifespan.py create mode 100644 record_archiver/models/res_config.py create mode 100644 record_archiver/static/src/img/icon.png create mode 100644 record_archiver/views/res_config.xml diff --git a/record_archiver/__init__.py b/record_archiver/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/record_archiver/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/record_archiver/__openerp__.py b/record_archiver/__openerp__.py new file mode 100644 index 000000000..2ac5c486b --- /dev/null +++ b/record_archiver/__openerp__.py @@ -0,0 +1,45 @@ +# -*- 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': 'Records Archiver', + 'version': '0.1', + 'description': """ +Create a cron job that deactivates old records in order to optimize +performance. + +Records are deactivated based on their last activity (write_date). + +You can configure lifespan of each type of record in +`Settings -> Configuration -> Records Archiver` + +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/record_archiver/data/cron.xml b/record_archiver/data/cron.xml new file mode 100644 index 000000000..f98f39ed0 --- /dev/null +++ b/record_archiver/data/cron.xml @@ -0,0 +1,19 @@ + + + + + + Records Archiver + + + 1 + months + -1 + + + + + + + + diff --git a/record_archiver/models/__init__.py b/record_archiver/models/__init__.py new file mode 100644 index 000000000..17ec9c568 --- /dev/null +++ b/record_archiver/models/__init__.py @@ -0,0 +1,3 @@ +from . import company +from . import res_config +from . import record_lifespan diff --git a/record_archiver/models/company.py b/record_archiver/models/company.py new file mode 100644 index 000000000..2b3c410a3 --- /dev/null +++ b/record_archiver/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/record_archiver/models/record_lifespan.py b/record_archiver/models/record_lifespan.py new file mode 100644 index 000000000..1a75ee229 --- /dev/null +++ b/record_archiver/models/record_lifespan.py @@ -0,0 +1,110 @@ +# -*- 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): + """ 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' + + _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 their 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_record_archiver(self, cr, uid, context=None): + lifespan_ids = self.search(cr, uid, [], context=context) + _logger.info('Records archiver starts archiving records') + for lifespan_id in lifespan_ids: + try: + self.archive_records(cr, uid, [lifespan_id], context=context) + except osv.except_osv as e: + _logger.error("Archiver error:\n%s", e[1]) + _logger.info('Rusty Records now rest in peace') + return True + + 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. + """ + lifespans = self.browse(cr, uid, ids, context=context) + today = datetime.today() + for lifespan in lifespans: + + model = self.pool[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/record_archiver/models/res_config.py b/record_archiver/models/res_config.py new file mode 100644 index 000000000..fb6c2bf61 --- /dev/null +++ b/record_archiver/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 RecordArchiverConfigSettings(orm.TransientModel): + _name = 'record.archiver.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): + _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. + 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/record_archiver/static/src/img/icon.png b/record_archiver/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/record_archiver/views/res_config.xml b/record_archiver/views/res_config.xml new file mode 100644 index 000000000..0642b4671 --- /dev/null +++ b/record_archiver/views/res_config.xml @@ -0,0 +1,60 @@ + + + + + + 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 + + + + +
+