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