Merge lp:~therp-nl/openupgrade-tools/7.0-add-database_cleanup into lp:openupgrade-tools

Proposed by Stefan Rijnhart (Opener) on 2014-01-28
Status: Work in progress
Proposed branch: lp:~therp-nl/openupgrade-tools/7.0-add-database_cleanup
Merge into: lp:openupgrade-tools
Diff against target: 1054 lines (+972/-0)
15 files modified
database_cleanup/__init__.py (+1/-0)
database_cleanup/__openerp__.py (+55/-0)
database_cleanup/model/__init__.py (+6/-0)
database_cleanup/model/purge_columns.py (+154/-0)
database_cleanup/model/purge_data.py (+106/-0)
database_cleanup/model/purge_models.py (+127/-0)
database_cleanup/model/purge_modules.py (+91/-0)
database_cleanup/model/purge_tables.py (+138/-0)
database_cleanup/model/purge_wizard.py (+64/-0)
database_cleanup/view/menu.xml (+48/-0)
database_cleanup/view/purge_columns.xml (+37/-0)
database_cleanup/view/purge_data.xml (+37/-0)
database_cleanup/view/purge_models.xml (+36/-0)
database_cleanup/view/purge_modules.xml (+36/-0)
database_cleanup/view/purge_tables.xml (+36/-0)
To merge this branch: bzr merge lp:~therp-nl/openupgrade-tools/7.0-add-database_cleanup
Reviewer Review Type Date Requested Status
Pedro Manuel Baeza code review and test 2014-01-28 Needs Fixing on 2014-03-28
Review via email: mp+203633@code.launchpad.net

Description of the change

Partial implementation of https://blueprints.launchpad.net/openupgrade-server/+spec/service-module-feature.

This proposal adds a module to clean up after a homebrew migration process such as OpenUpgrade.

To post a comment you must log in.
2. By Stefan Rijnhart (Opener) on 2014-01-29

[FIX] Typo, comment

3. By Stefan Rijnhart (Opener) on 2014-01-29

[FIX] Remove unused import, add missing copyright notice

Hi Stefan,

Thanks for this handy module!

I had an error when purging a model having attachments (poweremail.mailbox). Not sure if this issue lies in the ir.attachment code or the purge though.

In openerp/addons/base/ir/ir_attachment.py the method check() calls exists on the deleted model.

            mids = self.pool.get(model).exists(cr, uid, mids)
            ima.check(cr, uid, model, mode)
            self.pool.get(model).check_access_rule(cr, uid, mids, mode, context=context)

Maybe should the purge delete the attachments or drop the relationship beforehand?

Traceback (most recent call last):
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/netsvc.py", line 292, in dispatch_rpc
    result = ExportService.getService(service_name).dispatch(method, params)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/service/web_services.py", line 626, in dispatch
    res = fn(db, uid, *params)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/osv/osv.py", line 190, in execute_kw
    return self.execute(db, uid, obj, method, *args, **kw or {})
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/osv/osv.py", line 132, in wrapper
    return f(self, dbname, *args, **kwargs)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/osv/osv.py", line 199, in execute
    res = self.execute_cr(cr, uid, obj, method, *args, **kw)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/osv/osv.py", line 187, in execute_cr
    return getattr(object, method)(cr, uid, *args, **kw)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/openupgrade-tools/database_cleanup/model/purge_wizard.py", line 58, in purge_all
    context=context)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/openupgrade-tools/database_cleanup/model/purge_models.py", line 73, in purge
    context=context)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/addons/base_calendar/crm_meeting.py", line 160, in write
    return super(ir_attachment, self).write(cr, uid, ids, vals, context=context)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/addons/document/document.py", line 137, in write
    return super(document_file, self).write(cr, uid, ids, vals, context)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/addons/base/ir/ir_attachment.py", line 277, in write
    self.check(cr, uid, ids, 'write', context=context, values=vals)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/addons/document/document.py", line 77, in check
    super(document_file, self).check(cr, uid, ids, mode, context=context, values=values)
  File "/home/gbaconnier/code/instances/openerp_foobar/trunk7/parts/server/openerp/addons/base/ir/ir_attachment.py", line 211, in check
    mids = self.pool.get(model).exists(cr, uid, mids)
AttributeError: 'NoneType' object has no attribute 'exists'

>
> Maybe should the purge delete the attachments or drop the relationship
> beforehand?
>

This is already what is done through these lines:

                attachment_ids = attachment_pool.search(
                    cr, uid, [('res_model', '=', line.name)], context=context)
                if attachment_ids:
                    attachment_pool.write(
                        cr, uid, attachment_ids, {'res_model': False},
                        context=context)

I have to figure out what happens.

I got it. It happens on models still in ir.model but not in the registry because the python modules are not there.

I replaced the lines

                    attachment_pool.write(
                        cr, uid, attachment_ids, {'res_model': False},
                        context=context)
By
                    cr.execute(
                        "UPDATE ir_attachment SET res_model = FALSE "
                        "WHERE id IN %s",
                        (tuple(attachment_ids), ))

And it worked. Is it an acceptable solution?

When removing the models from ir.model, shouldn't we remove the ir.model.fields having a relation pointing to them?

> When removing the models from ir.model, shouldn't we remove the
> ir.model.fields having a relation pointing to them?

My proposal: https://code.launchpad.net/~camptocamp/openupgrade-tools/7.0-add-database_cleanup-purge-model-relations/+merge/204440

> I got it. It happens on models still in ir.model but not in the registry
> because the python modules are not there.
>
> I replaced the lines
>
> attachment_pool.write(
> cr, uid, attachment_ids, {'res_model': False},
> context=context)
> By
> cr.execute(
> "UPDATE ir_attachment SET res_model = FALSE "
> "WHERE id IN %s",
> (tuple(attachment_ids), ))
>
> And it worked. Is it an acceptable solution?

MP's here https://code.launchpad.net/~camptocamp/openupgrade-tools/7.0-add-database_cleanup-purge-model-attachment-error/+merge/204442

4. By Guewen Baconnier @ Camptocamp on 2014-02-05

[FIX] avoid ''NoneType' object has no attribute 'exists'' error when purging models

5. By Guewen Baconnier @ Camptocamp on 2014-02-05

[IMP] Remove the fields having a relation to the purged models.

Thank you very much, Guewen! I merged both your contributions into this branch.

Oo I don't know what I did... But my rev5 is obviously wrong (replaced the wrong lines), here is the correct fix https://code.launchpad.net/~camptocamp/openupgrade-tools/7.0-add-database_cleanup-purge-model-attachment-error2/+merge/205125

Sorry!

> Oo I don't know what I did... But my rev5 is obviously wrong (replaced the
> wrong lines), here is the correct fix https://code.launchpad.net/~camptocamp
> /openupgrade-tools/7.0-add-database_cleanup-purge-model-attachment-
> error2/+merge/205125
>
> Sorry!

