From ce4fa016546b39920356989b96d5fa32aa290e7d Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Fri, 31 May 2013 09:53:13 +0200 Subject: [PATCH 01/16] [ADD] email_template_template --- email_template_template/__init__.py | 1 + email_template_template/__openerp__.py | 52 ++++++++++++++ email_template_template/model/__init__.py | 21 ++++++ .../model/email_template.py | 61 ++++++++++++++++ .../view/email_template.xml | 71 +++++++++++++++++++ 5 files changed, 206 insertions(+) create mode 100644 email_template_template/__init__.py create mode 100644 email_template_template/__openerp__.py create mode 100644 email_template_template/model/__init__.py create mode 100644 email_template_template/model/email_template.py create mode 100644 email_template_template/view/email_template.xml diff --git a/email_template_template/__init__.py b/email_template_template/__init__.py new file mode 100644 index 000000000..16e8b082f --- /dev/null +++ b/email_template_template/__init__.py @@ -0,0 +1 @@ +import model diff --git a/email_template_template/__openerp__.py b/email_template_template/__openerp__.py new file mode 100644 index 000000000..ed8de2be1 --- /dev/null +++ b/email_template_template/__openerp__.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Templates for email templates", + "version": "1.0", + "author": "Therp BV", + "category": 'Tools', + 'complexity': "expert", + "description": """If an organisation's email layout is a bit more +complicated, changes can be tedious when having to do that across several email +templates. So this addon allows to define templates for mails that is referenced +by other mail templates. +This way we can put the layout parts into the template template and only content +in the other templates. Changing the layout is then only a matter of changing +the template template. + + +Usage: +Create an email template with the related document model 'Email Templates'. Now +most of the fields gray out and you can only edit body_text and body_html. Be +sure to use ${body_text} and ${body_html} respectively in your template +template. + +Then select this newly created template templates in one of your actual +templates.""", + 'website': 'http://therp.nl', + 'images': [], + 'depends': ['email_template'], + 'data': [ + 'view/email_template.xml', + ], + "license": 'AGPL-3', +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/email_template_template/model/__init__.py b/email_template_template/model/__init__.py new file mode 100644 index 000000000..90f325845 --- /dev/null +++ b/email_template_template/model/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import email_template diff --git a/email_template_template/model/email_template.py b/email_template_template/model/email_template.py new file mode 100644 index 000000000..2d1b2091c --- /dev/null +++ b/email_template_template/model/email_template.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from openerp.osv.orm import Model +from openerp.osv import fields +from openerp.addons.email_template.email_template import mako_template_env + + +class email_template(Model): + _inherit = 'email.template' + + def _get_is_template_template(self, cr, uid, ids, fields_name, arg, + context=None): + cr.execute('''select + id, (select count(*) > 0 from email_template e + where email_template_id=email_template.id) + from email_template + where id in %s''', (tuple(ids),)) + return dict(cr.fetchall()) + + _columns = { + 'email_template_id': fields.many2one('email.template', 'Template'), + 'is_template_template': fields.function( + _get_is_template_template, type='boolean', + string='Is a template template'), + } + + def get_email_template(self, cr, uid, template_id=False, record_id=None, + context=None): + this = super(email_template, self).get_email_template( + cr, uid, template_id, record_id, context) + + if this.email_template_id and not this.is_template_template: + for field in ['body_html']: + if this[field] and this.email_template_id[field]: + try: + mako_template_env.autoescape = False + this._data[this.id][field] = self.render_template( + cr, uid, this.email_template_id[field], + this.email_template_id.model, + this.id, this._context) + finally: + mako_template_env.autoescape = True + return this diff --git a/email_template_template/view/email_template.xml b/email_template_template/view/email_template.xml new file mode 100644 index 000000000..e38cd3c76 --- /dev/null +++ b/email_template_template/view/email_template.xml @@ -0,0 +1,71 @@ + + + + + email.template.form + email.template + + form + + + + + + + + + + {'readonly': [('is_template_template','=',True)]} + + + + 0 + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + 0 + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + 0 + + {'readonly': ['|',('is_template_template','=',True),('model_id', '=', %(email_template.model_email_template)s)]} + + + + + + + From ee6095e63ecd9b8da337e0df5499af4909adc63a Mon Sep 17 00:00:00 2001 From: Lorenzo Battistini Date: Wed, 12 Jun 2013 18:30:41 +0200 Subject: [PATCH 02/16] [add] base_optional_quick_create --- base_optional_quick_create/AUTHORS.txt | 1 + base_optional_quick_create/__init__.py | 20 ++++++++++ base_optional_quick_create/__openerp__.py | 39 +++++++++++++++++++ base_optional_quick_create/model.py | 46 +++++++++++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 base_optional_quick_create/AUTHORS.txt create mode 100644 base_optional_quick_create/__init__.py create mode 100644 base_optional_quick_create/__openerp__.py create mode 100644 base_optional_quick_create/model.py diff --git a/base_optional_quick_create/AUTHORS.txt b/base_optional_quick_create/AUTHORS.txt new file mode 100644 index 000000000..7106ca0eb --- /dev/null +++ b/base_optional_quick_create/AUTHORS.txt @@ -0,0 +1 @@ +Lorenzo Battistini diff --git a/base_optional_quick_create/__init__.py b/base_optional_quick_create/__init__.py new file mode 100644 index 000000000..44563ef86 --- /dev/null +++ b/base_optional_quick_create/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2013 Agile Business Group sagl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import model diff --git a/base_optional_quick_create/__openerp__.py b/base_optional_quick_create/__openerp__.py new file mode 100644 index 000000000..5d0f9f6a0 --- /dev/null +++ b/base_optional_quick_create/__openerp__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2013 Agile Business Group sagl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': "Optional quick create", + 'version': '0.1', + 'category': 'Tools', + 'description': """ + +""", + 'author': 'Agile Business Group', + 'website': 'http://www.agilebg.com', + 'license': 'AGPL-3', + "depends": ['base'], + "data": [ + ], + "demo": [], + 'test': [ + ], + "active": False, + "installable": True +} diff --git a/base_optional_quick_create/model.py b/base_optional_quick_create/model.py new file mode 100644 index 000000000..c0c9229c0 --- /dev/null +++ b/base_optional_quick_create/model.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Copyright (C) 2013 Agile Business Group sagl () +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import orm + +class ir_model(orm.Model): + + _inherit = 'ir.model' + + def _wrap_name_create(self, old_create, model): + + def wrapper(cr, uid, name, context=None): + import pdb; pdb.set_trace() + return old_create(cr, uid, name, context=context) + + return wrapper + + def _register_hook(self, cr, ids=None): + model = 'res.partner' + model_obj = self.pool.get(model) + if not hasattr(model_obj, 'check_quick_create'): + model_obj.name_create = self._wrap_name_create(model_obj.name_create, model) + model_obj.check_quick_create = True + return True + + def name_create(self, cr, uid, name, context=None): + res = super(ir_model, self).name_create(cr, uid, name, context=context) + self._register_hook(cr, [res]) + return res From ee58f34b79f7c3bf6b50f4e631bfdcce5dc08fb3 Mon Sep 17 00:00:00 2001 From: Lorenzo Battistini Date: Wed, 12 Jun 2013 18:45:49 +0200 Subject: [PATCH 03/16] [add] working version --- base_optional_quick_create/__openerp__.py | 2 ++ base_optional_quick_create/model.py | 41 +++++++++++++++-------- base_optional_quick_create/model_view.xml | 14 ++++++++ 3 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 base_optional_quick_create/model_view.xml diff --git a/base_optional_quick_create/__openerp__.py b/base_optional_quick_create/__openerp__.py index 5d0f9f6a0..d4ee51218 100644 --- a/base_optional_quick_create/__openerp__.py +++ b/base_optional_quick_create/__openerp__.py @@ -23,6 +23,7 @@ 'version': '0.1', 'category': 'Tools', 'description': """ +https://twitter.com/nbessi/status/337869826028605441 """, 'author': 'Agile Business Group', @@ -30,6 +31,7 @@ 'license': 'AGPL-3', "depends": ['base'], "data": [ + 'model_view.xml', ], "demo": [], 'test': [ diff --git a/base_optional_quick_create/model.py b/base_optional_quick_create/model.py index c0c9229c0..ad7b2142c 100644 --- a/base_optional_quick_create/model.py +++ b/base_optional_quick_create/model.py @@ -18,29 +18,42 @@ # ############################################################################## -from openerp.osv import orm +from openerp.osv import orm, fields +from openerp import SUPERUSER_ID class ir_model(orm.Model): _inherit = 'ir.model' + + _columns = { + 'avoid_quick_create': fields.boolean('Avoid quick create'), + } def _wrap_name_create(self, old_create, model): - def wrapper(cr, uid, name, context=None): - import pdb; pdb.set_trace() - return old_create(cr, uid, name, context=context) - + raise Exception("Can't create quickly. Opening create form") return wrapper def _register_hook(self, cr, ids=None): - model = 'res.partner' - model_obj = self.pool.get(model) - if not hasattr(model_obj, 'check_quick_create'): - model_obj.name_create = self._wrap_name_create(model_obj.name_create, model) - model_obj.check_quick_create = True + if ids is None: + ids = self.search(cr, SUPERUSER_ID, []) + for model in self.browse(cr, SUPERUSER_ID, ids): + if model.avoid_quick_create: + model_name = model.model + model_obj = self.pool.get(model_name) + if not hasattr(model_obj, 'check_quick_create'): + model_obj.name_create = self._wrap_name_create(model_obj.name_create, model_name) + model_obj.check_quick_create = True return True - def name_create(self, cr, uid, name, context=None): - res = super(ir_model, self).name_create(cr, uid, name, context=context) - self._register_hook(cr, [res]) - return res + def create(self, cr, uid, vals, context=None): + res_id = super(ir_model, self).create(cr, uid, vals, context=context) + self._register_hook(cr, [res_id]) + return res_id + + def write(self, cr, uid, ids, vals, context=None): + if isinstance(ids, (int, long)): + ids = [ids] + super(ir_model, self).write(cr, uid, ids, vals, context=context) + self._register_hook(cr, ids) + return True diff --git a/base_optional_quick_create/model_view.xml b/base_optional_quick_create/model_view.xml new file mode 100644 index 000000000..e1d11126b --- /dev/null +++ b/base_optional_quick_create/model_view.xml @@ -0,0 +1,14 @@ + + + + + ir.model + + + + + + + + + From 030b2a5c913f39895e282114ba209c3e859eba52 Mon Sep 17 00:00:00 2001 From: Lorenzo Battistini Date: Wed, 12 Jun 2013 19:05:17 +0200 Subject: [PATCH 04/16] [imp] descr --- base_optional_quick_create/__openerp__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/base_optional_quick_create/__openerp__.py b/base_optional_quick_create/__openerp__.py index d4ee51218..568dc1746 100644 --- a/base_optional_quick_create/__openerp__.py +++ b/base_optional_quick_create/__openerp__.py @@ -23,8 +23,10 @@ 'version': '0.1', 'category': 'Tools', 'description': """ -https://twitter.com/nbessi/status/337869826028605441 +This module allows to avoid to 'quick create' new records, through many2one fields, for a specific model. +You can configure which models should allow 'quick create'. When specified, 'quick create' option will always open the standard create form. +Got the idea from https://twitter.com/nbessi/status/337869826028605441 """, 'author': 'Agile Business Group', 'website': 'http://www.agilebg.com', From 7ed95427de28e0cc1843a0a7751e5d01a102be6b Mon Sep 17 00:00:00 2001 From: Lorenzo Battistini Date: Wed, 12 Jun 2013 19:08:49 +0200 Subject: [PATCH 05/16] [fix] descr --- base_optional_quick_create/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base_optional_quick_create/__openerp__.py b/base_optional_quick_create/__openerp__.py index 568dc1746..2b2a3f8ad 100644 --- a/base_optional_quick_create/__openerp__.py +++ b/base_optional_quick_create/__openerp__.py @@ -24,7 +24,7 @@ 'category': 'Tools', 'description': """ This module allows to avoid to 'quick create' new records, through many2one fields, for a specific model. -You can configure which models should allow 'quick create'. When specified, 'quick create' option will always open the standard create form. +You can configure which models should allow 'quick create'. When specified, the 'quick create' option will always open the standard create form. Got the idea from https://twitter.com/nbessi/status/337869826028605441 """, From 414316fdcc0aede1bee05d673d69d8bab7e785a5 Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Mon, 17 Jun 2013 11:03:49 +0200 Subject: [PATCH 06/16] [IMP] add a usage example in the description --- email_template_template/__openerp__.py | 46 +++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/email_template_template/__openerp__.py b/email_template_template/__openerp__.py index ed8de2be1..6f32b3da0 100644 --- a/email_template_template/__openerp__.py +++ b/email_template_template/__openerp__.py @@ -40,7 +40,51 @@ sure to use ${body_text} and ${body_html} respectively in your template template. Then select this newly created template templates in one of your actual -templates.""", +templates. + +For example, create a template template +----- +Example Corp logo +Example Corp header +${object.body_text} <- this gets evaluated to the body_text of a template using this template template +Example Corp +Example street 42 +Example city +Example Corp footer +----- + +Then in your template you write + +----- +Dear ${object.partner_id.name}, + +Your order has been booked on date ${object.date} for a total amount of ${object.sum}. +----- + +And it will be evaluated to + +----- +Example Corp logo +Example Corp header +Dear Jane Doe, + +Your order has been booked on date 04/17/2013 for a total amount of 42. +Example Corp +Example street 42 +Example city +Example Corp footer +----- + +Given the way evaluation works internally (body_text of the template template is evaluated two times, first with the instance of email.template of your own template, then with the object your template refers to), you can do some trickery if you know that a template template is always used with the same kind of model (that is, models that have the same field name): + +In your template template: + +------ +Dear ${'${object.name}'}, <-- gets evaluated to "${object.name}" in the first step, then to the content of object.name +${object.body_html} +Best, +Example Corp +------""", 'website': 'http://therp.nl', 'images': [], 'depends': ['email_template'], From 3363d32f051baff3279359b86025df9a0be636dd Mon Sep 17 00:00:00 2001 From: Jose Morales Date: Mon, 17 Jun 2013 16:31:38 -0530 Subject: [PATCH 07/16] [ADD] Added new modudel with which we can merge duplicate partner --- partner_do_merge/__init__.py | 2 + partner_do_merge/__openerp__.py | 67 ++ partner_do_merge/model/__init__.py | 1 + partner_do_merge/model/partner.py | 109 +++ partner_do_merge/wizard/__init__.py | 1 + partner_do_merge/wizard/base_partner_merge.py | 761 ++++++++++++++++++ .../wizard/base_partner_merge_view.xml | 123 +++ partner_do_merge/wizard/validate_email.py | 123 +++ 8 files changed, 1187 insertions(+) create mode 100644 partner_do_merge/__init__.py create mode 100644 partner_do_merge/__openerp__.py create mode 100644 partner_do_merge/model/__init__.py create mode 100644 partner_do_merge/model/partner.py create mode 100644 partner_do_merge/wizard/__init__.py create mode 100644 partner_do_merge/wizard/base_partner_merge.py create mode 100644 partner_do_merge/wizard/base_partner_merge_view.xml create mode 100644 partner_do_merge/wizard/validate_email.py diff --git a/partner_do_merge/__init__.py b/partner_do_merge/__init__.py new file mode 100644 index 000000000..b31324c4e --- /dev/null +++ b/partner_do_merge/__init__.py @@ -0,0 +1,2 @@ +import wizard +import model diff --git a/partner_do_merge/__openerp__.py b/partner_do_merge/__openerp__.py new file mode 100644 index 000000000..cb7e1887c --- /dev/null +++ b/partner_do_merge/__openerp__.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name' : 'Merge Duplicate Partner', + 'version' : '0.1', + 'author' : 'Vauxoo', + 'category' : 'Base', + 'description' : """ +Merge Partners +============== +We can merge duplicates partners and set the new id in all documents of partner merged + +We can merge partner using like mach parameter these fields: +-Email +-VAT +-Company +-Is company +-Name +-Parent Company + +We can select which partner will be the main partner + +This feature is in the follow path Sales/Tools/Deduplicate Contacts also is created an action menu in the partner view + + """, + 'website': 'http://www.vauxoo.com', + 'images' : [], + 'depends' : [ + 'base', + 'crm', + ], + 'data': [ + 'wizard/base_partner_merge_view.xml', + ], + 'js': [ + ], + 'qweb' : [ + ], + 'css':[ + ], + 'demo': [ + ], + 'test': [ + ], + 'installable': True, + 'auto_install': False, +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/partner_do_merge/model/__init__.py b/partner_do_merge/model/__init__.py new file mode 100644 index 000000000..0f63679e7 --- /dev/null +++ b/partner_do_merge/model/__init__.py @@ -0,0 +1 @@ +import partner diff --git a/partner_do_merge/model/partner.py b/partner_do_merge/model/partner.py new file mode 100644 index 000000000..d959de7cb --- /dev/null +++ b/partner_do_merge/model/partner.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2010 Tiny SPRL (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import openerp +from openerp import SUPERUSER_ID +from openerp import tools +from openerp.osv import osv, fields +from openerp.tools.translate import _ +from openerp.tools.yaml_import import is_comment + + +class res_partner(osv.Model): + _description = 'Partner' + _inherit = "res.partner" + + + def _commercial_partner_compute(self, cr, uid, ids, name, args, context=None): + """ Returns the partner that is considered the commercial + entity of this partner. The commercial entity holds the master data + for all commercial fields (see :py:meth:`~_commercial_fields`) """ + result = dict.fromkeys(ids, False) + for partner in self.browse(cr, uid, ids, context=context): + current_partner = partner + while not current_partner.is_company and current_partner.parent_id: + current_partner = current_partner.parent_id + result[partner.id] = current_partner.id + return result + + def _display_name_compute(self, cr, uid, ids, name, args, context=None): + return dict(self.name_get(cr, uid, ids, context=context)) + + # indirections to avoid passing a copy of the overridable method when declaring the function field + _display_name = lambda self, *args, **kwargs: self._display_name_compute(*args, **kwargs) + + _display_name_store_triggers = { + 'res.partner': (lambda self,cr,uid,ids,context=None: self.search(cr, uid, [('id','child_of',ids)]), + ['parent_id', 'is_company', 'name'], 10) + } + + _order = "display_name" + _columns = { + 'display_name': fields.function(_display_name, type='char', string='Name', store=_display_name_store_triggers), + 'id': fields.integer('Id', readonly=True), + 'create_date': fields.datetime('Create Date', readonly=True), + + + } + + def name_get(self, cr, uid, ids, context=None): + if context is None: + context = {} + if isinstance(ids, (int, long)): + ids = [ids] + res = [] + for record in self.browse(cr, uid, ids, context=context): + name = record.name + if record.parent_id and not record.is_company: + name = "%s, %s" % (record.parent_id.name, name) + if context.get('show_address'): + name = name + "\n" + self._display_address(cr, uid, record, without_company=True, context=context) + name = name.replace('\n\n','\n') + name = name.replace('\n\n','\n') + if context.get('show_email') and record.email: + name = "%s <%s>" % (name, record.email) + res.append((record.id, name)) + return res + + def name_search(self, cr, uid, name, args=None, operator='ilike', context=None, limit=100): + if not args: + args = [] + if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'): + # search on the name of the contacts and of its company + search_name = name + if operator in ('ilike', 'like'): + search_name = '%%%s%%' % name + if operator in ('=ilike', '=like'): + operator = operator[1:] + query_args = {'name': search_name} + limit_str = '' + if limit: + limit_str = ' limit %(limit)s' + query_args['limit'] = limit + cr.execute('''SELECT partner.id FROM res_partner partner + LEFT JOIN res_partner company ON partner.parent_id = company.id + WHERE partner.email ''' + operator +''' %(name)s OR + partner.display_name ''' + operator + ' %(name)s ' + limit_str, query_args) + ids = map(lambda x: x[0], cr.fetchall()) + ids = self.search(cr, uid, [('id', 'in', ids)] + args, limit=limit, context=context) + if ids: + return self.name_get(cr, uid, ids, context) + return super(res_partner,self).name_search(cr, uid, name, args, operator=operator, context=context, limit=limit) diff --git a/partner_do_merge/wizard/__init__.py b/partner_do_merge/wizard/__init__.py new file mode 100644 index 000000000..65c9179ec --- /dev/null +++ b/partner_do_merge/wizard/__init__.py @@ -0,0 +1 @@ +import base_partner_merge diff --git a/partner_do_merge/wizard/base_partner_merge.py b/partner_do_merge/wizard/base_partner_merge.py new file mode 100644 index 000000000..21d6fed5c --- /dev/null +++ b/partner_do_merge/wizard/base_partner_merge.py @@ -0,0 +1,761 @@ +#!/usr/bin/env python +from __future__ import absolute_import +from email.utils import parseaddr +import functools +import htmlentitydefs +import itertools +import logging +import operator +import re +from ast import literal_eval +from openerp.tools import mute_logger + +# Validation Library https://pypi.python.org/pypi/validate_email/1.1 +from .validate_email import validate_email + +import openerp +from openerp.osv import osv, orm +from openerp.osv import fields +from openerp.osv.orm import browse_record +from openerp.tools.translate import _ + +pattern = re.compile("&(\w+?);") + +_logger = logging.getLogger('base.partner.merge') + + +# http://www.php2python.com/wiki/function.html-entity-decode/ +def html_entity_decode_char(m, defs=htmlentitydefs.entitydefs): + try: + return defs[m.group(1)] + except KeyError: + return m.group(0) + + +def html_entity_decode(string): + return pattern.sub(html_entity_decode_char, string) + + +def sanitize_email(email): + assert isinstance(email, basestring) and email + + result = re.subn(r';|/|:', ',', + html_entity_decode(email or ''))[0].split(',') + + emails = [parseaddr(email)[1] + for item in result + for email in item.split()] + + return [email.lower() + for email in emails + if validate_email(email)] + + +def is_integer_list(ids): + return all(isinstance(i, (int, long)) for i in ids) + + +class MergePartnerLine(osv.TransientModel): + _name = 'base.partner.merge.line' + + _columns = { + 'wizard_id': fields.many2one('base.partner.merge.automatic.wizard', + 'Wizard'), + 'min_id': fields.integer('MinID'), + 'aggr_ids': fields.char('Ids', required=True), + } + + _order = 'min_id asc' + + +class MergePartnerAutomatic(osv.TransientModel): + """ + The idea behind this wizard is to create a list of potential partners to + merge. We use two objects, the first one is the wizard for the end-user. + And the second will contain the partner list to merge. + """ + _name = 'base.partner.merge.automatic.wizard' + + _columns = { + # Group by + 'group_by_email': fields.boolean('Email'), + 'group_by_name': fields.boolean('Name'), + 'group_by_is_company': fields.boolean('Is Company'), + 'group_by_vat': fields.boolean('VAT'), + 'group_by_parent_id': fields.boolean('Parent Company'), + + 'state': fields.selection([('option', 'Option'), + ('selection', 'Selection'), + ('finished', 'Finished')], + 'State', + readonly=True, + required=True), + 'number_group': fields.integer("Group of Contacts", readonly=True), + 'current_line_id': fields.many2one('base.partner.merge.line', 'Current Line'), + 'line_ids': fields.one2many('base.partner.merge.line', 'wizard_id', 'Lines'), + 'partner_ids': fields.many2many('res.partner', string='Contacts'), + 'dst_partner_id': fields.many2one('res.partner', string='Destination Contact'), + + 'exclude_contact': fields.boolean('A user associated to the contact'), + 'exclude_journal_item': fields.boolean('Journal Items associated to the contact'), + 'maximum_group': fields.integer("Maximum of Group of Contacts"), + } + + def default_get(self, cr, uid, fields, context=None): + if context is None: + context = {} + res = super(MergePartnerAutomatic, self).default_get(cr, uid, fields, context) + if context.get('active_model') == 'res.partner' and context.get('active_ids'): + partner_ids = context['active_ids'] + res['state'] = 'selection' + res['partner_ids'] = partner_ids + res['dst_partner_id'] = self._get_ordered_partner(cr, uid, partner_ids, context=context)[-1].id + return res + + _defaults = { + 'state': 'option' + } + + def get_fk_on(self, cr, table): + q = """ SELECT cl1.relname as table, + att1.attname as column + FROM pg_constraint as con, pg_class as cl1, pg_class as cl2, + pg_attribute as att1, pg_attribute as att2 + WHERE con.conrelid = cl1.oid + AND con.confrelid = cl2.oid + AND array_lower(con.conkey, 1) = 1 + AND con.conkey[1] = att1.attnum + AND att1.attrelid = cl1.oid + AND cl2.relname = %s + AND att2.attname = 'id' + AND array_lower(con.confkey, 1) = 1 + AND con.confkey[1] = att2.attnum + AND att2.attrelid = cl2.oid + AND con.contype = 'f' + """ + return cr.execute(q, (table,)) + + def _update_foreign_keys(self, cr, uid, src_partners, dst_partner, context=None): + _logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners))) + + # find the many2one relation to a partner + proxy = self.pool.get('res.partner') + self.get_fk_on(cr, 'res_partner') + + # ignore two tables + + for table, column in cr.fetchall(): + if 'base_partner_merge_' in table: + continue + partner_ids = tuple(map(int, src_partners)) + + query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table) + cr.execute(query, ()) + columns = [] + for data in cr.fetchall(): + if data[0] != column: + columns.append(data[0]) + + query_dic = { + 'table': table, + 'column': column, + 'value': columns[0], + } + if len(columns) <= 1: + # unique key treated + query = """ + UPDATE "%(table)s" as ___tu + SET %(column)s = %%s + WHERE + %(column)s = %%s AND + NOT EXISTS ( + SELECT 1 + FROM "%(table)s" as ___tw + WHERE + %(column)s = %%s AND + ___tu.%(value)s = ___tw.%(value)s + )""" % query_dic + for partner_id in partner_ids: + cr.execute(query, (dst_partner.id, partner_id, dst_partner.id)) + else: + cr.execute("SAVEPOINT recursive_partner_savepoint") + try: + query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic + cr.execute(query, (dst_partner.id, partner_ids,)) + + if column == proxy._parent_name and table == 'res_partner': + query = """ + WITH RECURSIVE cycle(id, parent_id) AS ( + SELECT id, parent_id FROM res_partner + UNION + SELECT cycle.id, res_partner.parent_id + FROM res_partner, cycle + WHERE res_partner.id = cycle.parent_id AND + cycle.id != cycle.parent_id + ) + SELECT id FROM cycle WHERE id = parent_id AND id = %s + """ + cr.execute(query, (dst_partner.id,)) + if cr.fetchall(): + cr.execute("ROLLBACK TO SAVEPOINT recursive_partner_savepoint") + finally: + cr.execute("RELEASE SAVEPOINT recursive_partner_savepoint") + + def _update_reference_fields(self, cr, uid, src_partners, dst_partner, context=None): + _logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners))) + + def update_records(model, src, field_model='model', field_id='res_id', context=None): + proxy = self.pool.get(model) + if proxy is None: + return + domain = [(field_model, '=', 'res.partner'), (field_id, '=', src.id)] + ids = proxy.search(cr, openerp.SUPERUSER_ID, domain, context=context) + return proxy.write(cr, openerp.SUPERUSER_ID, ids, {field_id: dst_partner.id}, context=context) + + update_records = functools.partial(update_records, context=context) + + for partner in src_partners: + update_records('base.calendar', src=partner, field_model='model_id.model') + update_records('ir.attachment', src=partner, field_model='res_model') + update_records('mail.followers', src=partner, field_model='res_model') + update_records('mail.message', src=partner) + update_records('marketing.campaign.workitem', src=partner, field_model='object_id.model') + update_records('ir.model.data', src=partner) + + proxy = self.pool['ir.model.fields'] + domain = [('ttype', '=', 'reference')] + record_ids = proxy.search(cr, openerp.SUPERUSER_ID, domain, context=context) + + for record in proxy.browse(cr, openerp.SUPERUSER_ID, record_ids, context=context): + proxy_model = self.pool[record.model] + + field_type = proxy_model._columns.get(record.name).__class__._type + + if field_type == 'function': + continue + + for partner in src_partners: + domain = [ + (record.name, '=', 'res.partner,%d' % partner.id) + ] + model_ids = proxy_model.search(cr, openerp.SUPERUSER_ID, domain, context=context) + values = { + record.name: 'res.partner,%d' % dst_partner.id, + } + proxy_model.write(cr, openerp.SUPERUSER_ID, model_ids, values, context=context) + + def _update_values(self, cr, uid, src_partners, dst_partner, context=None): + _logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, list(map(operator.attrgetter('id'), src_partners))) + + columns = dst_partner._columns + def write_serializer(column, item): + if isinstance(item, browse_record): + return item.id + else: + return item + + values = dict() + for column, field in columns.iteritems(): + if field._type not in ('many2many', 'one2many') and not isinstance(field, fields.function): + for item in itertools.chain(src_partners, [dst_partner]): + if item[column]: + values[column] = write_serializer(column, item[column]) + + values.pop('id', None) + parent_id = values.pop('parent_id', None) + dst_partner.write(values) + if parent_id and parent_id != dst_partner.id: + try: + dst_partner.write({'parent_id': parent_id}) + except (osv.except_osv, orm.except_orm): + _logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id) + + @mute_logger('openerp.osv.expression', 'openerp.osv.orm') + def _merge(self, cr, uid, partner_ids, dst_partner=None, context=None): + proxy = self.pool.get('res.partner') + + partner_ids = proxy.exists(cr, uid, list(partner_ids), context=context) + if len(partner_ids) < 2: + return + + if len(partner_ids) > 10: + raise osv.except_osv(_('Error'), _("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed.")) + + if openerp.SUPERUSER_ID != uid and len(set(partner.email for partner in proxy.browse(cr, uid, partner_ids, context=context))) > 1: + raise osv.except_osv(_('Error'), _("All contacts must have the same email. Only the Administrator can merge contacts with different emails.")) + + if dst_partner and dst_partner.id in partner_ids: + src_partners = proxy.browse(cr, uid, [id for id in partner_ids if id != dst_partner.id], context=context) + else: + ordered_partners = self._get_ordered_partner(cr, uid, partner_ids, context) + dst_partner = ordered_partners[-1] + src_partners = ordered_partners[:-1] + _logger.info("dst_partner: %s", dst_partner.id) + + if openerp.SUPERUSER_ID != uid and self._model_is_installed(cr, uid, 'account.move.line', context=context) and \ + self.pool.get('account.move.line').search(cr, openerp.SUPERUSER_ID, [('partner_id', 'in', [partner.id for partner in src_partners])], context=context): + raise osv.except_osv(_('Error'), _("Only the destination contact may be linked to existing Journal Items. Please ask the Administrator if you need to merge several contacts linked to existing Journal Items.")) + + call_it = lambda function: function(cr, uid, src_partners, dst_partner, + context=context) + + call_it(self._update_foreign_keys) + call_it(self._update_reference_fields) + call_it(self._update_values) + + _logger.info('(uid = %s) merged the partners %r with %s', uid, list(map(operator.attrgetter('id'), src_partners)), dst_partner.id) + dst_partner.message_post(body='%s %s'%(_("Merged with the following partners:"), ", ".join('%s<%s>(ID %s)' % (p.name, p.email or 'n/a', p.id) for p in src_partners))) + + for partner in src_partners: + partner.unlink() + + def clean_emails(self, cr, uid, context=None): + """ + Clean the email address of the partner, if there is an email field with + a mimum of two addresses, the system will create a new partner, with the + information of the previous one and will copy the new cleaned email into + the email field. + """ + if context is None: + context = {} + + proxy_model = self.pool['ir.model.fields'] + field_ids = proxy_model.search(cr, uid, [('model', '=', 'res.partner'), + ('ttype', 'like', '%2many')], + context=context) + fields = proxy_model.read(cr, uid, field_ids, context=context) + reset_fields = dict((field['name'], []) for field in fields) + + proxy_partner = self.pool['res.partner'] + context['active_test'] = False + ids = proxy_partner.search(cr, uid, [], context=context) + + fields = ['name', 'var' 'partner_id' 'is_company', 'email'] + partners = proxy_partner.read(cr, uid, ids, fields, context=context) + + partners.sort(key=operator.itemgetter('id')) + partners_len = len(partners) + + _logger.info('partner_len: %r', partners_len) + + for idx, partner in enumerate(partners): + if not partner['email']: + continue + + percent = (idx / float(partners_len)) * 100.0 + _logger.info('idx: %r', idx) + _logger.info('percent: %r', percent) + try: + emails = sanitize_email(partner['email']) + head, tail = emails[:1], emails[1:] + email = head[0] if head else False + + proxy_partner.write(cr, uid, [partner['id']], + {'email': email}, context=context) + + for email in tail: + values = dict(reset_fields, email=email) + proxy_partner.copy(cr, uid, partner['id'], values, + context=context) + + except Exception: + _logger.exception("There is a problem with this partner: %r", partner) + raise + return True + + def close_cb(self, cr, uid, ids, context=None): + return {'type': 'ir.actions.act_window_close'} + + def _generate_query(self, fields, maximum_group=100): + group_fields = ', '.join(fields) + + filters = [] + for field in fields: + if field in ['email', 'name']: + filters.append((field, 'IS NOT', 'NULL')) + + criteria = ' AND '.join('%s %s %s' % (field, operator, value) + for field, operator, value in filters) + + text = [ + "SELECT min(id), array_agg(id)", + "FROM res_partner", + ] + + if criteria: + text.append('WHERE %s' % criteria) + + text.extend([ + "GROUP BY %s" % group_fields, + "HAVING COUNT(*) >= 2", + "ORDER BY min(id)", + ]) + + if maximum_group: + text.extend([ + "LIMIT %s" % maximum_group, + ]) + + return ' '.join(text) + + def _compute_selected_groupby(self, this): + group_by_str = 'group_by_' + group_by_len = len(group_by_str) + + fields = [ + key[group_by_len:] + for key in self._columns.keys() + if key.startswith(group_by_str) + ] + + groups = [ + field + for field in fields + if getattr(this, '%s%s' % (group_by_str, field), False) + ] + + if not groups: + raise osv.except_osv(_('Error'), + _("You have to specify a filter for your selection")) + + return groups + + def next_cb(self, cr, uid, ids, context=None): + """ + Don't compute any thing + """ + context = dict(context or {}, active_test=False) + this = self.browse(cr, uid, ids[0], context=context) + if this.current_line_id: + this.current_line_id.unlink() + return self._next_screen(cr, uid, this, context) + + def _get_ordered_partner(self, cr, uid, partner_ids, context=None): + partners = self.pool.get('res.partner').browse(cr, uid, list(partner_ids), context=context) + ordered_partners = sorted(sorted(partners, + key=operator.attrgetter('create_date'), reverse=True), + key=operator.attrgetter('active'), reverse=True) + return ordered_partners + + def _next_screen(self, cr, uid, this, context=None): + this.refresh() + values = {} + if this.line_ids: + # in this case, we try to find the next record. + current_line = this.line_ids[0] + current_partner_ids = literal_eval(current_line.aggr_ids) + values.update({ + 'current_line_id': current_line.id, + 'partner_ids': [(6, 0, current_partner_ids)], + 'dst_partner_id': self._get_ordered_partner(cr, uid, current_partner_ids, context)[-1].id, + 'state': 'selection', + }) + else: + values.update({ + 'current_line_id': False, + 'partner_ids': [], + 'state': 'finished', + }) + + this.write(values) + + return { + 'type': 'ir.actions.act_window', + 'res_model': this._name, + 'res_id': this.id, + 'view_mode': 'form', + 'target': 'new', + } + + def _model_is_installed(self, cr, uid, model, context=None): + proxy = self.pool.get('ir.model') + domain = [('model', '=', model)] + return proxy.search_count(cr, uid, domain, context=context) > 0 + + def _partner_use_in(self, cr, uid, aggr_ids, models, context=None): + """ + Check if there is no occurence of this group of partner in the selected + model + """ + for model, field in models.iteritems(): + proxy = self.pool.get(model) + domain = [(field, 'in', aggr_ids)] + if proxy.search_count(cr, uid, domain, context=context): + return True + return False + + def compute_models(self, cr, uid, ids, context=None): + """ + Compute the different models needed by the system if you want to exclude + some partners. + """ + assert is_integer_list(ids) + + this = self.browse(cr, uid, ids[0], context=context) + + models = {} + if this.exclude_contact: + models['res.users'] = 'partner_id' + + if self._model_is_installed(cr, uid, 'account.move.line', context=context) and this.exclude_journal_item: + models['account.move.line'] = 'partner_id' + + return models + + def _process_query(self, cr, uid, ids, query, context=None): + """ + Execute the select request and write the result in this wizard + """ + proxy = self.pool.get('base.partner.merge.line') + this = self.browse(cr, uid, ids[0], context=context) + models = self.compute_models(cr, uid, ids, context=context) + cr.execute(query) + + counter = 0 + for min_id, aggr_ids in cr.fetchall(): + if models and self._partner_use_in(cr, uid, aggr_ids, models, context=context): + continue + values = { + 'wizard_id': this.id, + 'min_id': min_id, + 'aggr_ids': aggr_ids, + } + + proxy.create(cr, uid, values, context=context) + counter += 1 + + values = { + 'state': 'selection', + 'number_group': counter, + } + + this.write(values) + + _logger.info("counter: %s", counter) + + def start_process_cb(self, cr, uid, ids, context=None): + """ + Start the process. + * Compute the selected groups (with duplication) + * If the user has selected the 'exclude_XXX' fields, avoid the partners. + """ + assert is_integer_list(ids) + + context = dict(context or {}, active_test=False) + this = self.browse(cr, uid, ids[0], context=context) + groups = self._compute_selected_groupby(this) + query = self._generate_query(groups, this.maximum_group) + self._process_query(cr, uid, ids, query, context=context) + + return self._next_screen(cr, uid, this, context) + + def automatic_process_cb(self, cr, uid, ids, context=None): + assert is_integer_list(ids) + this = self.browse(cr, uid, ids[0], context=context) + this.start_process_cb() + this.refresh() + + for line in this.line_ids: + partner_ids = literal_eval(line.aggr_ids) + self._merge(cr, uid, partner_ids, context=context) + line.unlink() + cr.commit() + + this.write({'state': 'finished'}) + return { + 'type': 'ir.actions.act_window', + 'res_model': this._name, + 'res_id': this.id, + 'view_mode': 'form', + 'target': 'new', + } + + def parent_migration_process_cb(self, cr, uid, ids, context=None): + assert is_integer_list(ids) + + context = dict(context or {}, active_test=False) + this = self.browse(cr, uid, ids[0], context=context) + + query = """ + SELECT + min(p1.id), + array_agg(DISTINCT p1.id) + FROM + res_partner as p1 + INNER join + res_partner as p2 + ON + p1.email = p2.email AND + p1.name = p2.name AND + (p1.parent_id = p2.id OR p1.id = p2.parent_id) + WHERE + p2.id IS NOT NULL + GROUP BY + p1.email, + p1.name, + CASE WHEN p1.parent_id = p2.id THEN p2.id + ELSE p1.id + END + HAVING COUNT(*) >= 2 + ORDER BY + min(p1.id) + """ + + self._process_query(cr, uid, ids, query, context=context) + + for line in this.line_ids: + partner_ids = literal_eval(line.aggr_ids) + self._merge(cr, uid, partner_ids, context=context) + line.unlink() + cr.commit() + + this.write({'state': 'finished'}) + + cr.execute(""" + UPDATE + res_partner + SET + is_company = NULL, + parent_id = NULL + WHERE + parent_id = id + """) + + return { + 'type': 'ir.actions.act_window', + 'res_model': this._name, + 'res_id': this.id, + 'view_mode': 'form', + 'target': 'new', + } + + def update_all_process_cb(self, cr, uid, ids, context=None): + assert is_integer_list(ids) + + # WITH RECURSIVE cycle(id, parent_id) AS ( + # SELECT id, parent_id FROM res_partner + # UNION + # SELECT cycle.id, res_partner.parent_id + # FROM res_partner, cycle + # WHERE res_partner.id = cycle.parent_id AND + # cycle.id != cycle.parent_id + # ) + # UPDATE res_partner + # SET parent_id = NULL + # WHERE id in (SELECT id FROM cycle WHERE id = parent_id); + + this = self.browse(cr, uid, ids[0], context=context) + + self.parent_migration_process_cb(cr, uid, ids, context=None) + + list_merge = [ + {'group_by_vat': True, 'group_by_email': True, 'group_by_name': True}, + # {'group_by_name': True, 'group_by_is_company': True, 'group_by_parent_id': True}, + # {'group_by_email': True, 'group_by_is_company': True, 'group_by_parent_id': True}, + # {'group_by_name': True, 'group_by_vat': True, 'group_by_is_company': True, 'exclude_journal_item': True}, + # {'group_by_email': True, 'group_by_vat': True, 'group_by_is_company': True, 'exclude_journal_item': True}, + # {'group_by_email': True, 'group_by_is_company': True, 'exclude_contact': True, 'exclude_journal_item': True}, + # {'group_by_name': True, 'group_by_is_company': True, 'exclude_contact': True, 'exclude_journal_item': True} + ] + + for merge_value in list_merge: + id = self.create(cr, uid, merge_value, context=context) + self.automatic_process_cb(cr, uid, [id], context=context) + + cr.execute(""" + UPDATE + res_partner + SET + is_company = NULL + WHERE + parent_id IS NOT NULL AND + is_company IS NOT NULL + """) + + # cr.execute(""" + # UPDATE + # res_partner as p1 + # SET + # is_company = NULL, + # parent_id = ( + # SELECT p2.id + # FROM res_partner as p2 + # WHERE p2.email = p1.email AND + # p2.parent_id != p2.id + # LIMIT 1 + # ) + # WHERE + # p1.parent_id = p1.id + # """) + + return self._next_screen(cr, uid, this, context) + + def merge_cb(self, cr, uid, ids, context=None): + assert is_integer_list(ids) + + context = dict(context or {}, active_test=False) + this = self.browse(cr, uid, ids[0], context=context) + + partner_ids = set(map(int, this.partner_ids)) + if not partner_ids: + this.write({'state': 'finished'}) + return { + 'type': 'ir.actions.act_window', + 'res_model': this._name, + 'res_id': this.id, + 'view_mode': 'form', + 'target': 'new', + } + + self._merge(cr, uid, partner_ids, this.dst_partner_id, context=context) + + if this.current_line_id: + this.current_line_id.unlink() + + return self._next_screen(cr, uid, this, context) + + def auto_set_parent_id(self, cr, uid, ids, context=None): + assert is_integer_list(ids) + + # select partner who have one least invoice + partner_treated = ['@gmail.com'] + cr.execute(""" SELECT p.id, p.email + FROM res_partner as p + LEFT JOIN account_invoice as a + ON p.id = a.partner_id AND a.state in ('open','paid') + WHERE p.grade_id is NOT NULL + GROUP BY p.id + ORDER BY COUNT(a.id) DESC + """) + re_email = re.compile(r".*@") + for id, email in cr.fetchall(): + # check email domain + email = re_email.sub("@", email or "") + if not email or email in partner_treated: + continue + partner_treated.append(email) + + # don't update the partners if they are more of one who have invoice + cr.execute(""" SELECT * + FROM res_partner as p + WHERE p.id != %s AND p.email LIKE '%%%s' AND + EXISTS (SELECT * FROM account_invoice as a WHERE p.id = a.partner_id AND a.state in ('open','paid')) + """ % (id, email)) + + if len(cr.fetchall()) > 1: + _logger.info("%s MORE OF ONE COMPANY", email) + continue + + # to display changed values + cr.execute(""" SELECT id,email + FROM res_partner + WHERE parent_id != %s AND id != %s AND email LIKE '%%%s' + """ % (id, id, email)) + _logger.info("%r", cr.fetchall()) + + # upgrade + cr.execute(""" UPDATE res_partner + SET parent_id = %s + WHERE id != %s AND email LIKE '%%%s' + """ % (id, id, email)) + return False diff --git a/partner_do_merge/wizard/base_partner_merge_view.xml b/partner_do_merge/wizard/base_partner_merge_view.xml new file mode 100644 index 000000000..8ce0b5fd9 --- /dev/null +++ b/partner_do_merge/wizard/base_partner_merge_view.xml @@ -0,0 +1,123 @@ + + + + + + + + Deduplicate Contacts + base.partner.merge.automatic.wizard + form + form + new + {'active_test': False} + + + + + + base.partner.merge.automatic.wizard.form + base.partner.merge.automatic.wizard + +
+
+
+ + +

There is no more contacts to merge for this request...

+