Merge lp:~0k.io/openupgrade-server/8.0-openupgrade-base-2 into lp:~openupgrade-committers/openupgrade-server/8.0

Proposed by Valentin Lab
Status: Merged
Merged at revision: 5144
Proposed branch: lp:~0k.io/openupgrade-server/8.0-openupgrade-base-2
Merge into: lp:~openupgrade-committers/openupgrade-server/8.0
Diff against target: 1605 lines (+1301/-22)
13 files modified
openerp/addons/base/ir/ir_model.py (+28/-1)
openerp/addons/base/res/res_currency.py (+11/-3)
openerp/modules/graph.py (+11/-0)
openerp/modules/loading.py (+37/-10)
openerp/modules/migration.py (+6/-4)
openerp/openupgrade/openupgrade.py (+479/-0)
openerp/openupgrade/openupgrade_loading.py (+185/-0)
openerp/openupgrade/openupgrade_log.py (+56/-0)
openerp/openupgrade/openupgrade_tools.py (+8/-0)
openerp/osv/orm.py (+15/-4)
openerp/tools/convert.py (+3/-0)
scripts/compare_noupdate_xml_records.py (+201/-0)
scripts/migrate.py (+261/-0)
To merge this branch: bzr merge lp:~0k.io/openupgrade-server/8.0-openupgrade-base-2
Reviewer Review Type Date Requested Status
Stefan Rijnhart (Opener) Approve
Review via email: mp+214770@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

Thanks! This pretty much what I would expect, except for the following:

1) I seem to miss the part in openerp/modules/loading.py that is marked in 7.0 with the following comment

     # OpenUpgrade: Loop until no modules are processed

Is this change obsolete?

2) Maybe keep the references to deferred_70, change them to deferred_80 and add a void function migrate_deferred() for further use
3) I think I mentioned before that we would not need all the 7.0 related stuff, but for the docs we actually need to keep openerp/openupgrade/openupgrade_70.py if that is at all possible.
4) There is a conflict in adding openerp/openupgrade because that directory is now already there for the docs. Can you solve the conflict by adding the new files in this directory to the existing one?

review: Needs Fixing
Revision history for this message
Stefan Rijnhart (Opener) (stefan-opener) wrote :

To answer my own question (1): Olivier has adopted a similar change further down the openobject-server 7.0 lifecycle:
http://bazaar.launchpad.net/~openerp/openobject-server/7.0/revision/5071

I've merged this, resolving the conflict. My suggestions (2) and (3) are now in the following proposal: https://code.launchpad.net/~therp-nl/openupgrade-server/8.0-openupgrade_core_style/+merge/218285