The one merged in 4, not 5. Decidely need holidays ;-)

6. By Guewen Baconnier @ Camptocamp on 2014-02-06

[FIX] error in previous merge

7. By Stefan Rijnhart (Opener) on 2014-02-08

[FIX] Don't remove uid field from wkf_instance, which is written in
    raw SQL query (but never read afterwards). Workaround for
    lp:1277899

Apparently, base.sql comes with its own inconsistencies. Fixed in http://bazaar.launchpad.net/~therp-nl/openupgrade-tools/7.0-add-database_cleanup/revision/7.

8. By Stefan Rijnhart (Opener) on 2014-02-10

[FIX] Preserve dangling workflow table which is in use

Discovered a workflow table without corresponding model that should not be purged.

9. By Stefan Rijnhart (Opener) on 2014-02-12

[RFR] Group models per table when detecting columns to purge
      to prevent problems with models sharing the same table

After some extensive testing I fixed the case of 'sale_id' not being in stock.picking.in, thus to be purged from stock.picking. The fix allows for multiple models to share the same table in a generic way.

10. By Stefan Rijnhart (Opener) on 2014-02-20

[ADD] Allow purging of dangling data entries

11. By Stefan Rijnhart (Opener) on 2014-02-20

[FIX] Data purging now working

The module now allows to purge entries from ir.model.data that refer to a nonexisting row in its model's table.

12. By Stefan Rijnhart (Opener) on 2014-02-20

[IMP] Docstrings

13. By Stefan Rijnhart (Opener) on 2014-03-14

[FIX] Label
[FIX] Catch attempt to unlink field from nonexisting model

14. By Stefan Rijnhart (Opener) on 2014-03-14

[RFR] Flake8

Pedro Manuel Baeza (pedro.baeza) wrote :

Hi, Stefan, finally I have tried your module and it's fantastic. For the most of the sections, it works like intended, but in "Purge obsolete tables", I find some tables that cannot be removed due to foreign keys in other tables. Use DROP CASCADE is too risky to be implemented, but you can enclose removal in a try and warns user afterwards, but continuing with tables that possibly contain the foreign key it complains about.

Also a warn that it's convenient to remove obsolete fields before purging obsolete tables is a good improvements.

One more thing: if I try to click on line button to remove tables one by one, I get line form view, but button cannot be clicked. Maybe readonly attribute?

Thanks for this excellent module.

Regards.

P.S.: Don't you think this module better fits on server-env-tools?

review: Needs Fixing (code review and test)

Thanks for the review, Pedro! Glad you like it. I'll have a look at continuing after a drop table failed.

You can only click on the icons on individual line after you save the wizard form first. There is nothing that I can do about that, I think.

I could propose on server-env-tools, but as OpenERP 7.0 is pretty good in not leaving these remnants after module updates and uninstalls, this module is typically used after a home grown migration. We would need consensus on whether or not to adopt tools for home grown migration within OCA first. I expect such a discussion to pop up after the formalization of the association in the course of this year. Until there is a clear decision on this topic, I think these projects are better of separately.

Pedro Manuel Baeza (pedro.baeza) wrote :

Yeah, you are right about the inclusion on server-env-tools just right now.

About wizard saving before clicking on buttons, maybe you can present it on a popup or go to view mode to avoid this. Can it be done?

Regards.

Stéphane Bidoul (Acsone) (sbi) wrote :

Hi Stefan,

I just stumbled upon this module. This is most useful!

It seems to work fine on 8.0, except the individual purge buttons are disabled in the lists. Not sure why yet.

Is this the latest version, or did it find a home somewhere else?

Thanks!

@Stéphane: glad you found this useful. Doesn't saving the wizard form first work to access the list buttons in 8.0 like it does in 7.0?

I never got round to implementing Pedro's suggestion. I could leave it and propose to server-tools 7.0 as is. It can be improved afterwards. What do you think?

Pedro Manuel Baeza (pedro.baeza) wrote :

If you don't have to work on the improvement, the module itself is valuable, so I agree that you should propose to server-tools. Please put a section TODO on the module description with the considerations for being taking into account for other possible contributors.

Stéphane Bidoul (Acsone) (sbi) wrote :

@Stephan,

I indeed found that saving first solved the button issue, shame on me :)

The module is very useful as is, so I'm +1 to have it proposed to OCA/server-tools.

I have a short patch/hack to make it work on 8.0 in case anyone is interested.

Stéphane Bidoul (Acsone) (sbi) wrote :

@Stefan,

My colleague Anthony has made the pull request to OCA/server-tool.
https://github.com/OCA/server-tools/pull/95

Commit authorship has been preserved. I hope you take no issue with this.

Of course, feel free to clone the git branch and re-do the PR.

Best regards,

Hi Stéphane, thanks for picking this up. I see that I am already too late for the review, as it is already merged ;-)

Unmerged revisions

14. By Stefan Rijnhart (Opener) on 2014-03-14

[RFR] Flake8

13. By Stefan Rijnhart (Opener) on 2014-03-14

[FIX] Label
[FIX] Catch attempt to unlink field from nonexisting model

12. By Stefan Rijnhart (Opener) on 2014-02-20

[IMP] Docstrings

11. By Stefan Rijnhart (Opener) on 2014-02-20

[FIX] Data purging now working

10. By Stefan Rijnhart (Opener) on 2014-02-20

[ADD] Allow purging of dangling data entries

9. By Stefan Rijnhart (Opener) on 2014-02-12

[RFR] Group models per table when detecting columns to purge
      to prevent problems with models sharing the same table

8. By Stefan Rijnhart (Opener) on 2014-02-10

[FIX] Preserve dangling workflow table which is in use

7. By Stefan Rijnhart (Opener) on 2014-02-08

[FIX] Don't remove uid field from wkf_instance, which is written in
    raw SQL query (but never read afterwards). Workaround for
    lp:1277899

6. By Guewen Baconnier @ Camptocamp on 2014-02-06

[FIX] error in previous merge

5. By Guewen Baconnier @ Camptocamp on 2014-02-05

