-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[14.0] [ADD] mass_merge #1
base: 14.0
Are you sure you want to change the base?
Changes from 18 commits
244a729
5906862
c73a859
f3d035a
ed3987d
02342f1
66b7b8a
3833506
89ae06c
95841ff
21af3ac
0be31a3
79059ba
9703e21
e51ff22
ca2be23
d3f675a
c00cc24
f4fbf63
bd460a2
b905c6a
ed75425
a4548c1
dfd0271
410476b
f52b545
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.png | ||
:target: https://www.gnu.org/licenses/lgpl | ||
:alt: License: LGPL-3 | ||
|
||
Mass Merge Records | ||
================== | ||
This module is a general purpose module that merges records that may be similar | ||
or reflect similar properties when created. | ||
|
||
Merges any number of records from any table. Similar to the base_partner_merge functionality, | ||
but with any table and with an interfaces that permits partial merges. | ||
|
||
|
||
Considerations | ||
-------------- | ||
|
||
Technical Explanation | ||
--------------------- | ||
For demonstration there can exist a customer with two or more record entries that | ||
are same apart from difference in names. Among the records there could be caps | ||
in the name characters. e.g | ||
** 1.) Paul | ||
** 2.) PAul | ||
** 3.) pAuL | ||
the above records are just one "__Paul__" so in this case you can merge the | ||
records into one. | ||
|
||
Functional Usage | ||
---------------- | ||
Go to Settings - Manage user - Tick Mass Merge boolean field to give access. | ||
Go to Settings - Mass Merge - Mass Merge. Create merge for your chosen model. | ||
|
||
Go to Settings - Technical - Merge records. There are two tools: | ||
|
||
* Record Merge by ID: Given n ids from a table, merge them into a single record | ||
* Record Merge by criteria: Given a filter/domain on a table and a criteria to group the filtered records, create the corresponding "Merge by ID" models | ||
|
||
The Merge by ID is the basic tool, in "Draft" state you have to provide: | ||
|
||
* The model | ||
* N ids to merge and mark one of them as destiny (i.e. the record that the others will be merged upon) | ||
|
||
Press Launch to get to the "In progress" state. Several new tabs appear: | ||
|
||
* The first one is the Relation Fields, i.e. the fields that have a relationship with other tables (o2m and m2o), and should/could be considered for possible merging. The tree shows the number of records and a button to create a merge for those records (a Merge by Id in case of m2o relationship and a Merge by Criteria in case of o2m relationship). | ||
* The next four tabs are the steps of the Merge in itself: | ||
|
||
* Data consolidation: Copying the data from the merged records to the destiny one. You can choose which fields to merge and the order of consolidation (i.e. which record data will take prevalence) | ||
* FKs merge: Changing all fks in the database pointing to the merged records to the destiny one. The code of the base_parter_merge is used. You can choose which fields to merge. | ||
* References merge: Changing all reference fields (e.g. field value = res.partner, 145) to the destiny one. You can choose which fields to merge. | ||
* Non-relational merge: Changing all reference fields (two fields, one with model = res.partner, other res_id = 145) to the destiny one. You can choose which fields to merge. | ||
|
||
* The last two tabs are the steps to consider after the merge: | ||
|
||
* Recompute: List of computed fields that are stored and could need a recompute. You can choose which fields to recompute. | ||
* Delete merged: What to do with the merged records (not the destiny). You can delete (launch unlink), deactivate (write active False), recompute (all compute stored fields are recomputed), SQL delete (warning, just in case the ORM does not let us), or leave them (None). | ||
|
||
A Merge Button is provided to launch everything in the correct order, getting to the Done state. | ||
|
||
The Merge by Criteria is a process to generate the Merge by ID automatically. In "Draft" state you have to provide: | ||
|
||
* The model | ||
* A filter: A domain to get all records that will be merged | ||
* A group key: A python expression that will give the key that groups the records resulting of the filter in different merges | ||
* An order: an optional _order expression to apply in each merge group to determine the destiny one. If none is provided, the table _order is used | ||
|
||
Press Start to get to the "In progress" state. Several new tabs appear: | ||
* Merge groups: A list of all the merges that will be generated, presenting the destiny id. You can choose which groups will be merged | ||
* The same six tabs that appear in the Merge by ID model, and that will be used as default values for the generated groups (i.e. you can configure all the mergings before they are created) | ||
|
||
Press Create merges to generate all the Merge by IDs records and get to the Done state. Two buttons let you launch processes on all the groups: | ||
* Merge all groups | ||
* Cancel all groups | ||
|
||
Be warned: This module is both dangerous and useful, you can solve big problems in a very small time but you can create bigger problems as easily. Test any merges in a duplicate database before doing anything in production. | ||
|
||
Example: There are two projects that should be merged, as they are really the same project. | ||
|
||
* We start creating the Merge by ID on the project.project model, providing the ids | ||
* The relation fields tab warns us that we should consider many models: | ||
|
||
* Project.task: Both projects come from a template, so they each have 15 tasks that start with the same code. We press the button + in the task field lines to create a Merge by criteria. The filter is already filled, so we only need a group key. In this case the task should be merged two by two by the code (6 first letters of the name), so it would be: o.name[:6] . We launch all the merges | ||
* Other models: The analytic account / contract, sale order, sale order line... | ||
|
||
With this module all this merge is done in minutes. | ||
|
||
Also, this module could be inherited to create interfaces for the final user for a certain model (e.g. product merge). | ||
|
||
|
||
Known issues / Roadmap | ||
---------------------- | ||
|
||
- Tests: merge 2, 3 countries; check if partner countries have changed, | ||
check if ir_model_data was updated | ||
- On merge wizard, show a visual link that opens the ref in a pop window | ||
(such as web_tree_many2one_clickable offers) | ||
- Generic support for '_inherits' models: when one is merged, merge the other | ||
(now we only have specific support for product.product/product.template) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# coding: utf-8 | ||
# OpenERP, Open Source Management Solution | ||
# Copyright (C) 2012 Serpent Consulting Services (<http://www.serpentcs.com>) | ||
# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>) | ||
# Copyright (c) 2019-Today Sunflower IT (<https://www.sunflowerweb.nl>) | ||
# License GNU General Public License see <http://www.gnu.org/licenses/> | ||
|
||
from . import models | ||
from . import tools | ||
from . import wizard | ||
from .hooks import post_init_hook |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# coding: utf-8 | ||
# Copyright (C) 2010-2019 Today OpenERP SA (<http://www.openerp.com>) | ||
# Sunflower IT (<https://www.sunflowerweb.nl>) | ||
# programmed by: Oscar Alcala: [email protected] | ||
# programmed by: Jose Morales: [email protected] | ||
# programmed by: Sunflower IT: [email protected] | ||
# License GNU General Public License see <http://www.gnu.org/licenses/> | ||
{ | ||
"name": "Mass Merge Records", | ||
"version": "14.0.1.0.1", | ||
"author": "Vauxoo", | ||
"category": "Tools", | ||
"website": "http://www.serpentcs.com", | ||
"license": "AGPL-3", | ||
'depends': [ | ||
'base', 'stock' | ||
], | ||
'data': [ | ||
'security/merge_security.xml', | ||
'security/ir.model.access.csv', | ||
'wizard/merge_fuse_wizard.xml', | ||
'views/merge_editing_view.xml', | ||
'views/record_merge_id.xml', | ||
'views/record_merge_criteria.xml', | ||
'views/base_merge_views.xml', | ||
], | ||
'installable': True, | ||
'application': True, | ||
'post_init_hook': 'post_init_hook', | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# -*- coding: utf-8 -*- | ||
# Copyright 2019 Sunflower IT <http://sunflowerweb.nl> | ||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). | ||
|
||
from odoo import api, SUPERUSER_ID | ||
|
||
|
||
def post_init_hook(cr, pool): | ||
create_product_merge_action(cr) | ||
|
||
|
||
def create_product_merge_action(cr): | ||
env = api.Environment(cr, SUPERUSER_ID, {}) | ||
merge_obj = env['merge.object'] | ||
|
||
# Create product.product merge action | ||
wizard = merge_obj.new() | ||
wizard.name = 'Merge product.product'; | ||
wizard.model_id = env.ref('product.model_product_product') | ||
wizard.onchange_model() | ||
ppmerge = merge_obj.create(wizard._convert_to_write(wizard._cache)) | ||
ppmerge.create_action_fuse() | ||
|
||
# create product.template merge action | ||
wizard = merge_obj.new() | ||
wizard.name = 'Merge product.template'; | ||
wizard.model_id = env.ref('product.model_product_template') | ||
wizard.onchange_model() | ||
ptmerge = merge_obj.create(wizard._convert_to_write(wizard._cache)) | ||
ptmerge.create_action_fuse() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# -*- coding: utf-8 -*- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In Odoo 14.0 (since 11.0) we should no longer have coding lines in python files. |
||
|
||
from . import merge_editing | ||
from . import record_merge_mixin | ||
from . import record_merge_id | ||
from . import record_merge_criteria | ||
from . import base_merge_model | ||
from . import base_merge_model_lines | ||
from . import ir_ui_view | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# -*- coding: utf-8 -*- | ||
# © 2020 Therp BV <https://therp.nl> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use the word Copyright, not a character |
||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
from odoo import _, api, fields, models | ||
|
||
|
||
# This class holds configuration about which models | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make this into a proper class doc comment. |
||
# are allowed to be merged, and which | ||
# fields are to be used to to match | ||
class BaseMergeModel(models.Model): | ||
|
||
_name = 'base.merge.model' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add description
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Description Added |
||
|
||
_sql_constraints = [ | ||
( | ||
'model_id_unique', | ||
'UNIQUE (model_id)', | ||
_('A record for this model already exists'), | ||
), | ||
] | ||
|
||
model_id = fields.Many2one( | ||
comodel_name='ir.model', | ||
string="Model", | ||
ondelete='cascade', | ||
required=True) | ||
field_ids = fields.One2many( | ||
comodel_name='base.merge.model.line', | ||
inverse_name='merge_model_id') | ||
action_id = fields.Many2one( | ||
comodel_name='ir.actions.act_window') | ||
|
||
def action_find_duplicates(self): | ||
""" | ||
Use the fields defined in self to find the first set of duplicates, | ||
return an action that starts the merge wizard | ||
with the good context keys. This function can be used in | ||
server actions to start an interactive deduplication | ||
""" | ||
return NotImplementedError(_('Currently not available')) | ||
|
||
@api.model | ||
def create(self, vals): | ||
# Create proper action and ir.values records | ||
# This enables merging for model_id | ||
record = super(BaseMergeModel, self).create(vals) | ||
record.action_id = self.env['ir.actions.act_window'].create({ | ||
'name': 'Record merger', | ||
'type': 'ir.actions.act_window', | ||
'res_model': 'base.merge.wizard', | ||
'binding_model_id': record.model_id.id, | ||
'view_mode': 'form', | ||
'target': 'new', | ||
}) | ||
self.env['ir.values'].set_action( | ||
_('Merge action for %s') % record.model_id.model, | ||
'client_action_multi', | ||
record.model_id.model, | ||
'%s, %d' % (record.action_id.type, record.action_id.id)) | ||
return record | ||
|
||
def unlink(self): | ||
value_model = self.env['ir.values'] | ||
for this in self: | ||
# Unlink ir.values | ||
value_model.search([ | ||
('action_id', '=', this.action_id.id), | ||
('model_id', '=', this.model_id.id), | ||
]).unlink() | ||
# Unlink the action | ||
this.action_id.unlink() | ||
# Unlink in general | ||
return super(BaseMergeModel, self).unlink() | ||
|
||
def name_get(self): | ||
return [( | ||
merger_record.id, | ||
'Merging enabled for %s' % ( | ||
merger_record.model_id.name or '', | ||
)) for merger_record in self] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# -*- coding: utf-8 -*- | ||
# © 2020 Therp BV <https://therp.nl> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
from odoo import fields, models | ||
|
||
|
||
class BaseMergeModelLine(models.Model): | ||
|
||
_name = 'base.merge.model.line' | ||
douglas-tabut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
merge_model_id = fields.Many2one( | ||
douglas-tabut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
required=True) | ||
field_id = fields.Many2one( | ||
comodel_name='ir.model.fields', | ||
ondelete='cascade', | ||
required=True) | ||
# future extensions | ||
# operator = fields.Selected( | ||
# [('=', 'Strict equal'), ('=ilike', 'Case sensitive equal')]) | ||
# domain = fields.Text() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# -*- coding: utf-8 -*- | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the need for this file? Just one call to a super method without any extra action, all arguments passed unchanged... |
||
# © 2020 Therp BV <https://therp.nl> | ||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). | ||
from odoo import models | ||
|
||
|
||
class View(models.Model): | ||
|
||
_inherit = 'ir.ui.view' | ||
|
||
def postprocess_deactivated( | ||
douglas-tabut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self, | ||
model, | ||
node, | ||
view_id, | ||
in_tree_view, | ||
model_fields | ||
): | ||
dict_o_values = { | ||
'change_default': False, | ||
'company_dependent': False, | ||
'context': {}, | ||
'depends': (), | ||
'domain': [], | ||
'manual': False, | ||
'readonly': False, | ||
'required': False, | ||
'searchable': False, | ||
'sortable': False, | ||
'store': False, | ||
} | ||
if model == 'base.merge.wizard' and model_fields: | ||
active_model = self.env.context.get('active_model') or 'ir.model' | ||
model_fields['target_id'] = dict_o_values.copy() | ||
model_fields['target_id']['relation'] = active_model | ||
model_fields['target_id']['type'] = 'many2one' | ||
model_fields['target_id']['string'] = 'Target model' | ||
model_fields['source_ids'] = dict_o_values.copy() | ||
model_fields['source_ids']['relation'] = active_model | ||
model_fields['source_ids']['type'] = 'many2many' | ||
model_fields['source_ids']['string'] = 'Target model records' | ||
result = super(View, self).postprocess( | ||
model=model, | ||
node=node, | ||
view_id=view_id, | ||
in_tree_view=in_tree_view, | ||
model_fields=model_fields) | ||
return result |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# coding: utf-8 | ||
# License GNU General Public License see <http://www.gnu.org/licenses/> | ||
from odoo import fields, models, api, _ | ||
from odoo.exceptions import UserError | ||
|
||
|
||
class MergeObject(models.Model): | ||
_name = "merge.object" | ||
|
||
name = fields.Char("Name", size=64, required=True, index=True) | ||
douglas-tabut marked this conversation as resolved.
Show resolved
Hide resolved
|
||
model_id = fields.Many2one('ir.model', 'Model', required=True, index=True, ondelete='cascade') | ||
ref_ir_act_server_fuse = fields.Many2one( | ||
'ir.actions.server', | ||
'Sidebar fuse server action', readonly=True, | ||
help="Sidebar action to make this template\ | ||
available on records of the related document\ | ||
model") | ||
model_list = fields.Char('Model List', size=256) | ||
|
||
@api.onchange('model_id') | ||
def onchange_model(self): | ||
if self.model_id: | ||
model_obj = self.env['ir.model'] | ||
model_data = model_obj.browse(self.model_id.id) | ||
self.model_list = "[" + str(self.model_id) + "" | ||
active_model_obj = self.env[model_data.model] | ||
if active_model_obj._inherits: | ||
for key, val in active_model_obj._inherits.items(): | ||
model_ids = model_obj.search([('model', '=', key)]) | ||
if model_ids: | ||
self.model_list += "," + str(model_ids[0]) + "" | ||
self.model_list += "]" | ||
|
||
def create_action_fuse(self): | ||
vals = {} | ||
action_obj = self.env['ir.actions.server'] | ||
for data in self: | ||
src_obj = data.model_id.model | ||
button_name = _('Mass Fuse (%s)') % data.name | ||
vals['ref_ir_act_server_fuse'] = action_obj.create({ | ||
'name': button_name, | ||
'type': 'ir.actions.server', | ||
'state': 'code', | ||
'code': 'action = model._get_wizard_action()', | ||
'model_id': self.env.ref( | ||
'mass_merge.model_merge_fuse_wizard').id, | ||
'binding_model_id': data.model_id.id, | ||
# 'binding_type': 'report', | ||
}) | ||
|
||
self.write({ | ||
'ref_ir_act_server_fuse': vals.get('ref_ir_act_server_fuse', False).id | ||
}) | ||
return True | ||
|
||
def unlink_fuse_action(self): | ||
for template in self: | ||
try: | ||
if template.ref_ir_act_server_fuse: | ||
self.env['ir.actions.server'].search( | ||
[('id', '=', template.ref_ir_act_server_fuse.id)]).unlink() | ||
except: | ||
raise UserError(_("Deletion of the action record failed.")) | ||
return True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens in this init hook is more properly done by a data xml, or even better as demo data.