From c99dc09b6f99047e980bc21b23f35523a677139a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 10:33:31 +0200 Subject: [PATCH 01/10] 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 zcmW+)2|QHa7Z)K*WX%?uvQJD*wk)L)#@g7k@1~d;j5TYrC&@A~m{fzI2t~G#Y}sOD z%`&z@_BH$e{r>m!x$k}6JLlbZ?z!il@AsYr6C<6A%mU0bG&C3WbYTeKt_1YE7Z`x6 z{~NVz;C9|mOV8{A&_XY`#skkx7+q^W8XDSr|Mhd=g=#ABk{_#Wg*8RHV*{Oh-Dm;> z17(q@$9^tO7&lq8uLpijO#o2h_)p0UiS=@$(eXukiz3_rxi4!iuO1*||4(?=AM1(s zr7=bN`l6j7H@%b)T!2XMKhZs3HzzC-jS@9Odb`ocL$IErhEBnv@{pVIdk<>d0X4n< ztD!N$zDN&GERBi3vlr4u6o&S8LZU=Hu~>|%oSe)5M+tCqL3?|nQGWj!CkJS`q5RzZ z!5B+<9i;CdR4vbao1sv`EKp_5wYA96xO!R zeEts1tLa{olDcr;OhJLTjU+dSkCl@x4v9oIQ@0JJ{FVo$R8>{^goQKBq=^z~dSRZP z9g0xsT&H_wzSQoSY={wTXu0;(;aaM$otiMNwpKM$)-JKfz0Nf&_jX`~OFNrUj`Gcy zUGw&gY;>zlMntoawt#P7Wsc2r#vy8XjDi9JD=e2~UHji^6Rbl1hCrylol&4|S z4|5v9!p&@Kj;N1^OWlS(SQ853hX0CPx^U^jd5mhV?MOR>m5w97p&=LN5Y8WID;qa~ z(t&LRsd3G#3A4-Ml7<~3?7T-S-27y5b(T^Y{Ag-+w+e@kXLi*|tRTv2V>+6B2BTcC zq?~J1TwHvCA^P7*IMlFi<*+YLUEk93a;omLPz_`GtoK&nx9K2mW*@SlQs>y0$Bmn9 zg6Nz}scvu**YJ5G*ux3b{vMBv#aD}u5B+_F=jZ2tIG4*jyegqDo_1A1HBtQgXr)Kt z;0+Kf8{1aIR7jGeH9x!NG3IxY+_$g#u;L6vO&`4ks05VSQ39;Q5HHvYl8WMExOM9m z?A?QvfFtwYcJ}tY30xA&Q&TAoR@dK%ikV!>dh0>^?Ugp=PcWN^T1O&}%u~6Boq=mj znzpxc;=#p5MM1l%I>P>ED-pADCNf zLj|=0`|08CqVH0DXzN<9Kwk zKc#t&7l?dM&(|Yeis;Gi`2D3U2$%IafSQvb{5axFT|<@_XgQ~ z%hwlTY_znqvtt4;w9b*)6w!fuDctI2xdUO4jJy~x<=JWF<}-f(T8$qSVQ;UYT~=3z-~BsGCX-vX=DSod(~+t{8`F>t;f?DX$KS^) z+A<$$W>n&l#_##NSpv{6^LmYXFS43gSA4x@bN$1!w+#0BPUo!8U9yF9+>lz}%^#!n z=zRNk2f1}{d@2*m#nIwkdFAYIxN*()vnNNh4)=T%LlVTStX}?HNUT$MLM7ke%Cl!7k&8f!Jyt=4o)g3T(ExQz|9QdFnALg zdxb?ms+li{q18fgu$RzYw*T3tDUJsBB`vX|rKhK-I`n9K%AN8(hTK$zp6ZQe*C>=&fF$WK<=b`R zu-Lo=$VyyGbaZrT2Xkp&W1pAoh=YlJ#nh)CSZY?gA9*bhlWo9`yD>YL2bOkH29`d1 zX2*`Nif0)hXnQ@sPaUq-6K?!6FTZpKYYRtX53J zTn$Z4$usRD5uwZ@rzc0jGYJwSl^%Tr8Dd_o0p!@Ia;dErBC?zOO8NFD8>tLB6L_u? z>+J67>1jp*GO>?(Ez5xR<>h?+xO2FRrczrV*Yof@?jNF&S+{Y~acrchsHk-#)cPv` zMZ7c!>+|aoXGf+h-@}gfHW%NeWbUnul6}P-L0ca`wdc29yUQ6CO%tK1(r6}qVkn=L zu9bfW!k2@ny6~Q!;nvl#bF6I(Z;0I6zzl$dAwqMy@78>7J^ZiD)Bp}_EuZ=+;w*gV zqm8Ai-$J(#agfqQUniPkPM?#m?Ywfd_?4JGrmSKrwR=}O13u@g5}Dxb1R@xBmv#>P z`P2O^?8pp_UY~%IzR%V3Q(&&UKwMugv#CabR;Qbz)%HeQECB8_TbLUzCnj7ss#vDl zw7G3<6tExA{XVUa8u4p*xDZYpfF^l}_xtCLjXeaS^E4tIVI$W|@aYY1nB1K@njCqX zrh6U0bN7i_e+x@X%Ou3|T!vFr{MX_W*@^Wkzel3<8!KXF`#8AR12|>>=%!(sRz8Nw z-0CalDqxCvjqO@$)>dJ2LWhz_@EnJSsVSGS<_XlM%FD^s6(+`+Ko2MJPyKW2$Th;> zH;1+@1hUBx2Q_WA&?6OK*fYSN%0dN&g&y~{ab4PBVd_~1_cc!c&FVy+g_rk!c3Q$H z=QV8i-i|6q8H=Y~=S&cTlcFt|7%=!QcgO8Gm8?bn1MY@{SSLyT$`z&X`ux5&Tht&mvTlzXrr2-6M*lqb{?Ld@x8r9Yjx`yH@KNkO5zPvU`A02 zhJGh@ffMvi0M>Pslzpo;m)cb#aIxB@9R_9LDXHK)Q>>0XY;i8e=H}+goCyXw@-WwI zn_sH|z-AU_Yq@ge%K7NK7NxeGQ#+h@4NJSngQ4fQz7)lwNPHqU;uDzn%$&vIpra8 zudc3wZK@1VUn#MgJgG6WwOlNT@yFZQw;lsdPy<|T0kDV9=r!h6q!QGQYbyp6IRRk9 z=Fy3t5L?{8Pz|5DiGL@jOwoTTJ}|9Ym0MjGPVrkAx%sqNl!u3h<~DfjSmJe3fUx3;#XX%5eA`xiE+`tLn8@7u}V_-L)ajGz)~Z6Xrs@BzX zr|$T+)kTMDq_|mgo!QA=4hstd+tjglr!9?##@na6tCm3_Et(ujmX!;o8KG3l5vC?bArpy4 z6Zk?P3*z1LzNQ0?UifTQ7MF0mn0{vb)Vc%9n}qnst%*F}?CJu_0VW#p0Pb(c0^#sE zot%mBPeOTJ6JmC(9^VL=c=q!cs?;Zj%MEu3nr=Er{CfyR@BI7jTmHrOrmc8dGlZ2k zDw3;h;g9jmrgES9zU^J9U33{>Awqha5_hG+Bjivrqb)WI$>*7USErqWw;QjZt(mG@ z1O%~S*}k_rZY)Vuw9|)hl*`qYulCkiMkL$dBIr5t_q{aP3UPzFFQ)3ZADg(a zlP4!j0RVYN!TgJ`Tekm$ss^$dKRri4?DRBK#-dhK)ee803@9I350aT_^N5!`<7Bu!IC#1 zPv7C6^K;DK>&62h0fOr0KfAgP0NO0c%^bn*C`n`sI|p3hpsf{Sg3X!8i!16zzh>VA zI=;5D%Skm$jPi^A`bU^EL7jKw*#v58Z8yZN;ugZeruk$)JeU5Xp=+gz(AuT==94Mg zmcX+aCWbf->vY(8TvzMbM4hRva%x0=;7kGXc+xKL^s2*AG|?;_*YC6AA|ODm2tQ%i z6H#j;nHKU12po#g2&Z_waN;vH_=u6bWBrRgvSsNRO9KxZJu+*&qiTQOV9~eX8>%Cfag3NwN1U0$d>cbndmR4s^JZu#`|K480mlea>5O#5aviAR zQ+<7XGqeYpZ<4`t^$%BO_h52E8G6OalaJvbG;iv3uevS+aR#-fvanY?fO~%uZ0l1v zc%^>(6=;qJLi2Ri9-pxU_RFH-?N(re{az= z_O;&VLcjG6=Mr`NC$ZI=dpnN2G&k3o2BfG}1l-@j_H^qWvj)LSq!Y*2%UiU^cfLUu zz%CUYg$DVf^u%}x}~|fcAL7z5_W6= zx>^V)wWkYIKG&YB(bDUzF?-=!b*Hv0X{gP5C;1`SqC>y)-?xQ;I*IQ^ZDcv+jE9j* z61bXTdTrQG1F{!E!J{_?-LN~P3qV9|sf^w5@7-aCvewy6O*#uBY0f_Cb{X|Xl50Ve z$rQi9Oa@u@wuWKblit+;~VL6^?l`F?4LnK);%GSwgo2pijST0lYW;O z*R3+OA$$Ns37wyjxBTM=+8PGVx8+t~aS7wRxj~X?G|8-3PimLWk?sya+k{7g?w)Cs z4L?y9NRDMi^EY$~-Fp|@D`q5Lx4g9=_&oP_I^sFwu0f`O_&un=4X)IVxgD9OT&aIs zC>s$dBr-KW|7ysSC+6a52eF7{7Y->U!|mS`3dLDIt6s1uP+3SvT3XsZv3*vtkgVqq zg?D1e3OQHP1QrSk1QOr1;C}D@!w3-`r@6VhKJ$b!Z^T(%EJTj6XJ==he^XtJYi@4d zPe-iVb)1ePNblX`vu2$3!P{X) zipSxa;f)b(GLk*>g@MZ2_1&!`_Q1;bUorysuC%XV!E7j5If{L<;Z94~?9@~aRLwkZFi^TWGlD4sS<_)J#}rPCi;c~j zsPe`yudf58pSS&z{{H^mSqgCg{RDP>LDc+qaBOUBLAOi`WW%gy24kG)l@FWgIG6cl zNI^*eq@cQs+S*wn<90(E8X8u{zOay?hIYe*D485TB4X(Qhu)lTO(Qcyv?8I;gFp7_ z?CB2!@ptlB&oet+J6z?nY-Iyw34IdaE>-Vr_sSmkq}qwKH{+{q)NQ9_VU+>=`y0Y@ z8$;Dr75@~Wh9gtKnG{!MLZ8yS#&qR0>f3?)O2BP^_SZPT@@k70u^j#UM~G#aNJb1D zFNS-OMgsxw`P!BgPbk+N!W)upEk*XuqtL}~q^Duzi_AR9$G$p@0Gw)w6= l5_51_x&8H+`tj)O+~^*~JpIBA7NA5-qo-{IE7fv*`aiLFcJKfI 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 + + + + +
+
From 870dee8c6a459ff653ad8592f8335f1ce54a45dc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 10:44:38 +0200 Subject: [PATCH 02/10] Make the archiver more extensible by extracting methods. Now we are able to extend the module to implement other archiving stategies. --- record_archiver/data/cron.xml | 2 +- record_archiver/models/record_lifespan.py | 92 ++++++++++++++--------- 2 files changed, 57 insertions(+), 37 deletions(-) diff --git a/record_archiver/data/cron.xml b/record_archiver/data/cron.xml index f98f39ed0..5780f097a 100644 --- a/record_archiver/data/cron.xml +++ b/record_archiver/data/cron.xml @@ -11,7 +11,7 @@ -1 - + diff --git a/record_archiver/models/record_lifespan.py b/record_archiver/models/record_lifespan.py index 1a75ee229..74d24d9ef 100644 --- a/record_archiver/models/record_lifespan.py +++ b/record_archiver/models/record_lifespan.py @@ -21,7 +21,7 @@ import logging from datetime import datetime from dateutil.relativedelta import relativedelta -from openerp.osv import orm, fields, osv +from openerp.osv import orm, fields from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from openerp.tools.translate import _ @@ -61,50 +61,70 @@ class RecordLifespan(orm.Model): "Months must be a value greater than 0"), ] - def _scheduler_record_archiver(self, cr, uid, context=None): + def _scheduler_archive_records(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: + except orm.except_orm 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 + def _archive_domain(self, cr, uid, lifespan, expiration_date, + context=None): + """ Returns the domain used to find the records to archive. + + Can be inherited to change the archived records for a model. + """ + model = self.pool[lifespan.model] + domain = [('write_date', '<', expiration_date), + ('company_id', '=', lifespan.company_id.id)] + if 'state' in model._columns.keys(): + domain += [('state', 'in', ('done', 'cancel'))] + return domain + + def _archive_lifespan_records(self, cr, uid, lifespan, context=None): + """ Archive the records for a lifespan, so for a model. + + Can be inherited to customize the archive strategy. + The default strategy is to change the field ``active`` to False + on the records having a ``write_date`` older than the lifespan. + Only done and canceled records will be deactivated. - 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) + model = self.pool[lifespan.model] + if not model: + raise orm.except_orm( + _('Error'), + _('Model %s not found') % lifespan.model) + if 'active' not in model._columns.keys(): + raise orm.except_orm( + _('Error'), + _('Model %s has no active field') % lifespan.model) + + delta = relativedelta(months=lifespan.months) + expiration_date = (today - delta).strftime(DATE_FORMAT) + + domain = self._archive_domain(cr, uid, lifespan, expiration_date, + context=context) + rec_ids = model.search(cr, uid, domain, context=context) + if not rec_ids: + return + + # 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) + + def archive_records(self, cr, uid, ids, context=None): + """ Call the archiver for several record lifespans """ + for lifespan in self.browse(cr, uid, ids, context=context): + self._archive_lifespan_records(cr, uid, lifespan, context=context) + return True From 50343135bdefcc18bb3767b7547451804171d602 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 11:18:51 +0200 Subject: [PATCH 03/10] Use a m2o for the models --- record_archiver/models/company.py | 2 +- record_archiver/models/record_lifespan.py | 23 ++++++++++++++++------- record_archiver/models/res_config.py | 8 ++++---- record_archiver/views/res_config.xml | 4 ++-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/record_archiver/models/company.py b/record_archiver/models/company.py index 2b3c410a3..cf6a9c361 100644 --- a/record_archiver/models/company.py +++ b/record_archiver/models/company.py @@ -23,7 +23,7 @@ class Company(orm.Model): _inherit = 'res.company' _columns = { - 'lifespan_ids': fields.one2many( + 'record_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 index 74d24d9ef..e6b1928dd 100644 --- a/record_archiver/models/record_lifespan.py +++ b/record_archiver/models/record_lifespan.py @@ -39,9 +39,18 @@ class RecordLifespan(orm.Model): _order = 'model' _columns = { - 'model': fields.char( - "Model", - required=True), + 'model_id': fields.many2one( + 'ir.model', + string='Model', + required=True, + ), + 'model': fields.related( + 'model_id', 'model', + string='Model Name', + type='char', + readonly=True, + store=True, + ), 'months': fields.integer( "Months", required=True, @@ -55,7 +64,7 @@ class RecordLifespan(orm.Model): } _sql_constraints = [ - ('model_uniq', 'unique(model, company_id)', + ('model_uniq', 'unique(model_id, 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"), @@ -81,7 +90,7 @@ class RecordLifespan(orm.Model): model = self.pool[lifespan.model] domain = [('write_date', '<', expiration_date), ('company_id', '=', lifespan.company_id.id)] - if 'state' in model._columns.keys(): + if 'state' in model._columns: domain += [('state', 'in', ('done', 'cancel'))] return domain @@ -95,12 +104,12 @@ class RecordLifespan(orm.Model): """ today = datetime.today() - model = self.pool[lifespan.model] + model = self.pool.get(lifespan.model) if not model: raise orm.except_orm( _('Error'), _('Model %s not found') % lifespan.model) - if 'active' not in model._columns.keys(): + if 'active' not in model._columns: raise orm.except_orm( _('Error'), _('Model %s has no active field') % lifespan.model) diff --git a/record_archiver/models/res_config.py b/record_archiver/models/res_config.py index fb6c2bf61..3ca7f4b9c 100644 --- a/record_archiver/models/res_config.py +++ b/record_archiver/models/res_config.py @@ -29,8 +29,8 @@ class RecordArchiverConfigSettings(orm.TransientModel): _columns = { 'company_id': fields.many2one('res.company', 'Company', required=True), - 'lifespan_ids': fields.related( - 'company_id', 'lifespan_ids', + 'record_lifespan_ids': fields.related( + 'company_id', 'record_lifespan_ids', string='Record Lifespans', type='one2many', relation='record.lifespan'), @@ -63,8 +63,8 @@ class RecordArchiverConfigSettings(orm.TransientModel): 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] + lifespan_ids = [l.id for l in company.record_lifespan_ids] values = { - 'lifespan_ids': lifespan_ids, + 'record_lifespan_ids': lifespan_ids, } return {'value': values} diff --git a/record_archiver/views/res_config.xml b/record_archiver/views/res_config.xml index 0642b4671..83d85306d 100644 --- a/record_archiver/views/res_config.xml +++ b/record_archiver/views/res_config.xml @@ -31,9 +31,9 @@
- + - + From bdac334710183c4605154fdc0d887afbafde1e5b Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 30 Jun 2015 11:28:43 +0200 Subject: [PATCH 04/10] Hide models without an active field --- record_archiver/models/__init__.py | 1 + record_archiver/models/ir_model.py | 73 +++++++++++++++++++++++ record_archiver/models/record_lifespan.py | 1 + 3 files changed, 75 insertions(+) create mode 100644 record_archiver/models/ir_model.py diff --git a/record_archiver/models/__init__.py b/record_archiver/models/__init__.py index 17ec9c568..8637ee398 100644 --- a/record_archiver/models/__init__.py +++ b/record_archiver/models/__init__.py @@ -1,3 +1,4 @@ from . import company +from . import ir_model from . import res_config from . import record_lifespan diff --git a/record_archiver/models/ir_model.py b/record_archiver/models/ir_model.py new file mode 100644 index 000000000..a9162d1eb --- /dev/null +++ b/record_archiver/models/ir_model.py @@ -0,0 +1,73 @@ +# -*- 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 openerp.osv import orm, fields + + +class IrModel(orm.Model): + _inherit = 'ir.model' + + def _compute_has_an_active_field(self, cr, uid, ids, name, + args, context=None): + res = {} + for model_id in ids: + active_field_ids = self.pool['ir.model.fields'].search( + cr, uid, + [('model_id', '=', model_id), + ('name', '=', 'active'), + ], + limit=1, + context=context) + res[model_id] = bool(active_field_ids) + return res + + def _search_has_an_active_field(self, cr, uid, obj, name, args, + context=None): + if not len(args): + return [] + fields_model = self.pool['ir.model.fields'] + domain = [] + for field, operator, value in args: + assert field == name + active_field_ids = fields_model.search( + cr, uid, [('name', '=', 'active')], context=context) + active_fields = fields_model.read(cr, uid, active_field_ids, + fields=['model_id'], + load='_classic_write', + context=context) + model_ids = [field['model_id'] for field in active_fields] + if operator == '=': + domain.append(('id', 'in', model_ids)) + elif operator == '!=': + domain.append(('id', 'not in', model_ids)) + else: + raise AssertionError('operator %s not allowed' % operator) + return domain + + _columns = { + 'has_an_active_field': fields.function( + _compute_has_an_active_field, + fnct_search=_search_has_an_active_field, + string='Has an active field', + readonly=True, + type='boolean', + ), + } diff --git a/record_archiver/models/record_lifespan.py b/record_archiver/models/record_lifespan.py index e6b1928dd..66cd222c4 100644 --- a/record_archiver/models/record_lifespan.py +++ b/record_archiver/models/record_lifespan.py @@ -43,6 +43,7 @@ class RecordLifespan(orm.Model): 'ir.model', string='Model', required=True, + domain=[('has_an_active_field', '=', True)], ), 'model': fields.related( 'model_id', 'model', From c834aea434521f610ad7a029418029caa7cb74e9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 10:43:37 +0200 Subject: [PATCH 05/10] Remove the company on record.lifespan The company was mandatory and prevented to use the module for models without company_id. Also, there is currently no use case for a different configuration for different companies. It simplifies the setup too. If a use case exists for a different setup per company, it can be reintroduced later with a optional company_id. --- record_archiver/__openerp__.py | 2 +- record_archiver/models/__init__.py | 2 - record_archiver/models/company.py | 30 -------- record_archiver/models/record_lifespan.py | 9 +-- record_archiver/models/res_config.py | 70 ------------------- .../views/record_lifespan_view.xml | 50 +++++++++++++ record_archiver/views/res_config.xml | 60 ---------------- 7 files changed, 52 insertions(+), 171 deletions(-) delete mode 100644 record_archiver/models/company.py delete mode 100644 record_archiver/models/res_config.py create mode 100644 record_archiver/views/record_lifespan_view.xml delete mode 100644 record_archiver/views/res_config.xml diff --git a/record_archiver/__openerp__.py b/record_archiver/__openerp__.py index 2ac5c486b..e24ae22f6 100644 --- a/record_archiver/__openerp__.py +++ b/record_archiver/__openerp__.py @@ -37,7 +37,7 @@ Lifespan is defined per record per company. 'complexity': "easy", # easy, normal, expert 'depends': ['base'], 'website': 'www.camptocamp.com', - 'data': ['views/res_config.xml', + 'data': ['views/record_lifespan_view.xml', 'data/cron.xml'], 'test': [], 'installable': True, diff --git a/record_archiver/models/__init__.py b/record_archiver/models/__init__.py index 8637ee398..a48e87b41 100644 --- a/record_archiver/models/__init__.py +++ b/record_archiver/models/__init__.py @@ -1,4 +1,2 @@ -from . import company from . import ir_model -from . import res_config from . import record_lifespan diff --git a/record_archiver/models/company.py b/record_archiver/models/company.py deleted file mode 100644 index cf6a9c361..000000000 --- a/record_archiver/models/company.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- 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 = { - 'record_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 index 66cd222c4..510742cd4 100644 --- a/record_archiver/models/record_lifespan.py +++ b/record_archiver/models/record_lifespan.py @@ -57,16 +57,9 @@ class RecordLifespan(orm.Model): 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_id, 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"), ] @@ -90,7 +83,7 @@ class RecordLifespan(orm.Model): """ model = self.pool[lifespan.model] domain = [('write_date', '<', expiration_date), - ('company_id', '=', lifespan.company_id.id)] + ] if 'state' in model._columns: domain += [('state', 'in', ('done', 'cancel'))] return domain diff --git a/record_archiver/models/res_config.py b/record_archiver/models/res_config.py deleted file mode 100644 index 3ca7f4b9c..000000000 --- a/record_archiver/models/res_config.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- 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), - 'record_lifespan_ids': fields.related( - 'company_id', 'record_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.record_lifespan_ids] - values = { - 'record_lifespan_ids': lifespan_ids, - } - return {'value': values} diff --git a/record_archiver/views/record_lifespan_view.xml b/record_archiver/views/record_lifespan_view.xml new file mode 100644 index 000000000..45d69a31c --- /dev/null +++ b/record_archiver/views/record_lifespan_view.xml @@ -0,0 +1,50 @@ + + + + + + record.lifespan.tree + record.lifespan + + + + + + + + + + record.lifespan.search + record.lifespan + + + + + + + + + Records Archiver Lifespans + ir.actions.act_window + record.lifespan + form + tree + + +

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

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

+
+
+ + + +
+
diff --git a/record_archiver/views/res_config.xml b/record_archiver/views/res_config.xml deleted file mode 100644 index 83d85306d..000000000 --- a/record_archiver/views/res_config.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - 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 90f6c7d8f225f29d1aa1fd3201ecfac1bd7b99c5 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:14:31 +0200 Subject: [PATCH 06/10] 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 f1c727444562859e2745542617347c02873d673f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:19:37 +0200 Subject: [PATCH 07/10] 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 b5d99d4424b6f428182721a221f13c8477071206 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:33:09 +0200 Subject: [PATCH 08/10] 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 e8c1d96d4076f7f6e7a3e5f93ac3b547828a972b Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 1 Jul 2015 14:35:51 +0200 Subject: [PATCH 09/10] 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 46d41a8b7f908d04eff0932d343fe967555450b0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 17 Aug 2015 16:06:49 +0200 Subject: [PATCH 10/10] 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, }