[IMP] Remove the fields having a relation to the purged models.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'database_cleanup'
2=== added file 'database_cleanup/__init__.py'
3--- database_cleanup/__init__.py 1970-01-01 00:00:00 +0000
4+++ database_cleanup/__init__.py 2014-03-14 13:27:36 +0000
5@@ -0,0 +1,1 @@
6+from . import model
7
8=== added file 'database_cleanup/__openerp__.py'
9--- database_cleanup/__openerp__.py 1970-01-01 00:00:00 +0000
10+++ database_cleanup/__openerp__.py 2014-03-14 13:27:36 +0000
11@@ -0,0 +1,55 @@
12+# -*- coding: utf-8 -*-
13+##############################################################################
14+#
15+# OpenERP, Open Source Management Solution
16+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
17+#
18+# This program is free software: you can redistribute it and/or modify
19+# it under the terms of the GNU Affero General Public License as
20+# published by the Free Software Foundation, either version 3 of the
21+# License, or (at your option) any later version.
22+#
23+# This program is distributed in the hope that it will be useful,
24+# but WITHOUT ANY WARRANTY; without even the implied warranty of
25+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26+# GNU Affero General Public License for more details.
27+#
28+# You should have received a copy of the GNU Affero General Public License
29+# along with this program. If not, see <http://www.gnu.org/licenses/>.
30+#
31+##############################################################################
32+
33+{
34+ 'name': 'Database cleanup',
35+ 'version': '0.1',
36+ 'author': 'Therp BV',
37+ 'depends': ['base'],
38+ 'license': 'AGPL-3',
39+ 'category': 'Tools',
40+ 'data': [
41+ 'view/purge_modules.xml',
42+ 'view/purge_models.xml',
43+ 'view/purge_columns.xml',
44+ 'view/purge_tables.xml',
45+ 'view/purge_data.xml',
46+ 'view/menu.xml',
47+ ],
48+ 'description': """\
49+Clean your OpenERP database from remnants of modules, models, columns and
50+tables left by uninstalled modules (prior to 7.0) or a homebrew database
51+upgrade to a new major version of OpenERP.
52+
53+After installation of this module, go to the Settings menu -> Technical ->
54+Database cleanup. Go through the modules, models, columns and tables
55+entries under this menu (in that order) and find out if there is orphaned data
56+in your database. You can either delete entries by line, or sweep all entries
57+in one big step (if you are *really* confident).
58+
59+Caution! This module is potentially harmful and can *easily* destroy the
60+integrity of your data. Do not use if you are not entirely comfortable
61+with the technical details of the OpenERP data model of *all* the modules
62+that have ever been installed on your database, and do not purge any module,
63+model, column or table if you do not know exactly what you are doing.
64+""",
65+
66+}
67
68=== added directory 'database_cleanup/model'
69=== added file 'database_cleanup/model/__init__.py'
70--- database_cleanup/model/__init__.py 1970-01-01 00:00:00 +0000
71+++ database_cleanup/model/__init__.py 2014-03-14 13:27:36 +0000
72@@ -0,0 +1,6 @@
73+from . import purge_wizard
74+from . import purge_modules
75+from . import purge_models
76+from . import purge_columns
77+from . import purge_tables
78+from . import purge_data
79
80=== added file 'database_cleanup/model/purge_columns.py'
81--- database_cleanup/model/purge_columns.py 1970-01-01 00:00:00 +0000
82+++ database_cleanup/model/purge_columns.py 2014-03-14 13:27:36 +0000
83@@ -0,0 +1,154 @@
84+# -*- coding: utf-8 -*-
85+##############################################################################
86+#
87+# OpenERP, Open Source Management Solution
88+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
89+#
90+# This program is free software: you can redistribute it and/or modify
91+# it under the terms of the GNU Affero General Public License as
92+# published by the Free Software Foundation, either version 3 of the
93+# License, or (at your option) any later version.
94+#
95+# This program is distributed in the hope that it will be useful,
96+# but WITHOUT ANY WARRANTY; without even the implied warranty of
97+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
98+# GNU Affero General Public License for more details.
99+#
100+# You should have received a copy of the GNU Affero General Public License
101+# along with this program. If not, see <http://www.gnu.org/licenses/>.
102+#
103+##############################################################################
104+
105+from openerp.osv import orm, fields
106+from openerp.tools.translate import _
107+
108+
109+class CleanupPurgeLineColumn(orm.TransientModel):
110+ _inherit = 'cleanup.purge.line'
111+ _name = 'cleanup.purge.line.column'
112+
113+ _columns = {
114+ 'model_id': fields.many2one(
115+ 'ir.model', 'Model',
116+ required=True, ondelete='CASCADE'),
117+ 'wizard_id': fields.many2one(
118+ 'cleanup.purge.wizard.column', 'Purge Wizard', readonly=True),
119+ }
120+
121+ def purge(self, cr, uid, ids, context=None):
122+ """
123+ Unlink columns upon manual confirmation.
124+ """
125+ for line in self.browse(cr, uid, ids, context=context):
126+ if line.purged:
127+ continue
128+
129+ model_pool = self.pool[line.model_id.model]
130+
131+ # Check whether the column actually still exists.
132+ # Inheritance such as stock.picking.in from stock.picking
133+ # can lead to double attempts at removal
134+ cr.execute(
135+ 'SELECT count(attname) FROM pg_attribute '
136+ 'WHERE attrelid = '
137+ '( SELECT oid FROM pg_class WHERE relname = %s ) '
138+ 'AND attname = %s',
139+ (model_pool._table, line.name))
140+ if not cr.fetchone()[0]:
141+ continue
142+
143+ self.logger.info(
144+ 'Dropping column %s from table %s',
145+ line.name, model_pool._table)
146+ cr.execute(
147+ """
148+ ALTER TABLE "%s" DROP COLUMN "%s"
149+ """ % (model_pool._table, line.name))
150+ line.write({'purged': True})
151+ cr.commit()
152+ return True
153+
154+
155+class CleanupPurgeWizardColumn(orm.TransientModel):
156+ _inherit = 'cleanup.purge.wizard'
157+ _name = 'cleanup.purge.wizard.column'
158+
159+ # List of known columns in use without corresponding fields
160+ # Format: {table: [fields]}
161+ blacklist = {
162+ 'wkf_instance': ['uid'], # lp:1277899
163+ }
164+
165+ def default_get(self, cr, uid, fields, context=None):
166+ res = super(CleanupPurgeWizardColumn, self).default_get(
167+ cr, uid, fields, context=context)
168+ if 'name' in fields:
169+ res['name'] = _('Purge columns')
170+ return res
171+
172+ def get_orphaned_columns(self, cr, uid, model_pools, context=None):
173+ """
174+ From openobject-server/openerp/osv/orm.py
175+ Iterate on the database columns to identify columns
176+ of fields which have been removed
177+ """
178+
179+ columns = list(set([
180+ column for model_pool in model_pools
181+ for column in model_pool._columns
182+ if not (isinstance(model_pool._columns[column], fields.function)
183+ and not model_pool._columns[column].store)
184+ ]))
185+ columns += orm.MAGIC_COLUMNS
186+ columns += self.blacklist.get(model_pools[0]._table, [])
187+
188+ cr.execute("SELECT a.attname"
189+ " FROM pg_class c, pg_attribute a"
190+ " WHERE c.relname=%s"
191+ " AND c.oid=a.attrelid"
192+ " AND a.attisdropped=%s"
193+ " AND pg_catalog.format_type(a.atttypid, a.atttypmod)"
194+ " NOT IN ('cid', 'tid', 'oid', 'xid')"
195+ " AND a.attname NOT IN %s",
196+ (model_pools[0]._table, False, tuple(columns))),
197+ return [column[0] for column in cr.fetchall()]
198+
199+ def find(self, cr, uid, context=None):
200+ """
201+ Search for columns that are not in the corresponding model.
202+
203+ Group models by table to prevent false positives for columns
204+ that are only in some of the models sharing the same table.
205+ Example of this is 'sale_id' not being a field of stock.picking.in
206+ """
207+ res = []
208+ model_pool = self.pool['ir.model']
209+ model_ids = model_pool.search(cr, uid, [], context=context)
210+
211+ # mapping of tables to tuples (model id, [pool1, pool2, ...])
212+ table2model = {}
213+
214+ for model in model_pool.browse(cr, uid, model_ids, context=context):
215+ model_pool = self.pool.get(model.model)
216+ if not model_pool or not model_pool._auto:
217+ continue
218+ table2model.setdefault(
219+ model_pool._table, (model.id, []))[1].append(model_pool)
220+
221+ for table, model_spec in table2model.iteritems():
222+ for column in self.get_orphaned_columns(
223+ cr, uid, model_spec[1], context=context):
224+ res.append((0, 0, {
225+ 'name': column,
226+ 'model_id': model_spec[0]}))
227+ if not res:
228+ raise orm.except_orm(
229+ _('Nothing to do'),
230+ _('No orphaned columns found'))
231+ return res
232+
233+ _columns = {
234+ 'purge_line_ids': fields.one2many(
235+ 'cleanup.purge.line.column',
236+ 'wizard_id', 'Columns to purge'),
237+ }
238
239=== added file 'database_cleanup/model/purge_data.py'
240--- database_cleanup/model/purge_data.py 1970-01-01 00:00:00 +0000
241+++ database_cleanup/model/purge_data.py 2014-03-14 13:27:36 +0000
242@@ -0,0 +1,106 @@
243+# -*- coding: utf-8 -*-
244+##############################################################################
245+#
246+# OpenERP, Open Source Management Solution
247+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
248+#
249+# This program is free software: you can redistribute it and/or modify
250+# it under the terms of the GNU Affero General Public License as
251+# published by the Free Software Foundation, either version 3 of the
252+# License, or (at your option) any later version.
253+#
254+# This program is distributed in the hope that it will be useful,
255+# but WITHOUT ANY WARRANTY; without even the implied warranty of
256+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
257+# GNU Affero General Public License for more details.
258+#
259+# You should have received a copy of the GNU Affero General Public License
260+# along with this program. If not, see <http://www.gnu.org/licenses/>.
261+#
262+##############################################################################
263+
264+from openerp.osv import orm, fields
265+from openerp.tools.translate import _
266+
267+
268+class CleanupPurgeLineData(orm.TransientModel):
269+ _inherit = 'cleanup.purge.line'
270+ _name = 'cleanup.purge.line.data'
271+
272+ _columns = {
273+ 'data_id': fields.many2one(
274+ 'ir.model.data', 'Data entry',
275+ ondelete='SET NULL'),
276+ 'wizard_id': fields.many2one(
277+ 'cleanup.purge.wizard.data', 'Purge Wizard', readonly=True),
278+ }
279+
280+ def purge(self, cr, uid, ids, context=None):
281+ """
282+ Unlink data entries upon manual confirmation.
283+ """
284+ data_ids = []
285+ for line in self.browse(cr, uid, ids, context=context):
286+ if line.purged or not line.data_id:
287+ continue
288+ data_ids.append(line.data_id.id)
289+ self.logger.info('Purging data entry: %s', line.name)
290+ self.pool['ir.model.data'].unlink(cr, uid, data_ids, context=context)
291+ return self.write(cr, uid, ids, {'purged': True}, context=context)
292+
293+
294+class CleanupPurgeWizardData(orm.TransientModel):
295+ _inherit = 'cleanup.purge.wizard'
296+ _name = 'cleanup.purge.wizard.data'
297+
298+ def default_get(self, cr, uid, fields, context=None):
299+ res = super(CleanupPurgeWizardData, self).default_get(
300+ cr, uid, fields, context=context)
301+ if 'name' in fields:
302+ res['name'] = _('Purge data')
303+ return res
304+
305+ def find(self, cr, uid, context=None):
306+ """
307+ Collect all rows from ir_model_data that refer
308+ to a nonexisting model, or to a nonexisting
309+ row in the model's table.
310+ """
311+ res = []
312+ data_pool = self.pool['ir.model.data']
313+ data_ids = []
314+ unknown_models = []
315+ cr.execute("""SELECT DISTINCT(model) FROM ir_model_data""")
316+ for (model,) in cr.fetchall():
317+ if not model:
318+ continue
319+ if not self.pool.get(model):
320+ unknown_models.append(model)
321+ continue
322+ cr.execute(
323+ """
324+ SELECT id FROM ir_model_data
325+ WHERE model = %%s
326+ AND res_id IS NOT NULL
327+ AND res_id NOT IN (
328+ SELECT id FROM %s)
329+ """ % self.pool[model]._table, (model,))
330+ data_ids += [data_row[0] for data_row in cr.fetchall()]
331+ data_ids += data_pool.search(
332+ cr, uid, [('model', 'in', unknown_models)], context=context)
333+ for data in data_pool.browse(cr, uid, data_ids, context=context):
334+ res.append((0, 0, {
335+ 'data_id': data.id,
336+ 'name': "%s.%s, object of type %s" % (
337+ data.module, data.name, data.model)}))
338+ if not res:
339+ raise orm.except_orm(
340+ _('Nothing to do'),
341+ _('No orphaned data entries found'))
342+ return res
343+
344+ _columns = {
345+ 'purge_line_ids': fields.one2many(
346+ 'cleanup.purge.line.data',
347+ 'wizard_id', 'Data to purge'),
348+ }
349
350=== added file 'database_cleanup/model/purge_models.py'
351--- database_cleanup/model/purge_models.py 1970-01-01 00:00:00 +0000
352+++ database_cleanup/model/purge_models.py 2014-03-14 13:27:36 +0000
353@@ -0,0 +1,127 @@
354+# -*- coding: utf-8 -*-
355+##############################################################################
356+#
357+# OpenERP, Open Source Management Solution
358+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
359+#
360+# This program is free software: you can redistribute it and/or modify
361+# it under the terms of the GNU Affero General Public License as
362+# published by the Free Software Foundation, either version 3 of the
363+# License, or (at your option) any later version.
364+#
365+# This program is distributed in the hope that it will be useful,
366+# but WITHOUT ANY WARRANTY; without even the implied warranty of
367+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
368+# GNU Affero General Public License for more details.
369+#
370+# You should have received a copy of the GNU Affero General Public License
371+# along with this program. If not, see <http://www.gnu.org/licenses/>.
372+#
373+##############################################################################
374+
375+from openerp.osv import orm, fields
376+from openerp.tools.translate import _
377+from openerp.addons.base.ir.ir_model import MODULE_UNINSTALL_FLAG
378+
379+
380+class IrModel(orm.Model):
381+ _inherit = 'ir.model'
382+
383+ def _drop_table(self, cr, uid, ids, context=None):
384+ # Allow to skip this step during model unlink
385+ # The super method crashes if the model cannot be instantiated
386+ if context and context.get('no_drop_table'):
387+ return True
388+ return super(IrModel, self)._drop_table(cr, uid, ids, context=context)
389+
390+
391+class CleanupPurgeLineModel(orm.TransientModel):
392+ _inherit = 'cleanup.purge.line'
393+ _name = 'cleanup.purge.line.model'
394+
395+ _columns = {
396+ 'wizard_id': fields.many2one(
397+ 'cleanup.purge.wizard.model', 'Purge Wizard', readonly=True),
398+ }
399+
400+ def purge(self, cr, uid, ids, context=None):
401+ """
402+ Unlink models upon manual confirmation.
403+ """
404+ model_pool = self.pool['ir.model']
405+ attachment_pool = self.pool['ir.attachment']
406+ constraint_pool = self.pool['ir.model.constraint']
407+ fields_pool = self.pool['ir.model.fields']
408+
409+ local_context = (context or {}).copy()
410+ local_context.update({
411+ MODULE_UNINSTALL_FLAG: True,
412+ 'no_drop_table': True,
413+ })
414+
415+ for line in self.browse(cr, uid, ids, context=context):
416+ cr.execute(
417+ "SELECT id, model from ir_model WHERE model = %s",
418+ (line.name,))
419+ row = cr.fetchone()
420+ if row:
421+ self.logger.info('Purging model %s', row[1])
422+ attachment_ids = attachment_pool.search(
423+ cr, uid, [('res_model', '=', line.name)], context=context)
424+ if attachment_ids:
425+ cr.execute(
426+ "UPDATE ir_attachment SET res_model = FALSE "
427+ "WHERE id in %s",
428+ (tuple(attachment_ids), ))
429+ constraint_ids = constraint_pool.search(
430+ cr, uid, [('model', '=', line.name)], context=context)
431+ if constraint_ids:
432+ constraint_pool.unlink(
433+ cr, uid, constraint_ids, context=context)
434+ relation_ids = fields_pool.search(
435+ cr, uid, [('relation', '=', row[1])], context=context)
436+ for relation in relation_ids:
437+ try:
438+ # Fails if the model on the target side
439+ # cannot be instantiated
440+ fields_pool.unlink(cr, uid, [relation],
441+ context=local_context)
442+ except AttributeError:
443+ pass
444+ model_pool.unlink(cr, uid, [row[0]], context=local_context)
445+ line.write({'purged': True})
446+ cr.commit()
447+ return True
448+
449+
450+class CleanupPurgeWizardModel(orm.TransientModel):
451+ _inherit = 'cleanup.purge.wizard'
452+ _name = 'cleanup.purge.wizard.model'
453+
454+ def default_get(self, cr, uid, fields, context=None):
455+ res = super(CleanupPurgeWizardModel, self).default_get(
456+ cr, uid, fields, context=context)
457+ if 'name' in fields:
458+ res['name'] = _('Purge models')
459+ return res
460+
461+ def find(self, cr, uid, context=None):
462+ """
463+ Search for models that cannot be instantiated.
464+ """
465+ res = []
466+ cr.execute("SELECT model from ir_model")
467+ for (model,) in cr.fetchall():
468+ if not self.pool.get(model):
469+ res.append((0, 0, {'name': model}))
470+ if not res:
471+ raise orm.except_orm(
472+ _('Nothing to do'),
473+ _('No orphaned models found'))
474+ return res
475+
476+ _columns = {
477+ 'purge_line_ids': fields.one2many(
478+ 'cleanup.purge.line.model',
479+ 'wizard_id', 'Models to purge'),
480+ }
481
482=== added file 'database_cleanup/model/purge_modules.py'
483--- database_cleanup/model/purge_modules.py 1970-01-01 00:00:00 +0000
484+++ database_cleanup/model/purge_modules.py 2014-03-14 13:27:36 +0000
485@@ -0,0 +1,91 @@
486+# -*- coding: utf-8 -*-
487+##############################################################################
488+#
489+# OpenERP, Open Source Management Solution
490+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
491+#
492+# This program is free software: you can redistribute it and/or modify
493+# it under the terms of the GNU Affero General Public License as
494+# published by the Free Software Foundation, either version 3 of the
495+# License, or (at your option) any later version.
496+#
497+# This program is distributed in the hope that it will be useful,
498+# but WITHOUT ANY WARRANTY; without even the implied warranty of
499+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
500+# GNU Affero General Public License for more details.
501+#
502+# You should have received a copy of the GNU Affero General Public License
503+# along with this program. If not, see <http://www.gnu.org/licenses/>.
504+#
505+##############################################################################
506+
507+from openerp import pooler
508+from openerp.osv import orm, fields
509+from openerp.modules.module import get_module_path
510+from openerp.tools.translate import _
511+
512+
513+class CleanupPurgeLineModule(orm.TransientModel):
514+ _inherit = 'cleanup.purge.line'
515+ _name = 'cleanup.purge.line.module'
516+
517+ _columns = {
518+ 'wizard_id': fields.many2one(
519+ 'cleanup.purge.wizard.module', 'Purge Wizard', readonly=True),
520+ }
521+
522+ def purge(self, cr, uid, ids, context=None):
523+ """
524+ Uninstall modules upon manual confirmation, then reload
525+ the database.
526+ """
527+ module_pool = self.pool['ir.module.module']
528+ lines = self.browse(cr, uid, ids, context=context)
529+ module_names = [line.name for line in lines if not line.purged]
530+ module_ids = module_pool.search(
531+ cr, uid, [('name', 'in', module_names)], context=context)
532+ if not module_ids:
533+ return True
534+ self.logger.info('Purging modules %s', ', '.join(module_names))
535+ module_pool.write(
536+ cr, uid, module_ids, {'state': 'to remove'}, context=context)
537+ cr.commit()
538+ _db, _pool = pooler.restart_pool(cr.dbname, update_module=True)
539+ module_pool.unlink(cr, uid, module_ids, context=context)
540+ return self.write(cr, uid, ids, {'purged': True}, context=context)
541+
542+
543+class CleanupPurgeWizardModule(orm.TransientModel):
544+ _inherit = 'cleanup.purge.wizard'
545+ _name = 'cleanup.purge.wizard.module'
546+
547+ def default_get(self, cr, uid, fields, context=None):
548+ res = super(CleanupPurgeWizardModule, self).default_get(
549+ cr, uid, fields, context=context)
550+ if 'name' in fields:
551+ res['name'] = _('Purge modules')
552+ return res
553+
554+ def find(self, cr, uid, context=None):
555+ module_pool = self.pool['ir.module.module']
556+ module_ids = module_pool.search(cr, uid, [], context=context)
557+ res = []
558+ for module in module_pool.browse(cr, uid, module_ids, context=context):
559+ if get_module_path(module.name):
560+ continue
561+ if module.state == 'uninstalled':
562+ module_pool.unlink(cr, uid, module.id, context=context)
563+ continue
564+ res.append((0, 0, {'name': module.name}))
565+
566+ if not res:
567+ raise orm.except_orm(
568+ _('Nothing to do'),
569+ _('No modules found to purge'))
570+ return res
571+
572+ _columns = {
573+ 'purge_line_ids': fields.one2many(
574+ 'cleanup.purge.line.module',
575+ 'wizard_id', 'Modules to purge'),
576+ }
577
578=== added file 'database_cleanup/model/purge_tables.py'
579--- database_cleanup/model/purge_tables.py 1970-01-01 00:00:00 +0000
580+++ database_cleanup/model/purge_tables.py 2014-03-14 13:27:36 +0000
581@@ -0,0 +1,138 @@
582+# -*- coding: utf-8 -*-
583+##############################################################################
584+#
585+# OpenERP, Open Source Management Solution
586+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
587+#
588+# This program is free software: you can redistribute it and/or modify
589+# it under the terms of the GNU Affero General Public License as
590+# published by the Free Software Foundation, either version 3 of the
591+# License, or (at your option) any later version.
592+#
593+# This program is distributed in the hope that it will be useful,
594+# but WITHOUT ANY WARRANTY; without even the implied warranty of
595+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
596+# GNU Affero General Public License for more details.
597+#
598+# You should have received a copy of the GNU Affero General Public License
599+# along with this program. If not, see <http://www.gnu.org/licenses/>.
600+#
601+##############################################################################
602+
603+from openerp.osv import orm, fields
604+from openerp.tools.translate import _
605+
606+
607+class CleanupPurgeLineTable(orm.TransientModel):
608+ _inherit = 'cleanup.purge.line'
609+ _name = 'cleanup.purge.line.table'
610+
611+ _columns = {
612+ 'wizard_id': fields.many2one(
613+ 'cleanup.purge.wizard.table', 'Purge Wizard', readonly=True),
614+ }
615+
616+ def purge(self, cr, uid, ids, context=None):
617+ """
618+ Unlink tables upon manual confirmation.
619+ """
620+ lines = self.browse(cr, uid, ids, context=context)
621+ tables = [line.name for line in lines]
622+ for line in lines:
623+ if line.purged:
624+ continue
625+
626+ # Retrieve constraints on the tables to be dropped
627+ # This query is referenced in numerous places
628+ # on the Internet but credits probably go to Tom Lane
629+ # in this post http://www.postgresql.org/\
630+ # message-id/22895.1226088573@sss.pgh.pa.us
631+ # Only using the constraint name and the source table,
632+ # but I'm leaving the rest in for easier debugging
633+ cr.execute(
634+ """
635+ SELECT conname, confrelid::regclass, af.attname AS fcol,
636+ conrelid::regclass, a.attname AS col
637+ FROM pg_attribute af, pg_attribute a,
638+ (SELECT conname, conrelid, confrelid,conkey[i] AS conkey,
639+ confkey[i] AS confkey
640+ FROM (select conname, conrelid, confrelid, conkey,
641+ confkey, generate_series(1,array_upper(conkey,1)) AS i
642+ FROM pg_constraint WHERE contype = 'f') ss) ss2
643+ WHERE af.attnum = confkey AND af.attrelid = confrelid AND
644+ a.attnum = conkey AND a.attrelid = conrelid
645+ AND confrelid::regclass = '%s'::regclass;
646+ """ % line.name)
647+
648+ for constraint in cr.fetchall():
649+ if constraint[3] in tables:
650+ self.logger.info(
651+ 'Dropping constraint %s on table %s (to be dropped)',
652+ constraint[0], constraint[3])
653+ cr.execute(
654+ "ALTER TABLE %s DROP CONSTRAINT %s" % (
655+ constraint[3], constraint[0]))
656+
657+ self.logger.info(
658+ 'Dropping table %s', line.name)
659+ cr.execute("DROP TABLE \"%s\"" % (line.name,))
660+ line.write({'purged': True})
661+ cr.commit()
662+ return True
663+
664+
665+class CleanupPurgeWizardTable(orm.TransientModel):
666+ _inherit = 'cleanup.purge.wizard'
667+ _name = 'cleanup.purge.wizard.table'
668+
669+ def default_get(self, cr, uid, fields, context=None):
670+ res = super(CleanupPurgeWizardTable, self).default_get(
671+ cr, uid, fields, context=context)
672+ if 'name' in fields:
673+ res['name'] = _('Purge tables')
674+ return res
675+
676+ def find(self, cr, uid, context=None):
677+ """
678+ Search for tables that cannot be instantiated.
679+ Ignore views for now.
680+ """
681+ model_ids = self.pool['ir.model'].search(cr, uid, [], context=context)
682+ # Start out with known tables with no model
683+ known_tables = ['wkf_witm_trans']
684+ for model in self.pool['ir.model'].browse(
685+ cr, uid, model_ids, context=context):
686+
687+ model_pool = self.pool.get(model.model)
688+ if not model_pool:
689+ continue
690+ known_tables.append(model_pool._table)
691+ known_tables += [
692+ column._sql_names(model_pool)[0]
693+ for column in model_pool._columns.values()
694+ if column._type == 'many2many'
695+ # unstored function fields of type m2m don't have _rel
696+ and hasattr(column, '_rel')
697+ ]
698+
699+ # Cannot pass table names as a psycopg argument
700+ known_tables_repr = ",".join(
701+ [("'%s'" % table) for table in known_tables])
702+ cr.execute(
703+ """
704+ SELECT table_name FROM information_schema.tables
705+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
706+ AND table_name NOT IN (%s)""" % known_tables_repr)
707+
708+ res = [(0, 0, {'name': row[0]}) for row in cr.fetchall()]
709+ if not res:
710+ raise orm.except_orm(
711+ _('Nothing to do'),
712+ _('No orphaned tables found'))
713+ return res
714+
715+ _columns = {
716+ 'purge_line_ids': fields.one2many(
717+ 'cleanup.purge.line.table',
718+ 'wizard_id', 'Tables to purge'),
719+ }
720
721=== added file 'database_cleanup/model/purge_wizard.py'
722--- database_cleanup/model/purge_wizard.py 1970-01-01 00:00:00 +0000
723+++ database_cleanup/model/purge_wizard.py 2014-03-14 13:27:36 +0000
724@@ -0,0 +1,64 @@
725+# -*- coding: utf-8 -*-
726+##############################################################################
727+#
728+# OpenERP, Open Source Management Solution
729+# This module copyright (C) 2014 Therp BV (<http://therp.nl>).
730+#
731+# This program is free software: you can redistribute it and/or modify
732+# it under the terms of the GNU Affero General Public License as
733+# published by the Free Software Foundation, either version 3 of the
734+# License, or (at your option) any later version.
735+#
736+# This program is distributed in the hope that it will be useful,
737+# but WITHOUT ANY WARRANTY; without even the implied warranty of
738+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
739+# GNU Affero General Public License for more details.
740+#
741+# You should have received a copy of the GNU Affero General Public License
742+# along with this program. If not, see <http://www.gnu.org/licenses/>.
743+#
744+##############################################################################
745+
746+import logging
747+from openerp.osv import orm, fields
748+
749+
750+class CleanupPurgeLine(orm.AbstractModel):
751+ """ Abstract base class for the purge wizard lines """
752+ _name = 'cleanup.purge.line'
753+ _columns = {
754+ 'name': fields.char('Name', size=256, readonly=True),
755+ 'purged': fields.boolean('Purged', readonly=True),
756+ }
757+
758+ logger = logging.getLogger('openerp.addons.database_cleanup')
759+
760+ def purge(self, cr, uid, ids, context=None):
761+ raise NotImplementedError
762+
763+
764+class PurgeWizard(orm.AbstractModel):
765+ """ Abstract base class for the purge wizards """
766+ _name = 'cleanup.purge.wizard'
767+
768+ def default_get(self, cr, uid, fields, context=None):
769+ res = super(PurgeWizard, self).default_get(
770+ cr, uid, fields, context=context)
771+ if 'purge_line_ids' in fields:
772+ res['purge_line_ids'] = self.find(cr, uid, context=None)
773+ return res
774+
775+ def find(self, cr, uid, ids, context=None):
776+ raise NotImplementedError
777+
778+ def purge_all(self, cr, uid, ids, context=None):
779+ line_pool = self.pool[self._columns['purge_line_ids']._obj]
780+ for wizard in self.browse(cr, uid, ids, context=context):
781+ line_pool.purge(
782+ cr, uid, [line.id for line in wizard.purge_line_ids],
783+ context=context)
784+ return True
785+
786+ _columns = {
787+ 'name': fields.char('Name', size=64, readonly=True),
788+ }
789
790=== added directory 'database_cleanup/static'
791=== added directory 'database_cleanup/static/src'
792=== added directory 'database_cleanup/static/src/img'
793=== added file 'database_cleanup/static/src/img/icon.png'
794Binary files database_cleanup/static/src/img/icon.png 1970-01-01 00:00:00 +0000 and database_cleanup/static/src/img/icon.png 2014-03-14 13:27:36 +0000 differ
795=== added directory 'database_cleanup/view'
796=== added file 'database_cleanup/view/menu.xml'
797--- database_cleanup/view/menu.xml 1970-01-01 00:00:00 +0000
798+++ database_cleanup/view/menu.xml 2014-03-14 13:27:36 +0000
799@@ -0,0 +1,48 @@
800+<?xml version="1.0" encoding="utf-8"?>
801+<openerp>
802+ <data>
803+
804+ <record model="ir.ui.menu" id="menu_database_cleanup">
805+ <field name="name">Database cleanup</field>
806+ <field name="sequence" eval="10" />
807+ <!-- attach to Settings -> Technical -->
808+ <field name="parent_id" ref="base.menu_custom"/>
809+ </record>
810+
811+ <record model="ir.ui.menu" id="menu_purge_modules">
812+ <field name="name">Purge obsolete modules</field>
813+ <field name="sequence" eval="10" />
814+ <field name="action" ref="action_purge_modules" />
815+ <field name="parent_id" ref="menu_database_cleanup"/>
816+ </record>
817+
818+ <record model="ir.ui.menu" id="menu_purge_models">
819+ <field name="name">Purge obsolete models</field>
820+ <field name="sequence" eval="20" />
821+ <field name="action" ref="action_purge_models" />
822+ <field name="parent_id" ref="menu_database_cleanup"/>
823+ </record>
824+
825+ <record model="ir.ui.menu" id="menu_purge_columns">
826+ <field name="name">Purge obsolete columns</field>
827+ <field name="sequence" eval="30" />
828+ <field name="action" ref="action_purge_columns" />
829+ <field name="parent_id" ref="menu_database_cleanup"/>
830+ </record>
831+
832+ <record model="ir.ui.menu" id="menu_purge_tables">
833+ <field name="name">Purge obsolete tables</field>
834+ <field name="sequence" eval="40" />
835+ <field name="action" ref="action_purge_tables" />
836+ <field name="parent_id" ref="menu_database_cleanup"/>
837+ </record>
838+
839+ <record model="ir.ui.menu" id="menu_purge_data">
840+ <field name="name">Purge obsolete data entries</field>
841+ <field name="sequence" eval="50" />
842+ <field name="action" ref="action_purge_data" />
843+ <field name="parent_id" ref="menu_database_cleanup"/>
844+ </record>
845+
846+ </data>
847+</openerp>
848
849=== added file 'database_cleanup/view/purge_columns.xml'
850--- database_cleanup/view/purge_columns.xml 1970-01-01 00:00:00 +0000
851+++ database_cleanup/view/purge_columns.xml 2014-03-14 13:27:36 +0000
852@@ -0,0 +1,37 @@
853+<?xml version="1.0" encoding="utf-8"?>
854+<openerp>
855+ <data>
856+
857+ <record id="purge_columns_view" model="ir.ui.view">
858+ <field name="name">Form view for purge columns wizard</field>
859+ <field name="model">cleanup.purge.wizard.column</field>
860+ <field name="arch" type="xml">
861+ <form string="Purge columns" version="7.0">
862+ <h1>
863+ <field name="name"/>
864+ </h1>
865+ <button type="object" name="purge_all" string="Purge all columns" />
866+ <field name="purge_line_ids" colspan="4" nolabel="1">
867+ <tree string="Purge columns">
868+ <field name="name" />
869+ <field name="model_id" />
870+ <field name="purged" invisible="0" />
871+ <button type="object" name="purge"
872+ icon="gtk-cancel" string="Purge this column"
873+ attrs="{'invisible': [('purged', '=', True)]}"/>
874+ </tree>
875+ </field>
876+ </form>
877+ </field>
878+ </record>
879+
880+ <record id="action_purge_columns" model="ir.actions.act_window">
881+ <field name="name">Purge columns</field>
882+ <field name="type">ir.actions.act_window</field>
883+ <field name="res_model">cleanup.purge.wizard.column</field>
884+ <field name="view_type">form</field>
885+ <field name="view_mode">form</field>
886+ </record>
887+
888+ </data>
889+</openerp>
890
891=== added file 'database_cleanup/view/purge_data.xml'
892--- database_cleanup/view/purge_data.xml 1970-01-01 00:00:00 +0000
893+++ database_cleanup/view/purge_data.xml 2014-03-14 13:27:36 +0000
894@@ -0,0 +1,37 @@
895+<?xml version="1.0" encoding="utf-8"?>
896+<openerp>
897+ <data>
898+
899+ <record id="purge_data_view" model="ir.ui.view">
900+ <field name="name">Form view for purge data wizard</field>
901+ <field name="model">cleanup.purge.wizard.data</field>
902+ <field name="arch" type="xml">
903+ <form string="Purge data entries that refer to missing resources" version="7.0">
904+ <h1>
905+ <field name="name"/>
906+ </h1>
907+ <button type="object" name="purge_all" string="Purge all data" />
908+ <field name="purge_line_ids" colspan="4" nolabel="1">
909+ <tree string="Purge data">
910+ <field name="name" />
911+ <field name="data_id" />
912+ <field name="purged" invisible="0" />
913+ <button type="object" name="purge"
914+ icon="gtk-cancel" string="Purge this data"
915+ attrs="{'invisible': [('purged', '=', True)]}"/>
916+ </tree>
917+ </field>
918+ </form>
919+ </field>
920+ </record>
921+
922+ <record id="action_purge_data" model="ir.actions.act_window">
923+ <field name="name">Purge data entries that refer to missing resources</field>
924+ <field name="type">ir.actions.act_window</field>
925+ <field name="res_model">cleanup.purge.wizard.data</field>
926+ <field name="view_type">form</field>
927+ <field name="view_mode">form</field>
928+ </record>
929+
930+ </data>
931+</openerp>
932
933=== added file 'database_cleanup/view/purge_models.xml'
934--- database_cleanup/view/purge_models.xml 1970-01-01 00:00:00 +0000
935+++ database_cleanup/view/purge_models.xml 2014-03-14 13:27:36 +0000
936@@ -0,0 +1,36 @@
937+<?xml version="1.0" encoding="utf-8"?>
938+<openerp>
939+ <data>
940+
941+ <record id="purge_models_view" model="ir.ui.view">
942+ <field name="name">Form view for purge models wizard</field>
943+ <field name="model">cleanup.purge.wizard.model</field>
944+ <field name="arch" type="xml">
945+ <form string="Purge models" version="7.0">
946+ <h1>
947+ <field name="name"/>
948+ </h1>
949+ <button type="object" name="purge_all" string="Purge all models" />
950+ <field name="purge_line_ids" colspan="4" nolabel="1">
951+ <tree string="Purge models">
952+ <field name="name" />
953+ <field name="purged" invisible="0" />
954+ <button type="object" name="purge"
955+ icon="gtk-cancel" string="Purge this model"
956+ attrs="{'invisible': [('purged', '=', True)]}"/>
957+ </tree>
958+ </field>
959+ </form>
960+ </field>
961+ </record>
962+
963+ <record id="action_purge_models" model="ir.actions.act_window">
964+ <field name="name">Purge models</field>
965+ <field name="type">ir.actions.act_window</field>
966+ <field name="res_model">cleanup.purge.wizard.model</field>
967+ <field name="view_type">form</field>
968+ <field name="view_mode">form</field>
969+ </record>
970+
971+ </data>
972+</openerp>
973
974=== added file 'database_cleanup/view/purge_modules.xml'
975--- database_cleanup/view/purge_modules.xml 1970-01-01 00:00:00 +0000
976+++ database_cleanup/view/purge_modules.xml 2014-03-14 13:27:36 +0000
977@@ -0,0 +1,36 @@
978+<?xml version="1.0" encoding="utf-8"?>
979+<openerp>
980+ <data>
981+
982+ <record id="purge_modules_view" model="ir.ui.view">
983+ <field name="name">Form view for purge modules wizard</field>
984+ <field name="model">cleanup.purge.wizard.module</field>
985+ <field name="arch" type="xml">
986+ <form string="Purge modules" version="7.0">
987+ <h1>
988+ <field name="name"/>
989+ </h1>
990+ <button type="object" name="purge_all" string="Purge all modules" />
991+ <field name="purge_line_ids" colspan="4" nolabel="1">
992+ <tree string="Purge modules">
993+ <field name="name" />
994+ <field name="purged" invisible="0" />
995+ <button type="object" name="purge"
996+ icon="gtk-cancel" string="Purge this module"
997+ attrs="{'invisible': [('purged', '=', True)]}"/>
998+ </tree>
999+ </field>
1000+ </form>
1001+ </field>
1002+ </record>
1003+
1004+ <record id="action_purge_modules" model="ir.actions.act_window">
1005+ <field name="name">Purge modules</field>
1006+ <field name="type">ir.actions.act_window</field>
1007+ <field name="res_model">cleanup.purge.wizard.module</field>
1008+ <field name="view_type">form</field>
1009+ <field name="view_mode">form</field>
1010+ </record>
1011+
1012+ </data>
1013+</openerp>
1014
1015=== added file 'database_cleanup/view/purge_tables.xml'
1016--- database_cleanup/view/purge_tables.xml 1970-01-01 00:00:00 +0000
1017+++ database_cleanup/view/purge_tables.xml 2014-03-14 13:27:36 +0000
1018@@ -0,0 +1,36 @@
1019+<?xml version="1.0" encoding="utf-8"?>
1020+<openerp>
1021+ <data>
1022+
1023+ <record id="purge_tables_view" model="ir.ui.view">
1024+ <field name="name">Form view for purge tables wizard</field>
1025+ <field name="model">cleanup.purge.wizard.table</field>
1026+ <field name="arch" type="xml">
1027+ <form string="Purge tables" version="7.0">
1028+ <h1>
1029+ <field name="name"/>
1030+ </h1>
1031+ <button type="object" name="purge_all" string="Purge all tables" />
1032+ <field name="purge_line_ids" colspan="4" nolabel="1">
1033+ <tree string="Purge tables">
1034+ <field name="name" />
1035+ <field name="purged" invisible="0" />
1036+ <button type="object" name="purge"
1037+ icon="gtk-cancel" string="Purge this table"
1038+ attrs="{'invisible': [('purged', '=', True)]}"/>
1039+ </tree>
1040+ </field>
1041+ </form>
1042+ </field>
1043+ </record>
1044+
1045+ <record id="action_purge_tables" model="ir.actions.act_window">
1046+ <field name="name">Purge tables</field>
1047+ <field name="type">ir.actions.act_window</field>
1048+ <field name="res_model">cleanup.purge.wizard.table</field>
1049+ <field name="view_type">form</field>
1050+ <field name="view_mode">form</field>
1051+ </record>
1052+
1053+ </data>
1054+</openerp>

Subscribers

People subscribed via source and target branches