Thanks again!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'openerp/addons/base/ir/ir_model.py'
2--- openerp/addons/base/ir/ir_model.py 2014-03-12 18:06:14 +0000
3+++ openerp/addons/base/ir/ir_model.py 2014-04-08 14:07:20 +0000
4@@ -35,6 +35,8 @@
5 from openerp.tools.translate import _
6 from openerp.osv.orm import except_orm, browse_record, MAGIC_COLUMNS
7
8+from openerp.openupgrade import openupgrade_log, openupgrade
9+
10 _logger = logging.getLogger(__name__)
11
12 MODULE_UNINSTALL_FLAG = '_force_unlink'
13@@ -145,6 +147,11 @@
14
15 def _drop_table(self, cr, uid, ids, context=None):
16 for model in self.browse(cr, uid, ids, context):
17+ # OpenUpgrade: do not run the new table cleanup
18+ openupgrade.message(
19+ cr, 'Unknown', False, False,
20+ "Not dropping the table or view of model %s", model.model)
21+ continue
22 model_pool = self.pool[model.model]
23 cr.execute('select relkind from pg_class where relname=%s', (model_pool._table,))
24 result = cr.fetchone()
25@@ -304,6 +311,12 @@
26 for field in self.browse(cr, uid, ids, context):
27 if field.name in MAGIC_COLUMNS:
28 continue
29+ # OpenUpgrade: do not run the new column cleanup
30+ openupgrade.message(
31+ cr, 'Unknown', False, False,
32+ "Not dropping the column of field %s of model %s", field.name, field.model)
33+ continue
34+
35 model = self.pool[field.model]
36 cr.execute('select relkind from pg_class where relname=%s', (model._table,))
37 result = cr.fetchone()
38@@ -951,6 +964,10 @@
39 return super(ir_model_data,self).unlink(cr, uid, ids, context=context)
40
41 def _update(self,cr, uid, model, module, values, xml_id=False, store=True, noupdate=False, mode='init', res_id=False, context=None):
42+ #OpenUpgrade: log entry (used in csv import)
43+ if xml_id:
44+ openupgrade_log.log_xml_id(cr, module, xml_id)
45+
46 model_obj = self.pool[model]
47 if not context:
48 context = {}
49@@ -1177,7 +1194,17 @@
50 for (model, res_id) in to_unlink:
51 if model in self.pool:
52 _logger.info('Deleting %s@%s', res_id, model)
53- self.pool[model].unlink(cr, uid, [res_id])
54+ try:
55+ cr.execute('SAVEPOINT ir_model_data_delete');
56+ self.pool[model].unlink(cr, uid, [res_id])
57+ cr.execute('RELEASE SAVEPOINT ir_model_data_delete')
58+ except Exception:
59+ cr.execute('ROLLBACK TO SAVEPOINT ir_model_data_delete');
60+ _logger.warning(
61+ 'Could not delete obsolete record with id: %d of model %s\n'
62+ 'Please refer to the log message right above',
63+ res_id, model)
64+
65
66 class wizard_model_menu(osv.osv_memory):
67 _name = 'wizard.ir.model.menu.create'
68
69=== modified file 'openerp/addons/base/res/res_currency.py'
70--- openerp/addons/base/res/res_currency.py 2013-11-15 13:25:53 +0000
71+++ openerp/addons/base/res/res_currency.py 2014-04-08 14:07:20 +0000
72@@ -106,9 +106,17 @@
73 # we would allow duplicate "global" currencies (all having company_id == NULL)
74 cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'res_currency_unique_name_company_id_idx'""")
75 if not cr.fetchone():
76- cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
77- ON res_currency
78- (name, (COALESCE(company_id,-1)))""")
79+ try:
80+ cr.execute('SAVEPOINT index_currency');
81+ cr.execute("""CREATE UNIQUE INDEX res_currency_unique_name_company_id_idx
82+ ON res_currency
83+ (name, (COALESCE(company_id,-1)))""")
84+ cr.execute('RELEASE SAVEPOINT index_currency');
85+ except Exception, e:
86+ cr.execute('ROLLBACK TO SAVEPOINT index_currency');
87+ import logging
88+ logging.getLogger('OpenUpgrade').debug(
89+ 'Could not create currency unique index: %s', e)
90
91 def read(self, cr, user, ids, fields=None, context=None, load='_classic_read'):
92 res = super(res_currency, self).read(cr, user, ids, fields, context, load)
93
94=== modified file 'openerp/modules/graph.py'
95--- openerp/modules/graph.py 2013-03-27 17:06:39 +0000
96+++ openerp/modules/graph.py 2014-04-08 14:07:20 +0000
97@@ -92,12 +92,23 @@
98 force = []
99 packages = []
100 len_graph = len(self)
101+
102+ # force additional dependencies for the upgrade process if given
103+ # in config file
104+ forced_deps = tools.config.get_misc('openupgrade', 'force_deps', '{}')
105+ forced_deps = tools.config.get_misc('openupgrade',
106+ 'force_deps_' + release.version,
107+ forced_deps)
108+ forced_deps = tools.safe_eval.safe_eval(forced_deps)
109+
110 for module in module_list:
111 # This will raise an exception if no/unreadable descriptor file.
112 # NOTE The call to load_information_from_description_file is already
113 # done by db.initialize, so it is possible to not do it again here.
114 info = openerp.modules.module.load_information_from_description_file(module)
115+
116 if info and info['installable']:
117+ info['depends'].extend(forced_deps.get(module, []))
118 packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version
119 else:
120 _logger.warning('module %s: not installable, skipped', module)
121
122=== modified file 'openerp/modules/loading.py'
123--- openerp/modules/loading.py 2014-03-17 15:18:10 +0000
124+++ openerp/modules/loading.py 2014-04-08 14:07:20 +0000
125@@ -43,11 +43,13 @@
126 from openerp.modules.module import initialize_sys_path, \
127 load_openerp_module, init_module_models, adapt_version
128
129+from openerp.openupgrade import openupgrade_loading
130+
131 _logger = logging.getLogger(__name__)
132 _test_logger = logging.getLogger('openerp.tests')
133
134
135-def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
136+def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None, upg_registry=None):
137 """Migrates+Updates or Installs all module nodes from ``graph``
138 :param graph: graph of module nodes to load
139 :param status: status dictionary for keeping track of progress
140@@ -121,6 +123,9 @@
141 if status is None:
142 status = {}
143
144+ if skip_modules is None:
145+ skip_modules = []
146+
147 processed_modules = []
148 loaded_modules = []
149 registry = openerp.registry(cr.dbname)
150@@ -135,12 +140,18 @@
151 for field in cr.dictfetchall():
152 registry.fields_by_model.setdefault(field['model'], []).append(field)
153
154+ #suppress commits to have the upgrade of one module in just one transation
155+ cr.commit_org = cr.commit
156+ cr.commit = lambda *args: None
157+ cr.rollback_org = cr.rollback
158+ cr.rollback = lambda *args: None
159+
160 # register, instantiate and initialize models for each modules
161 for index, package in enumerate(graph):
162 module_name = package.name
163 module_id = package.id
164
165- if skip_modules and module_name in skip_modules:
166+ if module_name in skip_modules or module_name in loaded_modules:
167 continue
168
169 _logger.debug('module %s: loading objects', package.name)
170@@ -151,6 +162,13 @@
171
172 loaded_modules.append(package.name)
173 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
174+ # OpenUpgrade: add this module's models to the registry
175+ local_registry = {}
176+ for model in models:
177+ openupgrade_loading.log_model(model, local_registry)
178+ openupgrade_loading.compare_registries(
179+ cr, package.name, upg_registry, local_registry)
180+
181 init_module_models(cr, package.name, models)
182 registry._init_modules.add(package.name)
183 status['progress'] = float(index) / len(graph)
184@@ -179,7 +197,13 @@
185 _load_data(cr, module_name, idref, mode, kind='demo')
186 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
187
188- migrations.migrate_module(package, 'post')
189+ # OpenUpgrade: add 'try' block for logging exceptions
190+ # as errors in post scripts seem to be dropped
191+ try:
192+ migrations.migrate_module(package, 'post')
193+ except Exception, e:
194+ _logger.error('Error executing post migration script for module %s: %s', package, e)
195+ raise
196
197 if has_demo:
198 # launch tests only in demo mode, allowing tests to use demo data.
199@@ -206,12 +230,13 @@
200 if hasattr(package, kind):
201 delattr(package, kind)
202
203- cr.commit()
204+ cr.commit_org()
205
206 # The query won't be valid for models created later (i.e. custom model
207 # created after the registry has been loaded), so empty its result.
208 registry.fields_by_model = None
209-
210+
211+ cr.commit = cr.commit_org
212 cr.commit()
213
214 return loaded_modules, processed_modules
215@@ -230,16 +255,17 @@
216 incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
217 _logger.warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
218
219-def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks):
220+def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks, upg_registry):
221 """Loads modules marked with ``states``, adding them to ``graph`` and
222 ``loaded_modules`` and returns a list of installed/upgraded modules."""
223 processed_modules = []
224 while True:
225 cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),))
226 module_list = [name for (name,) in cr.fetchall() if name not in graph]
227+ module_list = openupgrade_loading.add_module_dependencies(cr, module_list)
228 graph.add_modules(cr, module_list, force)
229 _logger.debug('Updating graph with %d more modules', len(module_list))
230- loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks)
231+ loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks, upg_registry=upg_registry)
232 processed_modules.extend(processed)
233 loaded_modules.extend(loaded)
234 if not processed: break
235@@ -255,6 +281,7 @@
236 if force_demo:
237 force.append('demo')
238
239+ upg_registry = {}
240 cr = db.cursor()
241 try:
242 if not openerp.modules.db.is_initialized(cr):
243@@ -282,7 +309,7 @@
244 # processed_modules: for cleanup step after install
245 # loaded_modules: to avoid double loading
246 report = registry._assertion_report
247- loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=update_module, report=report)
248+ loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=update_module, report=report, upg_registry=upg_registry)
249
250 if tools.config['load_language']:
251 for lang in tools.config['load_language'].split(','):
252@@ -331,11 +358,11 @@
253 previously_processed = len(processed_modules)
254 processed_modules += load_marked_modules(cr, graph,
255 ['installed', 'to upgrade', 'to remove'],
256- force, status, report, loaded_modules, update_module)
257+ force, status, report, loaded_modules, update_module, upg_registry)
258 if update_module:
259 processed_modules += load_marked_modules(cr, graph,
260 ['to install'], force, status, report,
261- loaded_modules, update_module)
262+ loaded_modules, update_module, upg_registry)
263
264 # load custom models
265 cr.execute('select model from ir_model where state=%s', ('manual',))
266
267=== modified file 'openerp/modules/migration.py'
268--- openerp/modules/migration.py 2014-01-10 16:27:05 +0000
269+++ openerp/modules/migration.py 2014-04-08 14:07:20 +0000
270@@ -165,14 +165,16 @@
271 try:
272 mod = imp.load_source(name, pyfile, fp2)
273 _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
274- migrate = mod.migrate
275 except ImportError:
276 _logger.exception('module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
277 raise
278- except AttributeError:
279+
280+ _logger.info('module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
281+
282+ if hasattr(mod, 'migrate'):
283+ mod.migrate(self.cr, pkg.installed_version)
284+ else:
285 _logger.error('module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
286- else:
287- migrate(self.cr, pkg.installed_version)
288 finally:
289 if fp:
290 fp.close()
291
292=== added directory 'openerp/openupgrade'
293=== added file 'openerp/openupgrade/__init__.py'
294=== added file 'openerp/openupgrade/openupgrade.py'
295--- openerp/openupgrade/openupgrade.py 1970-01-01 00:00:00 +0000
296+++ openerp/openupgrade/openupgrade.py 2014-04-08 14:07:20 +0000
297@@ -0,0 +1,479 @@
298+# -*- coding: utf-8 -*-
299+##############################################################################
300+#
301+# OpenERP, Open Source Management Solution
302+# This module copyright (C) 2011-2013 Therp BV (<http://therp.nl>)
303+#
304+# This program is free software: you can redistribute it and/or modify
305+# it under the terms of the GNU Affero General Public License as
306+# published by the Free Software Foundation, either version 3 of the
307+# License, or (at your option) any later version.
308+#
309+# This program is distributed in the hope that it will be useful,
310+# but WITHOUT ANY WARRANTY; without even the implied warranty of
311+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
312+# GNU Affero General Public License for more details.
313+#
314+# You should have received a copy of the GNU Affero General Public License
315+# along with this program. If not, see <http://www.gnu.org/licenses/>.
316+#
317+##############################################################################
318+
319+import os
320+import inspect
321+import logging
322+from openerp import release, osv, pooler, tools, SUPERUSER_ID
323+import openupgrade_tools
324+
325+# The server log level has not been set at this point
326+# so to log at loglevel debug we need to set it
327+# manually here. As a consequence, DEBUG messages from
328+# this file are always logged
329+logger = logging.getLogger('OpenUpgrade')
330+logger.setLevel(logging.DEBUG)
331+
332+__all__ = [
333+ 'migrate',
334+ 'load_data',
335+ 'rename_columns',
336+ 'rename_tables',
337+ 'rename_models',
338+ 'rename_xmlids',
339+ 'drop_columns',
340+ 'delete_model_workflow',
341+ 'warn_possible_dataloss',
342+ 'set_defaults',
343+ 'logged_query',
344+ 'column_exists',
345+ 'table_exists',
346+ 'update_module_names',
347+ 'add_ir_model_fields',
348+ 'get_legacy_name',
349+ 'm2o_to_m2m',
350+ 'message',
351+]
352+
353+def load_data(cr, module_name, filename, idref=None, mode='init'):
354+ """
355+ Load an xml or csv data file from your post script. The usual case for this is the
356+ occurrence of newly added essential or useful data in the module that is
357+ marked with "noupdate='1'" and without "forcecreate='1'" so that it will
358+ not be loaded by the usual upgrade mechanism. Leaving the 'mode' argument to
359+ its default 'init' will load the data from your migration script.
360+
361+ Theoretically, you could simply load a stock file from the module, but be
362+ careful not to reinitialize any data that could have been customized.
363+ Preferably, select only the newly added items. Copy these to a file
364+ in your migrations directory and load that file.
365+ Leave it to the user to actually delete existing resources that are
366+ marked with 'noupdate' (other named items will be deleted
367+ automatically).
368+
369+
370+ :param module_name: the name of the module
371+ :param filename: the path to the filename, relative to the module \
372+ directory.
373+ :param idref: optional hash with ?id mapping cache?
374+ :param mode: one of 'init', 'update', 'demo'. Always use 'init' for adding new items \
375+ from files that are marked with 'noupdate'. Defaults to 'init'.
376+
377+ """
378+
379+ if idref is None:
380+ idref = {}
381+ logger.info('%s: loading %s' % (module_name, filename))
382+ _, ext = os.path.splitext(filename)
383+ pathname = os.path.join(module_name, filename)
384+ fp = tools.file_open(pathname)
385+ try:
386+ if ext == '.csv':
387+ noupdate = True
388+ tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
389+ else:
390+ tools.convert_xml_import(cr, module_name, fp, idref, mode=mode)
391+ finally:
392+ fp.close()
393+
394+# for backwards compatibility
395+load_xml = load_data
396+table_exists = openupgrade_tools.table_exists
397+
398+def rename_columns(cr, column_spec):
399+ """
400+ Rename table columns. Typically called in the pre script.
401+
402+ :param column_spec: a hash with table keys, with lists of tuples as values. \
403+ Tuples consist of (old_name, new_name). Use None for new_name to trigger a \
404+ conversion of old_name using get_legacy_name()
405+ """
406+ for table in column_spec.keys():
407+ for (old, new) in column_spec[table]:
408+ if new is None:
409+ new = get_legacy_name(old)
410+ logger.info("table %s, column %s: renaming to %s",
411+ table, old, new)
412+ cr.execute('ALTER TABLE "%s" RENAME "%s" TO "%s"' % (table, old, new,))
413+ cr.execute('DROP INDEX IF EXISTS "%s_%s_index"' % (table, old))
414+
415+def rename_tables(cr, table_spec):
416+ """
417+ Rename tables. Typically called in the pre script.
418+ This function also renames the id sequence if it exists and if it is
419+ not modified in the same run.
420+
421+ :param table_spec: a list of tuples (old table name, new table name).
422+
423+ """
424+ # Append id sequences
425+ to_rename = [x[0] for x in table_spec]
426+ for old, new in list(table_spec):
427+ if (table_exists(cr, old + '_id_seq') and
428+ old + '_id_seq' not in to_rename):
429+ table_spec.append((old + '_id_seq', new + '_id_seq'))
430+ for (old, new) in table_spec:
431+ logger.info("table %s: renaming to %s",
432+ old, new)
433+ cr.execute('ALTER TABLE "%s" RENAME TO "%s"' % (old, new,))
434+
435+def rename_models(cr, model_spec):
436+ """
437+ Rename models. Typically called in the pre script.
438+ :param model_spec: a list of tuples (old model name, new model name).
439+
440+ Use case: if a model changes name, but still implements equivalent
441+ functionality you will want to update references in for instance
442+ relation fields.
443+
444+ """
445+ for (old, new) in model_spec:
446+ logger.info("model %s: renaming to %s",
447+ old, new)
448+ cr.execute('UPDATE ir_model SET model = %s '
449+ 'WHERE model = %s', (new, old,))
450+ cr.execute('UPDATE ir_model_fields SET relation = %s '
451+ 'WHERE relation = %s', (new, old,))
452+ # TODO: signal where the model occurs in references to ir_model
453+
454+def rename_xmlids(cr, xmlids_spec):
455+ """
456+ Rename XML IDs. Typically called in the pre script.
457+ One usage example is when an ID changes module. In OpenERP 6 for example,
458+ a number of res_groups IDs moved to module base from other modules (
459+ although they were still being defined in their respective module).
460+ """
461+ for (old, new) in xmlids_spec:
462+ if not old.split('.') or not new.split('.'):
463+ logger.error(
464+ 'Cannot rename XMLID %s to %s: need the module '
465+ 'reference to be specified in the IDs' % (old, new))
466+ else:
467+ query = ("UPDATE ir_model_data SET module = %s, name = %s "
468+ "WHERE module = %s and name = %s")
469+ logged_query(cr, query, tuple(new.split('.') + old.split('.')))
470+
471+def drop_columns(cr, column_spec):
472+ """
473+ Drop columns but perform an additional check if a column exists.
474+ This covers the case of function fields that may or may not be stored.
475+ Consider that this may not be obvious: an additional module can govern
476+ a function fields' store properties.
477+
478+ :param column_spec: a list of (table, column) tuples
479+ """
480+ for (table, column) in column_spec:
481+ logger.info("table %s: drop column %s",
482+ table, column)
483+ if column_exists(cr, table, column):
484+ cr.execute('ALTER TABLE "%s" DROP COLUMN "%s"' %
485+ (table, column))
486+ else:
487+ logger.warn("table %s: column %s did not exist",
488+ table, column)
489+
490+def delete_model_workflow(cr, model):
491+ """
492+ Forcefully remove active workflows for obsolete models,
493+ to prevent foreign key issues when the orm deletes the model.
494+ """
495+ logged_query(
496+ cr,
497+ "DELETE FROM wkf_workitem WHERE act_id in "
498+ "( SELECT wkf_activity.id "
499+ " FROM wkf_activity, wkf "
500+ " WHERE wkf_id = wkf.id AND "
501+ " wkf.osv = %s"
502+ ")", (model,))
503+ logged_query(
504+ cr,
505+ "DELETE FROM wkf WHERE osv = %s", (model,))
506+
507+def warn_possible_dataloss(cr, pool, old_module, fields):
508+ """
509+ Use that function in the following case :
510+ if a field of a model was moved from a 'A' module to a 'B' module.
511+ ('B' depend on 'A'),
512+ This function will test if 'B' is installed.
513+ If not, count the number of different value and possibly warn the user.
514+ Use orm, so call from the post script.
515+
516+ :param old_module: name of the old module
517+ :param fields: list of dictionary with the following keys :
518+ 'table' : name of the table where the field is.
519+ 'field' : name of the field that are moving.
520+ 'new_module' : name of the new module
521+
522+ .. versionadded:: 7.0
523+ """
524+ module_obj = pool.get('ir.module.module')
525+ for field in fields:
526+ module_ids = module_obj.search(cr, SUPERUSER_ID, [
527+ ('name', '=', field['new_module']),
528+ ('state', 'in', ['installed', 'to upgrade', 'to install'])
529+ ])
530+ if not module_ids:
531+ cr.execute(
532+ "SELECT count(*) FROM (SELECT %s from %s group by %s) "
533+ "as tmp" % (
534+ field['field'], field['table'], field['field']))
535+ row = cr.fetchone()
536+ if row[0] == 1:
537+ # not a problem, that field wasn't used.
538+ # Just a loss of functionality
539+ logger.info(
540+ "Field '%s' from module '%s' was moved to module "
541+ "'%s' which is not installed: "
542+ "No dataloss detected, only loss of functionality"
543+ %(field['field'], old_module, field['new_module']))
544+ else:
545+ # there is data loss after the migration.
546+ message(
547+ cr, old_module,
548+ "Field '%s' was moved to module "
549+ "'%s' which is not installed: "
550+ "There were %s distinct values in this field.",
551+ field['field'], field['new_module'], row[0])
552+
553+def set_defaults(cr, pool, default_spec, force=False):
554+ """
555+ Set default value. Useful for fields that are newly required. Uses orm, so
556+ call from the post script.
557+
558+ :param default_spec: a hash with model names as keys. Values are lists of \
559+ tuples (field, value). None as a value has a special meaning: it assigns \
560+ the default value. If this value is provided by a function, the function is \
561+ called as the user that created the resource.
562+ :param force: overwrite existing values. To be used for assigning a non- \
563+ default value (presumably in the case of a new column). The ORM assigns \
564+ the default value as declared in the model in an earlier stage of the \
565+ process. Beware of issues with resources loaded from new data that \
566+ actually do require the model's default, in combination with the post \
567+ script possible being run multiple times.
568+ """
569+
570+ def write_value(ids, field, value):
571+ logger.debug("model %s, field %s: setting default value of resources %s to %s",
572+ model, field, ids, unicode(value))
573+ for res_id in ids:
574+ # Iterating over ids here as a workaround for lp:1131653
575+ obj.write(cr, SUPERUSER_ID, [res_id], {field: value})
576+
577+ for model in default_spec.keys():
578+ obj = pool.get(model)
579+ if not obj:
580+ raise osv.except_osv("Migration: error setting default, no such model: %s" % model, "")
581+
582+ for field, value in default_spec[model]:
583+ domain = not force and [(field, '=', False)] or []
584+ ids = obj.search(cr, SUPERUSER_ID, domain)
585+ if not ids:
586+ continue
587+ if value is None:
588+ # Set the value by calling the _defaults of the object.
589+ # Typically used for company_id on various models, and in that
590+ # case the result depends on the user associated with the object.
591+ # We retrieve create_uid for this purpose and need to call the _defaults
592+ # function per resource. Otherwise, write all resources at once.
593+ if field in obj._defaults:
594+ if not callable(obj._defaults[field]):
595+ write_value(ids, field, obj._defaults[field])
596+ else:
597+ # existence users is covered by foreign keys, so this is not needed
598+ # cr.execute("SELECT %s.id, res_users.id FROM %s LEFT OUTER JOIN res_users ON (%s.create_uid = res_users.id) WHERE %s.id IN %s" %
599+ # (obj._table, obj._table, obj._table, obj._table, tuple(ids),))
600+ cr.execute("SELECT id, COALESCE(create_uid, 1) FROM %s " % obj._table + "WHERE id in %s", (tuple(ids),))
601+ # Execute the function once per user_id
602+ user_id_map = {}
603+ for row in cr.fetchall():
604+ user_id_map.setdefault(row[1], []).append(row[0])
605+ for user_id in user_id_map:
606+ write_value(
607+ user_id_map[user_id], field,
608+ obj._defaults[field](obj, cr, user_id, None))
609+ else:
610+ error = ("OpenUpgrade: error setting default, field %s with "
611+ "None default value not in %s' _defaults" % (
612+ field, model))
613+ logger.error(error)
614+ # this exeption seems to get lost in a higher up try block
615+ osv.except_osv("OpenUpgrade", error)
616+ else:
617+ write_value(ids, field, value)
618+
619+def logged_query(cr, query, args=None):
620+ """
621+ Logs query and affected rows at level DEBUG
622+ """
623+ if args is None:
624+ args = []
625+ res = cr.execute(query, args)
626+ logger.debug('Running %s', query % tuple(args))
627+ logger.debug('%s rows affected', cr.rowcount)
628+ return cr.rowcount
629+
630+def column_exists(cr, table, column):
631+ """ Check whether a certain column exists """
632+ cr.execute(
633+ 'SELECT count(attname) FROM pg_attribute '
634+ 'WHERE attrelid = '
635+ '( SELECT oid FROM pg_class WHERE relname = %s ) '
636+ 'AND attname = %s',
637+ (table, column));
638+ return cr.fetchone()[0] == 1
639+
640+def update_module_names(cr, namespec):
641+ """
642+ Deal with changed module names of certified modules
643+ in order to prevent 'certificate not unique' error,
644+ as well as updating the module reference in the
645+ XML id.
646+
647+ :param namespec: tuple of (old name, new name)
648+ """
649+ for (old_name, new_name) in namespec:
650+ query = ("UPDATE ir_module_module SET name = %s "
651+ "WHERE name = %s")
652+ logged_query(cr, query, (new_name, old_name))
653+ query = ("UPDATE ir_model_data SET module = %s "
654+ "WHERE module = %s ")
655+ logged_query(cr, query, (new_name, old_name))
656+ query = ("UPDATE ir_module_module_dependency SET name = %s "
657+ "WHERE name = %s")
658+ logged_query(cr, query, (new_name, old_name))
659+
660+def add_ir_model_fields(cr, columnspec):
661+ """
662+ Typically, new columns on ir_model_fields need to be added in a very
663+ early stage in the upgrade process of the base module, in raw sql
664+ as they need to be in place before any model gets initialized.
665+ Do not use for fields with additional SQL constraints, such as a
666+ reference to another table or the cascade constraint, but craft your
667+ own statement taking them into account.
668+
669+ :param columnspec: tuple of (column name, column type)
670+ """
671+ for column in columnspec:
672+ query = 'ALTER TABLE ir_model_fields ADD COLUMN %s %s' % (
673+ column)
674+ logged_query(cr, query, [])
675+
676+def get_legacy_name(original_name):
677+ """
678+ Returns a versioned name for legacy tables/columns/etc
679+ Use this function instead of some custom name to avoid
680+ collisions with future or past legacy tables/columns/etc
681+
682+ :param original_name: the original name of the column
683+ :param version: current version as passed to migrate()
684+ """
685+ return 'openupgrade_legacy_'+('_').join(
686+ map(str, release.version_info[0:2]))+'_'+original_name
687+
688+def m2o_to_m2m(cr, model, table, field, source_field):
689+ """
690+ Recreate relations in many2many fields that were formerly
691+ many2one fields. Use rename_columns in your pre-migrate
692+ script to retain the column's old value, then call m2o_to_m2m
693+ in your post-migrate script.
694+
695+ :param model: The target model pool object
696+ :param table: The source table
697+ :param field: The field name of the target model
698+ :param source_field: the many2one column on the source table.
699+
700+ .. versionadded:: 7.0
701+ """
702+ cr.execute('SELECT id, %(field)s '
703+ 'FROM %(table)s '
704+ 'WHERE %(field)s is not null' % {
705+ 'table': table,
706+ 'field': source_field,
707+ })
708+ for row in cr.fetchall():
709+ model.write(cr, SUPERUSER_ID, row[0], {field: [(4, row[1])]})
710+
711+def message(cr, module, table, column,
712+ message, *args, **kwargs):
713+ """
714+ Log handler for non-critical notifications about the upgrade.
715+ To be extended with logging to a table for reporting purposes.
716+
717+ :param module: the module name that the message concerns
718+ :param table: the model that this message concerns (may be False, \
719+ but preferably not if 'column' is defined)
720+ :param column: the column that this message concerns (may be False)
721+
722+ .. versionadded:: 7.0
723+ """
724+ argslist = list(args or [])
725+ prefix = ': '
726+ if column:
727+ argslist.insert(0, column)
728+ prefix = ', column %s' + prefix
729+ if table:
730+ argslist.insert(0, table)
731+ prefix = ', table %s' + prefix
732+ argslist.insert(0, module)
733+ prefix = 'Module %s' + prefix
734+
735+ logger.warn(prefix + message, *argslist, **kwargs)
736+
737+def migrate():
738+ """
739+ This is the decorator for the migrate() function
740+ in migration scripts.
741+ Return when the 'version' argument is not defined,
742+ and log execeptions.
743+ Retrieve debug context data from the frame above for
744+ logging purposes.
745+ """
746+ def wrap(func):
747+ def wrapped_function(cr, version):
748+ stage = 'unknown'
749+ module = 'unknown'
750+ filename = 'unknown'
751+ try:
752+ frame = inspect.getargvalues(inspect.stack()[1][0])
753+ stage = frame.locals['stage']
754+ module = frame.locals['pkg'].name
755+ filename = frame.locals['fp'].name
756+ except Exception, e:
757+ logger.error(
758+ "'migrate' decorator: failed to inspect "
759+ "the frame above: %s" % e)
760+ pass
761+ if not version:
762+ return
763+ logger.info(
764+ "%s: %s-migration script called with version %s" %
765+ (module, stage, version))
766+ try:
767+ # The actual function is called here
768+ func(cr, version)
769+ except Exception, e:
770+ logger.error(
771+ "%s: error in migration script %s: %s" %
772+ (module, filename, str(e).decode('utf8')))
773+ logger.exception(e)
774+ raise
775+ return wrapped_function
776+ return wrap
777
778=== added file 'openerp/openupgrade/openupgrade_loading.py'
779--- openerp/openupgrade/openupgrade_loading.py 1970-01-01 00:00:00 +0000
780+++ openerp/openupgrade/openupgrade_loading.py 2014-04-08 14:07:20 +0000
781@@ -0,0 +1,185 @@
782+# -*- coding: utf-8 -*-
783+##############################################################################
784+#
785+# OpenERP, Open Source Management Solution
786+# This module copyright (C) 2011-2012 Therp BV (<http://therp.nl>)
787+#
788+# This program is free software: you can redistribute it and/or modify
789+# it under the terms of the GNU Affero General Public License as
790+# published by the Free Software Foundation, either version 3 of the
791+# License, or (at your option) any later version.
792+#
793+# This program is distributed in the hope that it will be useful,
794+# but WITHOUT ANY WARRANTY; without even the implied warranty of
795+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
796+# GNU Affero General Public License for more details.
797+#
798+# You should have received a copy of the GNU Affero General Public License
799+# along with this program. If not, see <http://www.gnu.org/licenses/>.
800+#
801+##############################################################################
802+
803+import types
804+from openerp import release
805+from openerp.osv.orm import TransientModel
806+from openerp.osv import fields
807+from openerp.openupgrade.openupgrade import table_exists
808+from openerp.tools import config, safe_eval
809+
810+# A collection of functions used in
811+# openerp/modules/loading.py
812+
813+def add_module_dependencies(cr, module_list):
814+ """
815+ Select (new) dependencies from the modules in the list
816+ so that we can inject them into the graph at upgrade
817+ time. Used in the modified OpenUpgrade Server,
818+ not to be called from migration scripts
819+
820+ Also take the OpenUpgrade configuration directives 'forced_deps'
821+ and 'autoinstall' into account. From any additional modules
822+ that these directives can add, the dependencies are added as
823+ well (but these directives are not checked for the occurrence
824+ of any of the dependencies).
825+ """
826+ if not module_list:
827+ return module_list
828+
829+ forced_deps = safe_eval.safe_eval(
830+ config.get_misc(
831+ 'openupgrade', 'forced_deps_' + release.version,
832+ config.get_misc('openupgrade', 'forced_deps', '{}')))
833+
834+ autoinstall = safe_eval.safe_eval(
835+ config.get_misc(
836+ 'openupgrade', 'autoinstall_' + release.version,
837+ config.get_misc('openupgrade', 'autoinstall', '{}')))
838+
839+ for module in list(module_list):
840+ module_list += forced_deps.get(module, [])
841+ module_list += autoinstall.get(module, [])
842+
843+ cr.execute("""
844+ SELECT ir_module_module_dependency.name
845+ FROM
846+ ir_module_module,
847+ ir_module_module_dependency
848+ WHERE
849+ module_id = ir_module_module.id
850+ AND ir_module_module.name in %s
851+ """, (tuple(module_list),))
852+
853+ return list(set(
854+ module_list + [x[0] for x in cr.fetchall()]
855+ ))
856+
857+def log_model(model, local_registry):
858+ """
859+ OpenUpgrade: Store the characteristics of the BaseModel and its fields
860+ in the local registry, so that we can compare changes with the
861+ main registry
862+ """
863+
864+ if not model._name: # new in 6.1
865+ return
866+
867+ # persistent models only
868+ if isinstance(model, TransientModel):
869+ return
870+
871+ model_registry = local_registry.setdefault(
872+ model._name, {})
873+ if model._inherits:
874+ model_registry['_inherits'] = {'_inherits': unicode(model._inherits)}
875+ for k, v in model._columns.items():
876+ properties = {
877+ 'type': v._type,
878+ 'isfunction': (
879+ isinstance(v, fields.function) and 'function' or ''),
880+ 'relation': (
881+ v._type in ('many2many', 'many2one','one2many')
882+ and v._obj or ''
883+ ),
884+ 'required': v.required and 'required' or '',
885+ 'selection_keys': '',
886+ 'req_default': '',
887+ 'inherits': '',
888+ }
889+ if v._type == 'selection':
890+ if hasattr(v.selection, "__iter__"):
891+ properties['selection_keys'] = unicode(
892+ sorted([x[0] for x in v.selection]))
893+ else:
894+ properties['selection_keys'] = 'function'
895+ if v.required and k in model._defaults:
896+ if isinstance(model._defaults[k], types.FunctionType):
897+ # todo: in OpenERP 5 (and in 6 as well),
898+ # literals are wrapped in a lambda function
899+ properties['req_default'] = 'function'
900+ else:
901+ properties['req_default'] = unicode(
902+ model._defaults[k])
903+ for key, value in properties.items():
904+ if value:
905+ model_registry.setdefault(k, {})[key] = value
906+
907+def get_record_id(cr, module, model, field, mode):
908+ """
909+ OpenUpgrade: get or create the id from the record table matching
910+ the key parameter values
911+ """
912+ cr.execute(
913+ "SELECT id FROM openupgrade_record "
914+ "WHERE module = %s AND model = %s AND "
915+ "field = %s AND mode = %s AND type = %s",
916+ (module, model, field, mode, 'field')
917+ )
918+ record = cr.fetchone()
919+ if record:
920+ return record[0]
921+ cr.execute(
922+ "INSERT INTO openupgrade_record "
923+ "(module, model, field, mode, type) "
924+ "VALUES (%s, %s, %s, %s, %s)",
925+ (module, model, field, mode, 'field')
926+ )
927+ cr.execute(
928+ "SELECT id FROM openupgrade_record "
929+ "WHERE module = %s AND model = %s AND "
930+ "field = %s AND mode = %s AND type = %s",
931+ (module, model, field, mode, 'field')
932+ )
933+ return cr.fetchone()[0]
934+
935+def compare_registries(cr, module, registry, local_registry):
936+ """
937+ OpenUpgrade: Compare the local registry with the global registry,
938+ log any differences and merge the local registry with
939+ the global one.
940+ """
941+ if not table_exists(cr, 'openupgrade_record'):
942+ return
943+ for model, fields in local_registry.items():
944+ registry.setdefault(model, {})
945+ for field, attributes in fields.items():
946+ old_field = registry[model].setdefault(field, {})
947+ mode = old_field and 'modify' or 'create'
948+ record_id = False
949+ for key, value in attributes.items():
950+ if key not in old_field or old_field[key] != value:
951+ if not record_id:
952+ record_id = get_record_id(
953+ cr, module, model, field, mode)
954+ cr.execute(
955+ "SELECT id FROM openupgrade_attribute "
956+ "WHERE name = %s AND value = %s AND "
957+ "record_id = %s",
958+ (key, value, record_id)
959+ )
960+ if not cr.fetchone():
961+ cr.execute(
962+ "INSERT INTO openupgrade_attribute "
963+ "(name, value, record_id) VALUES (%s, %s, %s)",
964+ (key, value, record_id)
965+ )
966+ old_field[key] = value
967
968=== added file 'openerp/openupgrade/openupgrade_log.py'
969--- openerp/openupgrade/openupgrade_log.py 1970-01-01 00:00:00 +0000
970+++ openerp/openupgrade/openupgrade_log.py 2014-04-08 14:07:20 +0000
971@@ -0,0 +1,56 @@
972+# -*- coding: utf-8 -*-
973+from openupgrade_tools import table_exists
974+
975+def log_xml_id(cr, module, xml_id):
976+ """
977+ Log xml_ids at load time in the records table.
978+ Called from openerp/tools/convert.py:xml_import._test_xml_id()
979+
980+ # Catcha's
981+ - The module needs to be loaded with 'init', or the calling method
982+ won't be called. This can be brought about by installing the
983+ module or updating the 'state' field of the module to 'to install'
984+ or call the server with '--init <module>' and the database argument.
985+
986+ - Do you get the right results immediately when installing the module?
987+ No, sorry. This method retrieves the model from the ir_model_table, but when
988+ the xml id is encountered for the first time, this method is called
989+ before the item is present in this table. Therefore, you will not
990+ get any meaningful results until the *second* time that you 'init'
991+ the module.
992+
993+ - The good news is that the openupgrade_records module that comes
994+ with this distribution allows you to deal with all of this with
995+ one click on the menu item Settings -> Customizations ->
996+ Database Structure -> OpenUpgrade -> Generate Records
997+
998+ - You cannot reinitialize the modules in your production database
999+ and expect to keep working on it happily ever after. Do not perform
1000+ this routine on your production database.
1001+
1002+ :param module: The module that contains the xml_id
1003+ :param xml_id: the xml_id, with or without 'module.' prefix
1004+ """
1005+ if not table_exists(cr, 'openupgrade_record'):
1006+ return
1007+ if not '.' in xml_id:
1008+ xml_id = '%s.%s' % (module, xml_id)
1009+ cr.execute(
1010+ "SELECT model FROM ir_model_data "
1011+ "WHERE module = %s AND name = %s",
1012+ xml_id.split('.'))
1013+ record = cr.fetchone()
1014+ if not record:
1015+ print "Cannot find xml_id %s" % xml_id
1016+ return
1017+ else:
1018+ cr.execute(
1019+ "SELECT id FROM openupgrade_record "
1020+ "WHERE module=%s AND model=%s AND name=%s AND type=%s",
1021+ (module, record[0], xml_id, 'xmlid'))
1022+ if not cr.fetchone():
1023+ cr.execute(
1024+ "INSERT INTO openupgrade_record "
1025+ "(module, model, name, type) values(%s, %s, %s, %s)",
1026+ (module, record[0], xml_id, 'xmlid'))
1027+
1028
1029=== added file 'openerp/openupgrade/openupgrade_tools.py'
1030--- openerp/openupgrade/openupgrade_tools.py 1970-01-01 00:00:00 +0000
1031+++ openerp/openupgrade/openupgrade_tools.py 2014-04-08 14:07:20 +0000
1032@@ -0,0 +1,8 @@
1033+# -*- coding: utf-8 -*-
1034+def table_exists(cr, table):
1035+ """ Check whether a certain table or view exists """
1036+ cr.execute(
1037+ 'SELECT count(relname) FROM pg_class WHERE relname = %s',
1038+ (table,))
1039+ return cr.fetchone()[0] == 1
1040+
1041
1042=== modified file 'openerp/osv/orm.py'
1043--- openerp/osv/orm.py 2014-03-18 12:41:12 +0000
1044+++ openerp/osv/orm.py 2014-04-08 14:07:20 +0000
1045@@ -1306,6 +1306,7 @@
1046
1047 position = 0
1048 try:
1049+ cr.execute('SAVEPOINT convert_records')
1050 for res_id, xml_id, res, info in self._convert_records(cr, uid,
1051 self._extract_records(cr, uid, fields, datas,
1052 context=context, log=log),
1053@@ -1323,8 +1324,9 @@
1054 if context.get('defer_parent_store_computation'):
1055 self._parent_store_compute(cr)
1056 cr.commit()
1057+ cr.execute('RELEASE SAVEPOINT convert_records')
1058 except Exception, e:
1059- cr.rollback()
1060+ cr.execute('ROLLBACK TO SAVEPOINT convert_records')
1061 return -1, {}, 'Line %d : %s' % (position + 1, tools.ustr(e)), ''
1062
1063 if context.get('defer_parent_store_computation'):
1064@@ -2827,11 +2829,15 @@
1065 # add the NOT NULL constraint
1066 cr.commit()
1067 try:
1068+ #use savepoints for openupgrade instead of transactions
1069+ cr.execute('SAVEPOINT add_constraint');
1070 cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
1071+ cr.execute('RELEASE SAVEPOINT add_constraint');
1072 cr.commit()
1073 _schema.debug("Table '%s': column '%s': added NOT NULL constraint",
1074 self._table, k)
1075 except Exception:
1076+ cr.execute('ROLLBACK TO SAVEPOINT add_constraint');
1077 msg = "Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
1078 "If you want to have it, you should update the records and execute manually:\n"\
1079 "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL"
1080@@ -2909,11 +2915,14 @@
1081 cr.execute('CREATE INDEX "%s_%s_index" ON "%s" ("%s")' % (self._table, k, self._table, k))
1082 if f.required:
1083 try:
1084- cr.commit()
1085+ #use savepoints for openupgrade instead of transactions
1086+ cr.execute('SAVEPOINT add_constraint');
1087 cr.execute('ALTER TABLE "%s" ALTER COLUMN "%s" SET NOT NULL' % (self._table, k), log_exceptions=False)
1088 _schema.debug("Table '%s': column '%s': added a NOT NULL constraint",
1089 self._table, k)
1090+ cr.execute('RELEASE SAVEPOINT add_constraint');
1091 except Exception:
1092+ cr.execute('ROLLBACK TO SAVEPOINT add_constraint');
1093 msg = "WARNING: unable to set column %s of table %s not null !\n"\
1094 "Try to re-run: openerp-server --update=module\n"\
1095 "If it doesn't work, update records and execute manually:\n"\
1096@@ -3104,12 +3113,14 @@
1097 sql_actions.sort(key=lambda x: x['order'])
1098 for sql_action in [action for action in sql_actions if action['execute']]:
1099 try:
1100+ #use savepoints for openupgrade instead of transactions
1101+ cr.execute('SAVEPOINT add_constraint2');
1102 cr.execute(sql_action['query'])
1103- cr.commit()
1104+ cr.execute('RELEASE SAVEPOINT add_constraint2');
1105 _schema.debug(sql_action['msg_ok'])
1106 except:
1107 _schema.warning(sql_action['msg_err'])
1108- cr.rollback()
1109+ cr.execute('ROLLBACK TO SAVEPOINT add_constraint2');
1110
1111
1112 def _execute_sql(self, cr):
1113
1114=== modified file 'openerp/tools/convert.py'
1115--- openerp/tools/convert.py 2014-01-16 09:17:16 +0000
1116+++ openerp/tools/convert.py 2014-04-08 14:07:20 +0000
1117@@ -66,6 +66,8 @@
1118 unsafe_eval = eval
1119 from safe_eval import safe_eval as eval
1120
1121+from openerp.openupgrade import openupgrade_log
1122+
1123 class ParseError(Exception):
1124 def __init__(self, msg, text, filename, lineno):
1125 self.msg = msg
1126@@ -296,6 +298,7 @@
1127
1128 if len(id) > 64:
1129 _logger.error('id: %s is to long (max: 64)', id)
1130+ openupgrade_log.log_xml_id(self.cr, self.module, xml_id)
1131
1132 def _tag_delete(self, cr, rec, data_node=None):
1133 d_model = rec.get("model",'')
1134
1135=== added file 'scripts/compare_noupdate_xml_records.py'
1136--- scripts/compare_noupdate_xml_records.py 1970-01-01 00:00:00 +0000
1137+++ scripts/compare_noupdate_xml_records.py 2014-04-08 14:07:20 +0000
1138@@ -0,0 +1,201 @@
1139+#!/usr/bin/python
1140+# -*- coding: utf-8 -*-
1141+##############################################################################
1142+#
1143+# OpenERP, Open Source Management Solution
1144+# This module copyright (C) 2013 Therp BV (<http://therp.nl>).
1145+#
1146+# This program is free software: you can redistribute it and/or modify
1147+# it under the terms of the GNU Affero General Public License as
1148+# published by the Free Software Foundation, either version 3 of the
1149+# License, or (at your option) any later version.
1150+#
1151+# This program is distributed in the hope that it will be useful,
1152+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1153+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1154+# GNU Affero General Public License for more details.
1155+#
1156+# You should have received a copy of the GNU Affero General Public License
1157+# along with this program. If not, see <http://www.gnu.org/licenses/>.
1158+#
1159+##############################################################################
1160+
1161+import sys, os, ast, argparse
1162+from copy import deepcopy
1163+from lxml import etree
1164+
1165+def read_manifest(addon_dir):
1166+ with open(os.path.join(addon_dir, '__openerp__.py'), 'r') as f:
1167+ manifest_string = f.read()
1168+ return ast.literal_eval(manifest_string)
1169+
1170+# from openerp.tools
1171+def nodeattr2bool(node, attr, default=False):
1172+ if not node.get(attr):
1173+ return default
1174+ val = node.get(attr).strip()
1175+ if not val:
1176+ return default
1177+ return val.lower() not in ('0', 'false', 'off')
1178+
1179+def get_node_dict(element):
1180+ res = {}
1181+ for child in element:
1182+ if 'name' in child.attrib:
1183+ key = "./%s[@name='%s']" % (
1184+ child.tag, child.attrib['name'])
1185+ res[key] = child
1186+ return res
1187+
1188+def get_node_value(element):
1189+ if 'eval' in element.attrib.keys():
1190+ return element.attrib['eval']
1191+ if 'ref' in element.attrib.keys():
1192+ return element.attrib['ref']
1193+ if not len(element):
1194+ return element.text
1195+ return etree.tostring(element)
1196+
1197+def update_node(target, source):
1198+ for element in source:
1199+ if 'name' in element.attrib:
1200+ query = "./%s[@name='%s']" % (
1201+ element.tag, element.attrib['name'])
1202+ else:
1203+ # query = "./%s" % element.tag
1204+ continue
1205+ for existing in target.xpath(query):
1206+ target.remove(existing)
1207+ target.append(element)
1208+
1209+def get_records(addon_dir):
1210+ addon_dir = addon_dir.rstrip(os.sep)
1211+ addon_name = os.path.basename(addon_dir)
1212+ manifest = read_manifest(addon_dir)
1213+ # The order of the keys are important.
1214+ # Load files in the same order as in
1215+ # module/loading.py:load_module_graph
1216+ keys = ['init_xml', 'update_xml', 'data']
1217+ records_update = {}
1218+ records_noupdate = {}
1219+
1220+ def process_data_node(data_node):
1221+ noupdate = nodeattr2bool(data_node, 'noupdate', False)
1222+ record_nodes = data_node.xpath("./record")
1223+ for record in record_nodes:
1224+ xml_id = record.get("id")
1225+ if ('.' in xml_id
1226+ and xml_id.startswith(addon_name + '.')):
1227+ xml_id = xml_id[len(addon_name) + 1:]
1228+ for records in records_noupdate, records_update:
1229+ # records can occur multiple times in the same module
1230+ # with different noupdate settings
1231+ if xml_id in records:
1232+ # merge records (overwriting an existing element
1233+ # with the same tag). The order processing the
1234+ # various directives from the manifest is
1235+ # important here
1236+ update_node(records[xml_id], record)
1237+ break
1238+ else:
1239+ target_dict = (
1240+ records_noupdate if noupdate else records_update)
1241+ target_dict[xml_id] = record
1242+
1243+ for key in keys:
1244+ if not manifest.get(key):
1245+ continue
1246+ for xml_file in manifest[key]:
1247+ xml_path = xml_file.split('/')
1248+ try:
1249+ tree = etree.parse(os.path.join(addon_dir, *xml_path))
1250+ except etree.XMLSyntaxError:
1251+ continue
1252+ for data_node in tree.xpath("/openerp/data"):
1253+ process_data_node(data_node)
1254+ return records_update, records_noupdate
1255+
1256+def main(argv=None):
1257+ """
1258+ Attempt to represent the differences in data records flagged with
1259+ 'noupdate' between to different versions of the same OpenERP module.
1260+
1261+ Print out a complete XML data file that can be loaded in a post-migration
1262+ script using openupgrade::load_xml().
1263+
1264+ Known issues:
1265+ - Does not detect if a deleted value belongs to a field
1266+ which has been removed.
1267+ - Ignores forcecreate=False. This hardly occurs, but you should
1268+ check manually for new data records with this tag. Note that
1269+ 'True' is the default value for data elements without this tag.
1270+ - Does not take csv data into account (obviously)
1271+ - Is not able to check cross module data
1272+ - etree's pretty_print is not *that* pretty
1273+ - Does not take translations into account (e.g. in the case of
1274+ email templates)
1275+ - Does not handle the shorthand records <menu>, <act_window> etc.,
1276+ although that could be done using the same expansion logic as
1277+ is used in their parsers in openerp/tools/convert.py
1278+ """
1279+ parser = argparse.ArgumentParser()
1280+ parser.add_argument(
1281+ 'olddir', metavar='older_module_directory')
1282+ parser.add_argument(
1283+ 'newdir', metavar='newer_module_directory')
1284+ arguments = parser.parse_args(argv)
1285+
1286+ old_update, old_noupdate = get_records(arguments.olddir)
1287+ new_update, new_noupdate = get_records(arguments.newdir)
1288+
1289+ data = etree.Element("data")
1290+
1291+ for xml_id, record_new in new_noupdate.items():
1292+ record_old = None
1293+ if xml_id in old_update:
1294+ record_old = old_update[xml_id]
1295+ elif xml_id in old_noupdate:
1296+ record_old = old_noupdate[xml_id]
1297+
1298+ if record_old is None:
1299+ continue
1300+
1301+ element = etree.Element(
1302+ "record", id=xml_id, model=record_new.attrib['model'])
1303+ record_old_dict = get_node_dict(record_old)
1304+ record_new_dict = get_node_dict(record_new)
1305+ for key in record_old_dict.keys():
1306+ if not record_new.xpath(key):
1307+ # The element is no longer present.
1308+ # Overwrite an existing value with an
1309+ # empty one. Of course, we do not know
1310+ # if this field has actually been removed
1311+ attribs = deepcopy(record_old_dict[key]).attrib
1312+ for attr in ['eval', 'ref']:
1313+ if attr in attribs:
1314+ del attribs[attr]
1315+ element.append(etree.Element(record_old_dict[key].tag, attribs))
1316+ else:
1317+ oldrepr = get_node_value(record_old_dict[key])
1318+ newrepr = get_node_value(record_new_dict[key])
1319+
1320+ if oldrepr != newrepr:
1321+ element.append(deepcopy(record_new_dict[key]))
1322+
1323+ for key in record_new_dict.keys():
1324+ if not record_old.xpath(key):
1325+ element.append(deepcopy(record_new_dict[key]))
1326+
1327+ if len(element):
1328+ data.append(element)
1329+
1330+ openerp = etree.Element("openerp")
1331+ openerp.append(data)
1332+ document = etree.ElementTree(openerp)
1333+
1334+ print etree.tostring(
1335+ document, pretty_print=True, xml_declaration=True, encoding='utf-8')
1336+
1337+if __name__ == "__main__":
1338+ main()
1339+
1340
1341=== added file 'scripts/migrate.py'
1342--- scripts/migrate.py 1970-01-01 00:00:00 +0000
1343+++ scripts/migrate.py 2014-04-08 14:07:20 +0000
1344@@ -0,0 +1,261 @@
1345+#!/usr/bin/python
1346+
1347+import os
1348+import sys
1349+import StringIO
1350+import psycopg2
1351+import psycopg2.extensions
1352+from optparse import OptionParser
1353+from ConfigParser import SafeConfigParser
1354+from bzrlib.branch import Branch
1355+from bzrlib.repository import Repository
1356+from bzrlib.workingtree import WorkingTree
1357+import bzrlib.plugin
1358+import bzrlib.builtins
1359+import bzrlib.info
1360+
1361+migrations={
1362+ '7.0': {
1363+ 'addons': {
1364+ 'addons': 'lp:openupgrade-addons/7.0',
1365+ 'web': {'url': 'lp:openerp-web/7.0', 'addons_dir': 'addons'},
1366+ },
1367+ 'server': {
1368+ 'url': 'lp:openupgrade-server/7.0',
1369+ 'addons_dir': os.path.join('openerp','addons'),
1370+ 'root_dir': os.path.join(''),
1371+ 'cmd': 'openerp-server --update=all --database=%(db)s '+
1372+ '--config=%(config)s --stop-after-init --no-xmlrpc --no-netrpc',
1373+ },
1374+ },
1375+ '6.1': {
1376+ 'addons': {
1377+ 'addons': 'lp:openupgrade-addons/6.1',
1378+ 'web': {'url': 'lp:openerp-web/6.1', 'addons_dir': 'addons'},
1379+ },
1380+ 'server': {
1381+ 'url': 'lp:openupgrade-server/6.1',
1382+ 'addons_dir': os.path.join('openerp','addons'),
1383+ 'root_dir': os.path.join(''),
1384+ 'cmd': 'openerp-server --update=all --database=%(db)s '+
1385+ '--config=%(config)s --stop-after-init --no-xmlrpc --no-netrpc',
1386+ },
1387+ },
1388+ '6.0': {
1389+ 'addons': {
1390+ 'addons': 'lp:openupgrade-addons/6.0',
1391+ },
1392+ 'server': {
1393+ 'url': 'lp:openupgrade-server/6.0',
1394+ 'addons_dir': os.path.join('bin','addons'),
1395+ 'root_dir': os.path.join('bin'),
1396+ 'cmd': 'bin/openerp-server.py --update=all --database=%(db)s '+
1397+ '--config=%(config)s --stop-after-init --no-xmlrpc --no-netrpc',
1398+ },
1399+ },
1400+}
1401+config = SafeConfigParser()
1402+parser = OptionParser(description="""Migrate script for the impatient or lazy.
1403+Makes a copy of your database, downloads the files necessary to migrate
1404+it as requested and runs the migration on the copy (so your original
1405+database will not be touched). While the migration is running only errors are
1406+shown, for a detailed log see ${branch-dir}/migration.log
1407+""")
1408+parser.add_option("-C", "--config", action="store", type="string",
1409+ dest="config",
1410+ help="current openerp config (required)")
1411+parser.add_option("-D", "--database", action="store", type="string",
1412+ dest="database",
1413+ help="current openerp database (required if not given in config)")
1414+parser.add_option("-B", "--branch-dir", action="store", type="string",
1415+ dest="branch_dir",
1416+ help="the directory to download openupgrade-server code to [%default]",
1417+ default='/var/tmp/openupgrade')
1418+parser.add_option("-R", "--run-migrations", action="store", type="string",
1419+ dest="migrations",
1420+ help="comma separated list of migrations to run, ie. \""+
1421+ ','.join(sorted([a for a in migrations]))+
1422+ "\" (required)")
1423+parser.add_option("-A", "--add", action="store", type="string", dest="add",
1424+ help="load a python module that declares a dict 'migrations' which is "+
1425+ "merged with the one of this script (see the source for details). "
1426+ "You also can pass a string that evaluates to a dict. For the banking "
1427+ "addons, pass "
1428+ "\"{'6.1': {'addons': {'banking': 'lp:banking-addons/6.1'}}}\"")
1429+parser.add_option("-I", "--inplace", action="store_true", dest="inplace",
1430+ help="don't copy database before attempting upgrade (dangerous)")
1431+(options, args) = parser.parse_args()
1432+
1433+if (not options.config or not options.migrations
1434+ or not reduce(lambda a,b: a and (b in migrations),
1435+ options.migrations.split(','), True)):
1436+ parser.print_help()
1437+ sys.exit()
1438+
1439+config.read(options.config)
1440+
1441+conn_parms = {}
1442+for parm in ('host', 'port', 'user', 'password'):
1443+ db_parm = 'db_' + parm
1444+ if config.has_option('options', db_parm):
1445+ conn_parms[parm] = config.get('options', db_parm)
1446+
1447+if not 'user' in conn_parms:
1448+ print 'No user found in configuration'
1449+ sys.exit()
1450+db_user = conn_parms['user']
1451+
1452+db_name=options.database or config.get('options', 'db_name')
1453+
1454+if not db_name or db_name=='' or db_name.isspace() or db_name.lower()=='false':
1455+ parser.print_help()
1456+ sys.exit()
1457+
1458+conn_parms['database'] = db_name
1459+
1460+if options.inplace:
1461+ db=db_name
1462+else:
1463+ db=db_name+'_migrated'
1464+
1465+if options.add:
1466+ merge_migrations={}
1467+ if os.path.isfile(options.add):
1468+ import imp
1469+ merge_migrations_mod=imp.load_source('merge_migrations_mod',
1470+ options.add)
1471+ merge_migrations=merge_migrations_mod.migrations
1472+ else:
1473+ merge_migrations=eval(options.add)
1474+
1475+ def deep_update(dict1, dict2):
1476+ result={}
1477+ for (name,value) in dict1.iteritems():
1478+ if dict2.has_key(name):
1479+ if isinstance(dict1[name], dict) and isinstance(dict2[name],
1480+ dict):
1481+ result[name]=deep_update(dict1[name], dict2[name])
1482+ else:
1483+ result[name]=dict2[name]
1484+ else:
1485+ result[name]=dict1[name]
1486+ for (name,value) in dict2.iteritems():
1487+ if name not in dict1:
1488+ result[name]=value
1489+ return result
1490+
1491+ migrations=deep_update(migrations, merge_migrations)
1492+
1493+for version in options.migrations.split(','):
1494+ if version not in migrations:
1495+ print '%s is not a valid version! (valid verions are %s)' % (version,
1496+ ','.join(sorted([a for a in migrations])))
1497+
1498+bzrlib.plugin.load_plugins()
1499+bzrlib.trace.enable_default_logging()
1500+logfile=os.path.join(options.branch_dir,'migration.log')
1501+
1502+if not os.path.exists(options.branch_dir):
1503+ os.mkdir(options.branch_dir)
1504+
1505+for version in options.migrations.split(','):
1506+ if not os.path.exists(os.path.join(options.branch_dir,version)):
1507+ os.mkdir(os.path.join(options.branch_dir,version))
1508+ for (name,url) in dict(migrations[version]['addons'],
1509+ server=migrations[version]['server']['url']).iteritems():
1510+ link=url.get('link', False) if isinstance(url, dict) else False
1511+ url=url['url'] if isinstance(url, dict) else url
1512+ if os.path.exists(os.path.join(options.branch_dir,version,name)):
1513+ if link:
1514+ continue
1515+ cmd_revno=bzrlib.builtins.cmd_revno()
1516+ cmd_revno.outf=StringIO.StringIO()
1517+ cmd_revno.run(location=os.path.join(options.branch_dir,version,
1518+ name))
1519+ print 'updating %s rev%s' %(os.path.join(version,name),
1520+ cmd_revno.outf.getvalue().strip())
1521+ cmd_update=bzrlib.builtins.cmd_update()
1522+ cmd_update.outf=StringIO.StringIO()
1523+ cmd_update.outf.encoding='utf8'
1524+ cmd_update.run(dir=os.path.join(options.branch_dir,version,
1525+ name))
1526+ if hasattr(cmd_update, '_operation'):
1527+ cmd_update.cleanup_now()
1528+ print 'now at rev'+cmd_revno.outf.getvalue().strip()
1529+ else:
1530+ if link:
1531+ print 'linking %s to %s'%(url,
1532+ os.path.join(options.branch_dir,version,name))
1533+ os.symlink(url, os.path.join(options.branch_dir,version,name))
1534+ else:
1535+ print 'getting '+url
1536+ cmd_checkout=bzrlib.builtins.cmd_checkout()
1537+ cmd_checkout.outf=StringIO.StringIO()
1538+ cmd_checkout.run(url, os.path.join(options.branch_dir,version,
1539+ name), lightweight=True)
1540+
1541+if not options.inplace:
1542+ print('copying database %(db_name)s to %(db)s...' % {'db_name': db_name,
1543+ 'db': db})
1544+ conn = psycopg2.connect(**conn_parms)
1545+ conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
1546+ cur=conn.cursor()
1547+ cur.execute('drop database if exists "%(db)s"' % {'db': db})
1548+ cur.execute('create database "%(db)s"' % {'db': db})
1549+ cur.close()
1550+
1551+ os.environ['PGUSER'] = db_user
1552+ if ('host' in conn_parms and conn_parms['host']
1553+ and not os.environ.get('PGHOST')):
1554+ os.environ['PGHOST'] = conn_parms['host']
1555+
1556+ if ('port' in conn_parms and conn_parms['port']
1557+ and not os.environ.get('PGPORT')):
1558+ os.environ['PGPORT'] = conn_parms['port']
1559+
1560+ password_set = False
1561+ if ('password' in conn_parms and conn_parms['password']
1562+ and not os.environ.get('PGPASSWORD')):
1563+ os.environ['PGPASSWORD'] = conn_parms['password']
1564+ password_set = True
1565+
1566+ os.system(
1567+ ('pg_dump --format=custom --no-password %(db_name)s ' +
1568+ '| pg_restore --no-password --dbname=%(db)s') %
1569+ {'db_name': db_name, 'db': db}
1570+ )
1571+
1572+ if password_set:
1573+ del os.environ['PGPASSWORD']
1574+
1575+for version in options.migrations.split(','):
1576+ print 'running migration for '+version
1577+ config.set('options', 'without_demo', 'True')
1578+ config.set('options', 'logfile', logfile)
1579+ config.set('options', 'port', 'False')
1580+ config.set('options', 'netport', 'False')
1581+ config.set('options', 'xmlrpc_port', 'False')
1582+ config.set('options', 'netrpc_port', 'False')
1583+ config.set('options', 'addons_path',
1584+ ','.join([os.path.join(options.branch_dir,
1585+ version,'server',migrations[version]['server']['addons_dir'])] +
1586+ [
1587+ os.path.join(options.branch_dir,version,name,
1588+ url.get('addons_dir', '') if isinstance(url, dict) else '')
1589+ for (name,url) in migrations[version]['addons'].iteritems()
1590+ ]
1591+ )
1592+ )
1593+ config.set('options', 'root_path', os.path.join(options.branch_dir,version,
1594+ 'server', migrations[version]['server']['root_dir']))
1595+ config.write(open(
1596+ os.path.join(options.branch_dir,version,'server.cfg'), 'w+'))
1597+ os.system(
1598+ os.path.join(options.branch_dir,version,'server',
1599+ migrations[version]['server']['cmd'] % {
1600+ 'db': db,
1601+ 'config': os.path.join(options.branch_dir,version,
1602+ 'server.cfg')
1603+ }
1604+ )
1605+ )

Subscribers

People subscribed via source and target branches

to all changes: