You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

202 lines
8.1 KiB

  1. # -*- coding: utf-8 -*-
  2. ##############################################################################
  3. #
  4. # Contributor: David Dufresne <david.dufresne@savoirfairelinux.com>
  5. # Sandy Carter <sandy.carter@savoirfairelinux.com>
  6. #
  7. # This program is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU Affero General Public License as
  9. # published by the Free Software Foundation, either version 3 of the
  10. # License, or (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU Affero General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU Affero General Public License
  18. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  19. #
  20. ##############################################################################
  21. from openerp import pooler, SUPERUSER_ID
  22. from itertools import groupby
  23. from operator import attrgetter
  24. def remove_sql_constraint_duplicates(cr, model, constraint_attrs):
  25. """
  26. This function was copied from OpenUpgrade
  27. Remove all duplicates after a sql constraint is applied on a model.
  28. For every field many2one and many2many with the given model as relation,
  29. change the duplicate ids with the id of the record kept.
  30. This script must be called in post-migration so that the model being
  31. edited can be accessed through the orm.
  32. When upgrading the module, if there are duplicates, integrity errors
  33. will be raised before the script is run but this will not prevent
  34. the script from running.
  35. :param model: the model on witch the constraint is applied
  36. :param constraint_attrs: a list of string containing the fields that
  37. form the uniq key
  38. """
  39. pool = pooler.get_pool(cr.dbname)
  40. model_pool = pool[model]
  41. model_table = model_pool._table
  42. # Get all fields with the given model as many2one relation
  43. field_pool = pool['ir.model.fields']
  44. field_m2o_ids = field_pool.search(cr, SUPERUSER_ID, [
  45. ('relation', '=', model),
  46. ('ttype', '=', 'many2one'),
  47. ])
  48. # List of tables where to look for duplicates
  49. # This is trivial for many2one relations
  50. tables_to_lookup = [
  51. (
  52. pool[field.model_id.model]._table,
  53. field.name, 'many2one'
  54. ) for field in field_pool.browse(cr, SUPERUSER_ID, field_m2o_ids)
  55. ]
  56. # For many2many relations, we need to check over the existing
  57. # foreign keys in the database in order to find the tables
  58. # Get all fields with the given model as many2many relation
  59. field_m2m_ids = field_pool.search(cr, SUPERUSER_ID, [
  60. ('relation', '=', model),
  61. ('ttype', '=', 'many2many'),
  62. ])
  63. fields_m2m = field_pool.browse(cr, SUPERUSER_ID, field_m2m_ids)
  64. for field in fields_m2m:
  65. other_model_table = pool[field.model_id.model]._table
  66. # Get all primary key constraints for the given table
  67. query = "SELECT " \
  68. " tc.table_name, kcu.column_name, ccu.table_name " \
  69. "FROM " \
  70. " information_schema.table_constraints AS tc " \
  71. " JOIN information_schema.key_column_usage AS kcu " \
  72. " ON tc.constraint_name = kcu.constraint_name " \
  73. " JOIN information_schema.constraint_column_usage AS ccu " \
  74. " ON ccu.constraint_name = tc.constraint_name " \
  75. "WHERE constraint_type = 'FOREIGN KEY' " \
  76. " and ccu.table_name " \
  77. " in ('%(model_table)s', '%(other_model_table)s') " \
  78. " ORDER BY tc.table_name;" % {
  79. 'model_table': model_table,
  80. 'other_model_table': other_model_table
  81. }
  82. cr.execute(query)
  83. for key, group in groupby(cr.fetchall(), key=lambda c: c[0]):
  84. constraints = list(group)
  85. model_field = next(
  86. (c[1] for c in constraints if c[2] == model_table), False)
  87. other_field = next(
  88. (c[1] for c in constraints if c[2] == other_model_table), False
  89. )
  90. if model_field and other_field:
  91. # Add the current table to the list of tables where to look
  92. # for duplicates
  93. tables_to_lookup.append((
  94. key, model_field, 'many2many', other_field))
  95. # Get all records
  96. record_ids = model_pool.search(cr, SUPERUSER_ID, [])
  97. records = model_pool.browse(cr, SUPERUSER_ID, record_ids)
  98. # Sort records by the constraint attributes
  99. # so that they can be grouped with itertools.groupby
  100. records.sort(key=attrgetter(*constraint_attrs))
  101. for key, group in groupby(records, key=lambda x: tuple(
  102. x[attr] for attr in constraint_attrs)
  103. ):
  104. grouped_records = list(group)
  105. if len(grouped_records) > 1:
  106. # Define a record to keep
  107. new_record_id = grouped_records[0].id
  108. # All other records are to remove
  109. old_record_ids = [z.id for z in grouped_records[1:]]
  110. all_record_ids = old_record_ids + [new_record_id]
  111. # Replace every many2one record in the database that has an old
  112. # record as value with the record to keep
  113. for table in tables_to_lookup:
  114. table_name = table[0]
  115. # Prevent the upgrade script to create duplicates
  116. # in the many2many relation table and raise a constraint error
  117. if table[2] == 'many2many':
  118. cr.execute(
  119. " SELECT t.%(other_field)s, t.%(field_name)s "
  120. " FROM %(table_name)s as t"
  121. " WHERE %(field_name)s "
  122. " in %(all_record_ids)s "
  123. " ORDER BY %(other_field)s" %
  124. {
  125. 'table_name': table_name,
  126. 'field_name': table[1],
  127. 'other_field': table[3],
  128. 'all_record_ids': tuple(all_record_ids),
  129. })
  130. for k, group_to_check in groupby(
  131. cr.fetchall(), lambda rec: rec[0]
  132. ):
  133. group_to_check = list(group_to_check)
  134. if len(group_to_check) > 1:
  135. for rec_to_unlink in group_to_check[1:]:
  136. cr.execute(
  137. " DELETE FROM %(table_name)s "
  138. " WHERE %(field_name)s = %(field_value)s"
  139. " AND %(other_field)s "
  140. " = %(other_field_value)s" %
  141. {
  142. 'table_name': table_name,
  143. 'field_name': table[1],
  144. 'field_value': rec_to_unlink[1],
  145. 'other_field': table[3],
  146. 'other_field_value': rec_to_unlink[0],
  147. })
  148. # Main upgrade script
  149. cr.execute(
  150. " UPDATE %(table_name)s"
  151. " SET %(field_name)s = %(new_value)s"
  152. " WHERE %(field_name)s %(old_record_ids)s;" %
  153. {
  154. 'table_name': table_name,
  155. 'field_name': table[1],
  156. 'new_value': new_record_id,
  157. 'old_record_ids': len(old_record_ids) > 1 and
  158. 'in %s' % (tuple(old_record_ids),) or '= %s' %
  159. old_record_ids[0]
  160. })
  161. model_pool.unlink(cr, SUPERUSER_ID, old_record_ids)
  162. def migrate(cr, version):
  163. """
  164. Remove duplicated locations
  165. """
  166. if not version:
  167. return
  168. constraint_attrs = ['name', 'city', 'state_id', 'country_id']
  169. remove_sql_constraint_duplicates(
  170. cr, 'res.better.zip', constraint_attrs)