Skip to content
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

Open
wants to merge 26 commits into
base: 14.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
244a729
[ADD] mass_merge
Kiplangatdan Mar 11, 2022
5906862
[MIG] mass_merge: Migration to 14.0
douglas-tabut Mar 14, 2022
c73a859
[IMP] bring merge_editing_product into mass_merge
douglas-tabut Mar 17, 2022
f3d035a
[ADD]Technical feature to merge records from any table (feature from …
douglas-tabut Mar 22, 2022
ed3987d
[ADD] mass_merge
Kiplangatdan Mar 11, 2022
02342f1
[MIG] mass_merge: Migration to 14.0
douglas-tabut Mar 14, 2022
66b7b8a
[IMP] bring merge_editing_product into mass_merge
douglas-tabut Mar 17, 2022
3833506
[ADD]Technical feature to merge records from any table (feature from …
douglas-tabut Mar 22, 2022
89ae06c
[ADD]Features from base_merge/ui migrations complete. python migratio…
douglas-tabut Mar 29, 2022
95841ff
Merge branch '14.0-add-mass-merge' of github.com:sunflowerit/server-u…
douglas-tabut Mar 29, 2022
21af3ac
[ADD] show mass merge button with an action wizard on the chosen model
douglas-tabut Mar 31, 2022
0be31a3
[ADD]Fix value error for ref field to merge records
douglas-tabut Apr 6, 2022
79059ba
[ADD]merge selected records - fix value violates uniq constr product_…
douglas-tabut Apr 6, 2022
9703e21
[ADD]Actual merging of records
douglas-tabut Apr 11, 2022
e51ff22
[FIX] Fixup add merge.dummy Transient model
douglas-tabut Apr 20, 2022
ca2be23
[MIG] Adding explicit security ACLs for transient models
douglas-tabut Apr 21, 2022
d3f675a
[MIG] Merge by record Id migrate Computed fields method.
douglas-tabut Apr 27, 2022
c00cc24
[FIX] Tick destination record for merge
douglas-tabut Apr 27, 2022
f4fbf63
[IMP] Improvements as per https://github.com/sunflowerit/server-ux/pu…
douglas-tabut May 11, 2022
bd460a2
[IMP] Removed size definition from field
douglas-tabut May 11, 2022
b905c6a
[MIG]updated the postprocess method in ir.ui.view
douglas-tabut May 13, 2022
ed75425
[IMP] Remove indentation
douglas-tabut Mar 17, 2024
a4548c1
[FIX] Indentation fixup
douglas-tabut Mar 17, 2024
dfd0271
[FIX]Fixups suggested in PR https://github.com/sunflowerit/server-ux/…
douglas-tabut Mar 17, 2024
410476b
[FIX] Fix indentation
douglas-tabut Mar 19, 2024
f52b545
[REM] Remove customer from res.partner in tests
douglas-tabut Mar 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions mass_merge/README.rst
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)
11 changes: 11 additions & 0 deletions mass_merge/__init__.py
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
30 changes: 30 additions & 0 deletions mass_merge/__manifest__.py
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',
}
30 changes: 30 additions & 0 deletions mass_merge/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
Copy link

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.

# 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()
10 changes: 10 additions & 0 deletions mass_merge/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
Copy link

Choose a reason for hiding this comment

The 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

80 changes: 80 additions & 0 deletions mass_merge/models/base_merge_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
# © 2020 Therp BV <https://therp.nl>
Copy link

Choose a reason for hiding this comment

The 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
Copy link

Choose a reason for hiding this comment

The 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'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add description

_description = 'Base Merge Model'

Choose a reason for hiding this comment

The 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]
20 changes: 20 additions & 0 deletions mass_merge/models/base_merge_model_lines.py
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()
48 changes: 48 additions & 0 deletions mass_merge/models/ir_ui_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
Copy link

Choose a reason for hiding this comment

The 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
64 changes: 64 additions & 0 deletions mass_merge/models/merge_editing.py
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
Loading