Browse Source

[ADD]Partner merger spinoff when not using CRM module

stable
Fabien Bourgeois 3 years ago
parent
commit
6c2cd40020
8 changed files with 1607 additions and 0 deletions
  1. +18
    -0
      odoo_partner_merge/__init__.py
  2. +30
    -0
      odoo_partner_merge/__manifest__.py
  3. +328
    -0
      odoo_partner_merge/i18n/fr.po
  4. +321
    -0
      odoo_partner_merge/i18n/odoo_partner_merge.pot
  5. +18
    -0
      odoo_partner_merge/wizard/__init__.py
  6. +654
    -0
      odoo_partner_merge/wizard/base_partner_merge.py
  7. +115
    -0
      odoo_partner_merge/wizard/base_partner_merge_views.xml
  8. +123
    -0
      odoo_partner_merge/wizard/validate_email.py

+ 18
- 0
odoo_partner_merge/__init__.py View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-

# Copyright 2017 Fabien Bourgeois <fabien@yaltik.com>
#
# 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 <http://www.gnu.org/licenses/>.

from . import wizard

+ 30
- 0
odoo_partner_merge/__manifest__.py View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-

# Copyright 2017 Fabien Bourgeois <fabien@yaltik.com>
#
# 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 <http://www.gnu.org/licenses/>.

{
'name': 'Odoo spinoff partner merger from CRM',
'summary': 'Odoo spinoff partner merger from CRM',
'description': 'Odoo spinoff partner merger from CRM (wizard, mainly)',
'version': '10.0.1.0.0',
'category': 'GOLEM',
'author': 'Odoo SA, Fabien Bourgeois',
'license': 'LGPL-3',
'application': False,
'installable': True,
'depends': ['base'],
'data': ['wizard/base_partner_merge_views.xml']
}

+ 328
- 0
odoo_partner_merge/i18n/fr.po View File

@@ -0,0 +1,328 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odoo_partner_merge
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-04 15:11+0000\n"
"PO-Revision-Date: 2017-06-04 15:11+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_exclude_contact
msgid "A user associated to the contact"
msgstr "Utilisateur associé au contact"

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:310
#, python-format
msgid "All contacts must have the same email. Only the Administrator can merge contacts with different emails."
msgstr "Tous les contacts doivent avoir la même adresse courriel. Seul l'administrateur peut fusionner des contacts avec des adresses courriel différentes."

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Are you sure to execute the automatic merge of your contacts ?"
msgstr "Voulez-vous vraiment réaliser la fusion automatique des contacts ?"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Are you sure to execute the list of automatic merges of your contacts ?"
msgstr "Voulez-vous vraiment réaliser la fusion automatique de ces contacts ?"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Automatic Merge Wizard"
msgstr "Assistant de fusion automatique"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Cancel"
msgstr "Annuler"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Close"
msgstr "Fermer"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_partner_ids
msgid "Contacts"
msgstr "Contacts"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_create_uid
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_create_uid
msgid "Created by"
msgstr "Créé par"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_create_date
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_create_date
msgid "Created on"
msgstr "Créé le"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_current_line_id
msgid "Current Line"
msgstr "Ligne actuelle"

#. module: odoo_partner_merge
#: model:ir.actions.act_window,name:odoo_partner_merge.action_partner_deduplicate
msgid "Deduplicate Contacts"
msgstr "Fusionner les contacts"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Deduplicate the other Contacts"
msgstr "Fusionner d'autres contacts"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_dst_partner_id
msgid "Destination Contact"
msgstr "Destination Contact"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_display_name
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_display_name
msgid "Display Name"
msgstr "Nom affiché"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_email
msgid "Email"
msgstr "Courriel"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Exclude contacts having"
msgstr "Exclure les contacts avec"

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Finished"
msgstr "Terminé"

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:299
#, python-format
msgid "For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."
msgstr "Pour des raisons de sécurité, vous ne pouvez pas fusionner plus de 3 contacts. Si besoin, vous pouvez ré-ouvrir l'assistant autant de fois que nécessaire."

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_number_group
msgid "Group of Contacts"
msgstr "Groupe de contacts"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_id
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_id
msgid "ID"
msgstr "ID"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_aggr_ids
msgid "Ids"
msgstr "IDS"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_is_company
msgid "Is Company"
msgstr "Est une société"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_exclude_journal_item
msgid "Journal Items associated to the contact"
msgstr "Écritures comptables associées au contact"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard___last_update
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line___last_update
msgid "Last Modified on"
msgstr "Dernière Modification le"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_write_uid
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_write_date
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_line_ids
msgid "Lines"
msgstr "Lignes"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_maximum_group
msgid "Maximum of Group of Contacts"
msgstr "Limite Maximum du Groupe de Contacts"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Automatically"
msgstr "Fusionner automatiquement"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Automatically all process"
msgstr "Fusionne automatiquement tous les processus"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Contacts"
msgstr "Fusionner les contacts"

#. module: odoo_partner_merge
#: model:ir.actions.act_window,name:odoo_partner_merge.action_partner_merge
msgid "Merge Selected Contacts"
msgstr "Fusionner les contacts sélectionnés"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge the following contacts"
msgstr "Fusionner les contacts suivants"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge with Manual Check"
msgstr "Fusionner avec vérification manuelle"

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:331
#, python-format
msgid "Merged with the following partners:"
msgstr "Fusionnés avec les partenaires suivants"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_min_id
msgid "MinID"
msgstr "MinID"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_name
msgid "Name"
msgstr "Nom"

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:323
#, python-format
msgid "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."
msgstr "Seul le contact de destination peut être lié à des écritures comptables existantes. Si vous avez besoin de fusionner plusieurs contacts liés à des écritures comptables existantes, veuillez contacter l'administrateur ."

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Option"
msgstr "Option"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Options"
msgstr "Options"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_parent_id
msgid "Parent Company"
msgstr "Société mère"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Partners"
msgstr "Partenaires"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Search duplicates based on duplicated data in"
msgstr "Recherche de doublons basés sur les données dupliquées dans"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Select the list of fields used to search for\n"
" duplicated records. If you select several fields,\n"
" Odoo will propose you to merge only those having\n"
" all these fields in common. (not one of the fields)."
msgstr "Sélectionnez la liste des champs utilisés pour chercher\n"
"les enregistrements dupliqués. Si vous sélectionnez plusieurs champs,\n"
"Odoo vous proposera de fusionner seulement ceux qui ont\n"
"tous ces champs en commun (pas uniquement l'un des champs)."

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Selected contacts will be merged together.\n"
" All documents linked to one of these contacts\n"
" will be redirected to the destination contact.\n"
" You can remove contacts from this list to avoid merging them."
msgstr ""
"Sélectionnez les contacts qui seront fusionnés.\n"
"Tous les documents liés à un de ces contacts\n"
"sera redirigé au contact de destination.\n"
"Vous pouvez enlever les contacts de cette liste pour éviter de les fusionner."

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Selection"
msgstr "Sélection"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Skip these contacts"
msgstr "Ignorez ces contacts"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_state
msgid "State"
msgstr "État"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "There is no more contacts to merge for this request..."
msgstr "Il n'y a plus de contacts a fusionner pour cette requête"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_vat
msgid "VAT"
msgstr "TVA"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_wizard_id
msgid "Wizard"
msgstr "Assistant"

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:306
#, python-format
msgid "You cannot merge a contact with one of his parent."
msgstr "Vous ne pouvez pas fusionner un contact avec l'un de ses parents."

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:398
#, python-format
msgid "You have to specify a filter for your selection"
msgstr "Vous devez spécifier un filtre a cette sélection"

#. module: odoo_partner_merge
#: model:ir.model,name:odoo_partner_merge.model_base_partner_merge_automatic_wizard
msgid "base.partner.merge.automatic.wizard"
msgstr "base.partner.merge.automatic.wizard"

#. module: odoo_partner_merge
#: model:ir.model,name:odoo_partner_merge.model_base_partner_merge_line
msgid "base.partner.merge.line"
msgstr "base.partner.merge.line"

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "or"
msgstr "ou"


+ 321
- 0
odoo_partner_merge/i18n/odoo_partner_merge.pot View File

@@ -0,0 +1,321 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * odoo_partner_merge
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-06-04 15:11+0000\n"
"PO-Revision-Date: 2017-06-04 15:11+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_exclude_contact
msgid "A user associated to the contact"
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:310
#, python-format
msgid "All contacts must have the same email. Only the Administrator can merge contacts with different emails."
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Are you sure to execute the automatic merge of your contacts ?"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Are you sure to execute the list of automatic merges of your contacts ?"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Automatic Merge Wizard"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Cancel"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Close"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_partner_ids
msgid "Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_create_uid
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_create_uid
msgid "Created by"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_create_date
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_create_date
msgid "Created on"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_current_line_id
msgid "Current Line"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.actions.act_window,name:odoo_partner_merge.action_partner_deduplicate
msgid "Deduplicate Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Deduplicate the other Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_dst_partner_id
msgid "Destination Contact"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_display_name
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_display_name
msgid "Display Name"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_email
msgid "Email"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Exclude contacts having"
msgstr ""

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Finished"
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:299
#, python-format
msgid "For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_number_group
msgid "Group of Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_id
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_id
msgid "ID"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_aggr_ids
msgid "Ids"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_is_company
msgid "Is Company"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_exclude_journal_item
msgid "Journal Items associated to the contact"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard___last_update
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line___last_update
msgid "Last Modified on"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_write_uid
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_write_uid
msgid "Last Updated by"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_write_date
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_write_date
msgid "Last Updated on"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_line_ids
msgid "Lines"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_maximum_group
msgid "Maximum of Group of Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Automatically"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Automatically all process"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.actions.act_window,name:odoo_partner_merge.action_partner_merge
msgid "Merge Selected Contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge the following contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Merge with Manual Check"
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:331
#, python-format
msgid "Merged with the following partners:"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_min_id
msgid "MinID"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_name
msgid "Name"
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:323
#, python-format
msgid "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."
msgstr ""

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Option"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Options"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_parent_id
msgid "Parent Company"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Partners"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Search duplicates based on duplicated data in"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Select the list of fields used to search for\n"
" duplicated records. If you select several fields,\n"
" Odoo will propose you to merge only those having\n"
" all these fields in common. (not one of the fields)."
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Selected contacts will be merged together.\n"
" All documents linked to one of these contacts\n"
" will be redirected to the destination contact.\n"
" You can remove contacts from this list to avoid merging them."
msgstr ""

#. module: odoo_partner_merge
#: selection:base.partner.merge.automatic.wizard,state:0
msgid "Selection"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "Skip these contacts"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_state
msgid "State"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "There is no more contacts to merge for this request..."
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_automatic_wizard_group_by_vat
msgid "VAT"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model.fields,field_description:odoo_partner_merge.field_base_partner_merge_line_wizard_id
msgid "Wizard"
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:306
#, python-format
msgid "You cannot merge a contact with one of his parent."
msgstr ""

#. module: odoo_partner_merge
#: code:addons/odoo_partner_merge/wizard/base_partner_merge.py:398
#, python-format
msgid "You have to specify a filter for your selection"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model,name:odoo_partner_merge.model_base_partner_merge_automatic_wizard
msgid "base.partner.merge.automatic.wizard"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.model,name:odoo_partner_merge.model_base_partner_merge_line
msgid "base.partner.merge.line"
msgstr ""

#. module: odoo_partner_merge
#: model:ir.ui.view,arch_db:odoo_partner_merge.base_partner_merge_automatic_wizard_form
msgid "or"
msgstr ""


+ 18
- 0
odoo_partner_merge/wizard/__init__.py View File

@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-

# Copyright 2017 Fabien Bourgeois <fabien@yaltik.com>
#
# 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 <http://www.gnu.org/licenses/>.

from . import base_partner_merge

+ 654
- 0
odoo_partner_merge/wizard/base_partner_merge.py View File

@@ -0,0 +1,654 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from ast import literal_eval
from email.utils import parseaddr
import functools
import htmlentitydefs
import itertools
import logging
import operator
import psycopg2
import re

# Validation Library https://pypi.python.org/pypi/validate_email/1.1
from .validate_email import validate_email

from odoo import api, fields, models
from odoo import SUPERUSER_ID, _
from odoo.exceptions import ValidationError, UserError
from odoo.tools import mute_logger


_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):
pattern = re.compile("&(\w+?);")
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)]


class MergePartnerLine(models.TransientModel):

_name = 'base.partner.merge.line'
_order = 'min_id asc'

wizard_id = fields.Many2one('base.partner.merge.automatic.wizard', 'Wizard')
min_id = fields.Integer('MinID')
aggr_ids = fields.Char('Ids', required=True)


class MergePartnerAutomatic(models.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'

@api.model
def default_get(self, fields):
res = super(MergePartnerAutomatic, self).default_get(fields)
active_ids = self.env.context.get('active_ids')
if self.env.context.get('active_model') == 'res.partner' and active_ids:
res['state'] = 'selection'
res['partner_ids'] = active_ids
res['dst_partner_id'] = self._get_ordered_partner(active_ids)[-1].id
return res

# 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')
], readonly=True, required=True, string='State', default='option')

number_group = fields.Integer('Group of Contacts', readonly=True)
current_line_id = fields.Many2one('base.partner.merge.line', string='Current Line')
line_ids = fields.One2many('base.partner.merge.line', 'wizard_id', string='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')

# ----------------------------------------
# Update method. Core methods to merge steps
# ----------------------------------------

def _get_fk_on(self, table):
""" return a list of many2one relation with the given table.
:param table : the name of the sql table to return relations
:returns a list of tuple 'table name', 'column name'.
"""
query = """
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'
"""
self._cr.execute(query, (table,))
return self._cr.fetchall()

@api.model
def _update_foreign_keys(self, src_partners, dst_partner):
""" Update all foreign key from the src_partner to dst_partner. All many2one fields will be updated.
:param src_partners : merge source res.partner recordset (does not include destination one)
:param dst_partner : record of destination res.partner
"""
_logger.debug('_update_foreign_keys for dst_partner: %s for src_partners: %s', dst_partner.id, str(src_partners.ids))

# find the many2one relation to a partner
Partner = self.env['res.partner']
relations = self._get_fk_on('res_partner')

for table, column in relations:
if 'base_partner_merge_' in table: # ignore two tables
continue

# get list of columns of current table (exept the current fk column)
query = "SELECT column_name FROM information_schema.columns WHERE table_name LIKE '%s'" % (table)
self._cr.execute(query, ())
columns = []
for data in self._cr.fetchall():
if data[0] != column:
columns.append(data[0])

# do the update for the current table/column in SQL
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 in src_partners:
self._cr.execute(query, (dst_partner.id, partner.id, dst_partner.id))
else:
try:
with mute_logger('odoo.sql_db'), self._cr.savepoint():
query = 'UPDATE "%(table)s" SET %(column)s = %%s WHERE %(column)s IN %%s' % query_dic
self._cr.execute(query, (dst_partner.id, tuple(src_partners.ids),))

# handle the recursivity with parent relation
if column == Partner._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
"""
self._cr.execute(query, (dst_partner.id,))
# NOTE JEM : shouldn't we fetch the data ?
except psycopg2.Error:
# updating fails, most likely due to a violated unique constraint
# keeping record with nonexistent partner_id is useless, better delete it
query = 'DELETE FROM %(table)s WHERE %(column)s IN %%s' % query_dic
self._cr.execute(query, (tuple(src_partners.ids),))

@api.model
def _update_reference_fields(self, src_partners, dst_partner):
""" Update all reference fields from the src_partner to dst_partner.
:param src_partners : merge source res.partner recordset (does not include destination one)
:param dst_partner : record of destination res.partner
"""
_logger.debug('_update_reference_fields for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)

def update_records(model, src, field_model='model', field_id='res_id'):
Model = self.env[model] if model in self.env else None
if Model is None:
return
records = Model.sudo().search([(field_model, '=', 'res.partner'), (field_id, '=', src.id)])
try:
with mute_logger('odoo.sql_db'), self._cr.savepoint():
return records.sudo().write({field_id: dst_partner.id})
except psycopg2.Error:
# updating fails, most likely due to a violated unique constraint
# keeping record with nonexistent partner_id is useless, better delete it
return records.sudo().unlink()

update_records = functools.partial(update_records)

for partner in src_partners:
update_records('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)

records = self.env['ir.model.fields'].search([('ttype', '=', 'reference')])
for record in records.sudo():
try:
Model = self.env[record.model]
field = Model._fields[record.name]
except KeyError:
# unknown model or field => skip
continue

if field.compute is not None:
continue

for partner in src_partners:
records_ref = Model.sudo().search([(record.name, '=', 'res.partner,%d' % partner.id)])
values = {
record.name: 'res.partner,%d' % dst_partner.id,
}
records_ref.sudo().write(values)

@api.model
def _update_values(self, src_partners, dst_partner):
""" Update values of dst_partner with the ones from the src_partners.
:param src_partners : recordset of source res.partner
:param dst_partner : record of destination res.partner
"""
_logger.debug('_update_values for dst_partner: %s for src_partners: %r', dst_partner.id, src_partners.ids)

model_fields = dst_partner._fields

def write_serializer(item):
if isinstance(item, models.BaseModel):
return item.id
else:
return item
# get all fields that are not computed or x2many
values = dict()
for column, field in model_fields.iteritems():
if field.type not in ('many2many', 'one2many') and field.compute is None:
for item in itertools.chain(src_partners, [dst_partner]):
if item[column]:
values[column] = write_serializer(item[column])
# remove fields that can not be updated (id and parent_id)
values.pop('id', None)
parent_id = values.pop('parent_id', None)
dst_partner.write(values)
# try to update the parent_id
if parent_id and parent_id != dst_partner.id:
try:
dst_partner.write({'parent_id': parent_id})
except ValidationError:
_logger.info('Skip recursive partner hierarchies for parent_id %s of partner: %s', parent_id, dst_partner.id)

def _merge(self, partner_ids, dst_partner=None):
""" private implementation of merge partner
:param partner_ids : ids of partner to merge
:param dst_partner : record of destination res.partner
"""
Partner = self.env['res.partner']
partner_ids = Partner.browse(partner_ids).exists()
if len(partner_ids) < 2:
return

if len(partner_ids) > 3:
raise UserError(_("For safety reasons, you cannot merge more than 3 contacts together. You can re-open the wizard several times if needed."))

# check if the list of partners to merge contains child/parent relation
child_ids = self.env['res.partner']
for partner_id in partner_ids:
child_ids |= Partner.search([('id', 'child_of', [partner_id.id])]) - partner_id
if partner_ids & child_ids:
raise UserError(_("You cannot merge a contact with one of his parent."))

# check only admin can merge partners with different emails
if SUPERUSER_ID != self.env.uid and len(set(partner.email for partner in partner_ids)) > 1:
raise UserError(_("All contacts must have the same email. Only the Administrator can merge contacts with different emails."))

# remove dst_partner from partners to merge
if dst_partner and dst_partner in partner_ids:
src_partners = partner_ids - dst_partner
else:
ordered_partners = self._get_ordered_partner(partner_ids.ids)
dst_partner = ordered_partners[-1]
src_partners = ordered_partners[:-1]
_logger.info("dst_partner: %s", dst_partner.id)

# FIXME: is it still required to make and exception for account.move.line since accounting v9.0 ?
if SUPERUSER_ID != self.env.uid and 'account.move.line' in self.env and self.env['account.move.line'].sudo().search([('partner_id', 'in', [partner.id for partner in src_partners])]):
raise UserError(_("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 sub methods to do the merge
self._update_foreign_keys(src_partners, dst_partner)
self._update_reference_fields(src_partners, dst_partner)
self._update_values(src_partners, dst_partner)

_logger.info('(uid = %s) merged the partners %r with %s', self._uid, src_partners.ids, 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)))

# delete source partner, since they are merged
src_partners.unlink()

# ----------------------------------------
# Helpers
# ----------------------------------------

@api.model
def _generate_query(self, fields, maximum_group=100):
""" Build the SQL query on res.partner table to group them according to given criteria
:param fields : list of column names to group by the partners
:param maximum_group : limit of the query
"""
# make the list of column to group by in sql query
sql_fields = []
for field in fields:
if field in ['email', 'name']:
sql_fields.append('lower(%s)' % field)
elif field in ['vat']:
sql_fields.append("replace(%s, ' ', '')" % field)
else:
sql_fields.append(field)
group_fields = ', '.join(sql_fields)

# where clause : for given group by columns, only keep the 'not null' record
filters = []
for field in fields:
if field in ['email', 'name', 'vat']:
filters.append((field, 'IS NOT', 'NULL'))
criteria = ' AND '.join('%s %s %s' % (field, operator, value) for field, operator, value in filters)

# build the query
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.append("LIMIT %s" % maximum_group,)

return ' '.join(text)

@api.model
def _compute_selected_groupby(self):
""" Returns the list of field names the partner can be grouped (as merge
criteria) according to the option checked on the wizard
"""
groups = []
group_by_prefix = 'group_by_'

for field_name in self._fields:
if field_name.startswith(group_by_prefix):
if getattr(self, field_name, False):
groups.append(field_name[len(group_by_prefix):])

if not groups:
raise UserError(_("You have to specify a filter for your selection"))

return groups

@api.model
def _partner_use_in(self, aggr_ids, models):
""" Check if there is no occurence of this group of partner in the selected model
:param aggr_ids : stringified list of partner ids separated with a comma (sql array_agg)
:param models : dict mapping a model name with its foreign key with res_partner table
"""
return any(
self.env[model].search_count([(field, 'in', aggr_ids)])
for model, field in models.iteritems()
)

@api.model
def _get_ordered_partner(self, partner_ids):
""" Helper : returns a `res.partner` recordset ordered by create_date/active fields
:param partner_ids : list of partner ids to sort
"""
return self.env['res.partner'].browse(partner_ids).sorted(
key=lambda p: (p.active, p.create_date),
reverse=True,
)

@api.multi
def _compute_models(self):
""" Compute the different models needed by the system if you want to exclude some partners. """
model_mapping = {}
if self.exclude_contact:
model_mapping['res.users'] = 'partner_id'
if 'account.move.line' in self.env and self.exclude_journal_item:
model_mapping['account.move.line'] = 'partner_id'
return model_mapping

# ----------------------------------------
# Actions
# ----------------------------------------

@api.multi
def action_skip(self):
""" Skip this wizard line. Don't compute any thing, and simply redirect to the new step."""
if self.current_line_id:
self.current_line_id.unlink()
return self._action_next_screen()

@api.multi
def _action_next_screen(self):
""" return the action of the next screen ; this means the wizard is set to treat the
next wizard line. Each line is a subset of partner that can be merged together.
If no line left, the end screen will be displayed (but an action is still returned).
"""
self.invalidate_cache() # FIXME: is this still necessary?
values = {}
if self.line_ids:
# in this case, we try to find the next record.
current_line = self.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(current_partner_ids)[-1].id,
'state': 'selection',
})
else:
values.update({
'current_line_id': False,
'partner_ids': [],
'state': 'finished',
})

self.write(values)

return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}

@api.multi
def _process_query(self, query):
""" Execute the select request and write the result in this wizard
:param query : the SQL query used to fill the wizard line
"""
self.ensure_one()
model_mapping = self._compute_models()

# group partner query
self._cr.execute(query)

counter = 0
for min_id, aggr_ids in self._cr.fetchall():
# To ensure that the used partners are accessible by the user
partners = self.env['res.partner'].search([('id', 'in', aggr_ids)])
if len(partners) < 2:
continue

# exclude partner according to options
if model_mapping and self._partner_use_in(partners.ids, model_mapping):
continue

self.env['base.partner.merge.line'].create({
'wizard_id': self.id,
'min_id': min_id,
'aggr_ids': partners.ids,
})
counter += 1

self.write({
'state': 'selection',
'number_group': counter,
})

_logger.info("counter: %s", counter)

@api.multi
def action_start_manual_process(self):
""" Start the process 'Merge with Manual Check'. Fill the wizard according to the group_by and exclude
options, and redirect to the first step (treatment of first wizard line). After, for each subset of
partner to merge, the wizard will be actualized.
- Compute the selected groups (with duplication)
- If the user has selected the 'exclude_xxx' fields, avoid the partners
"""
self.ensure_one()
groups = self._compute_selected_groupby()
query = self._generate_query(groups, self.maximum_group)
self._process_query(query)
return self._action_next_screen()

@api.multi
def action_start_automatic_process(self):
""" Start the process 'Merge Automatically'. This will fill the wizard with the same mechanism as 'Merge
with Manual Check', but instead of refreshing wizard with the current line, it will automatically process
all lines by merging partner grouped according to the checked options.
"""
self.ensure_one()
self.action_start_manual_process() # here we don't redirect to the next screen, since it is automatic process
self.invalidate_cache() # FIXME: is this still necessary?

for line in self.line_ids:
partner_ids = literal_eval(line.aggr_ids)
self._merge(partner_ids)
line.unlink()
self._cr.commit() # TODO JEM : explain why

self.write({'state': 'finished'})
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}

@api.multi
def parent_migration_process_cb(self):
self.ensure_one()

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(query)

for line in self.line_ids:
partner_ids = literal_eval(line.aggr_ids)
self._merge(partner_ids)
line.unlink()
self._cr.commit()

self.write({'state': 'finished'})

self._cr.execute("""
UPDATE
res_partner
SET
is_company = NULL,
parent_id = NULL
WHERE
parent_id = id
""")

return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}

@api.multi
def action_update_all_process(self):
self.ensure_one()
self.parent_migration_process_cb()

# NOTE JEM : seems louche to create a new wizard instead of reuse the current one with updated options.
# since it is like this from the initial commit of this wizard, I don't change it. yet ...
wizard = self.create({'group_by_vat': True, 'group_by_email': True, 'group_by_name': True})
wizard.action_start_automatic_process()

# NOTE JEM : no idea if this query is usefull
self._cr.execute("""
UPDATE
res_partner
SET
is_company = NULL
WHERE
parent_id IS NOT NULL AND
is_company IS NOT NULL
""")

return self._action_next_screen()

@api.multi
def action_merge(self):
""" Merge Contact button. Merge the selected partners, and redirect to
the end screen (since there is no other wizard line to process.
"""
if not self.partner_ids:
self.write({'state': 'finished'})
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}

self._merge(self.partner_ids.ids, self.dst_partner_id)

if self.current_line_id:
self.current_line_id.unlink()

return self._action_next_screen()

+ 115
- 0
odoo_partner_merge/wizard/base_partner_merge_views.xml View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="action_partner_deduplicate" model="ir.actions.act_window">
<field name="name">Deduplicate Contacts</field>
<field name="res_model">base.partner.merge.automatic.wizard</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'active_test': False}</field>
</record>

<record id="base_partner_merge_automatic_wizard_form" model="ir.ui.view">
<field name='name'>base.partner.merge.automatic.wizard.form</field>
<field name='model'>base.partner.merge.automatic.wizard</field>
<field name='arch' type='xml'>
<form string='Automatic Merge Wizard'>
<sheet>
<group attrs="{'invisible': [('state', '!=', 'finished')]}" col="1">
<h2>There is no more contacts to merge for this request...</h2>
<button name="%(action_partner_deduplicate)d" string="Deduplicate the other Contacts" class="oe_highlight" type="action"/>
</group>
<p class="oe_grey" attrs="{'invisible': [('state', '!=', ('option'))]}">
Select the list of fields used to search for
duplicated records. If you select several fields,
Odoo will propose you to merge only those having
all these fields in common. (not one of the fields).
</p>
<group attrs="{'invisible': ['|', ('state', 'not in', ('selection', 'finished')), ('number_group', '=', 0)]}">
<field name="state" invisible="1" />
<field name="number_group"/>
</group>
<group string="Search duplicates based on duplicated data in"
attrs="{'invisible': [('state', 'not in', ('option',))]}">
<field name='group_by_email' />
<field name='group_by_name' />
<field name='group_by_is_company' />
<field name='group_by_vat' />
<field name='group_by_parent_id' />
</group>
<group string="Exclude contacts having"
attrs="{'invisible': [('state', 'not in', ('option',))]}">
<field name='exclude_contact' />
<field name='exclude_journal_item' />
</group>
<separator string="Options" attrs="{'invisible': [('state', 'not in', ('option',))]}"/>
<group attrs="{'invisible': [('state', 'not in', ('option','finished'))]}">
<field name='maximum_group' attrs="{'readonly': [('state', 'in', ('finished'))]}"/>
</group>
<separator string="Merge the following contacts"
attrs="{'invisible': [('state', 'in', ('option', 'finished'))]}"/>
<group attrs="{'invisible': [('state', 'in', ('option', 'finished'))]}" col="1">
<p class="oe_grey">
Selected contacts will be merged together.
All documents linked to one of these contacts
will be redirected to the destination contact.
You can remove contacts from this list to avoid merging them.
</p>
<group col="2">
<field name="dst_partner_id" domain="[('id', 'in', partner_ids and partner_ids[0] and partner_ids[0][2] or False)]" attrs="{'required': [('state', '=', 'selection')]}"/>
</group>
<field name="partner_ids" nolabel="1">
<tree string="Partners">
<field name="id" />
<field name="display_name" />
<field name="email" />
<field name="is_company" />
<field name="vat" />
<field name="country_id" />
</tree>
</field>
</group>
</sheet>
<footer>
<button name='action_merge' string='Merge Contacts'
class='oe_highlight'
type='object'
attrs="{'invisible': [('state', 'in', ('option', 'finished' ))]}" />
<button name='action_skip' string='Skip these contacts'
type='object'
attrs="{'invisible': [('state', '!=', 'selection')]}" />
<button name='action_start_manual_process'
string='Merge with Manual Check'
type='object' class='oe_highlight'
attrs="{'invisible': [('state', '!=', 'option')]}" />
<button name='action_start_automatic_process'
string='Merge Automatically'
type='object' class='oe_highlight'
confirm="Are you sure to execute the automatic merge of your contacts ?"
attrs="{'invisible': [('state', '!=', 'option')]}" />
<button name='action_update_all_process'
string='Merge Automatically all process'
type='object'
confirm="Are you sure to execute the list of automatic merges of your contacts ?"
attrs="{'invisible': [('state', '!=', 'option')]}" />
<span class="or_cancel" attrs="{'invisible': [('state', '=', 'finished')]} ">or
<button special="cancel" string="Cancel" type="object" class="oe_link oe_inline"/>
</span>
<span class="or_cancel" attrs="{'invisible': [('state', '!=', 'finished')]} ">
<button special="cancel" string="Close" type="object" class="oe_link oe_inline"/>
</span>
</footer>
</form>
</field>
</record>

<act_window
id="action_partner_merge"
res_model="base.partner.merge.automatic.wizard"
src_model="res.partner"
target="new"
multi="True"
key2="client_action_multi"
view_mode="form"
name="Merge Selected Contacts"/>
</odoo>

+ 123
- 0
odoo_partner_merge/wizard/validate_email.py View File

@@ -0,0 +1,123 @@
# RFC 2822 - style email validation for Python
# (c) 2012 Syrus Akbary <me@syrusakbary.com>
# Extended from (c) 2011 Noel Bush <noel@aitools.org>
# for support of mx and user check
# This code is made available to you under the GNU LGPL v3.
#
# This module provides a single method, valid_email_address(),
# which returns True or False to indicate whether a given address
# is valid according to the 'addr-spec' part of the specification
# given in RFC 2822. Ideally, we would like to find this
# in some other library, already thoroughly tested and well-
# maintained. The standard Python library email.utils
# contains a parse_addr() function, but it is not sufficient
# to detect many malformed addresses.
#
# This implementation aims to be faithful to the RFC, with the
# exception of a circular definition (see comments below), and
# with the omission of the pattern components marked as "obsolete".

import re
import smtplib
import socket

try:
import DNS
ServerError = DNS.ServerError
except:
DNS = None
class ServerError(Exception): pass
# All we are really doing is comparing the input string to one
# gigantic regular expression. But building that regexp, and
# ensuring its correctness, is made much easier by assembling it
# from the "tokens" defined by the RFC. Each of these tokens is
# tested in the accompanying unit test file.
#
# The section of RFC 2822 from which each pattern component is
# derived is given in an accompanying comment.
#
# (To make things simple, every string below is given as 'raw',
# even when it's not strictly necessary. This way we don't forget
# when it is necessary.)
#
WSP = r'[ \t]' # see 2.2.2. Structured Header Field Bodies
CRLF = r'(?:\r\n)' # see 2.2.3. Long Header Fields
NO_WS_CTL = r'\x01-\x08\x0b\x0c\x0f-\x1f\x7f' # see 3.2.1. Primitive Tokens
QUOTED_PAIR = r'(?:\\.)' # see 3.2.2. Quoted characters
FWS = r'(?:(?:' + WSP + r'*' + CRLF + r')?' + \
WSP + r'+)' # see 3.2.3. Folding white space and comments
CTEXT = r'[' + NO_WS_CTL + \
r'\x21-\x27\x2a-\x5b\x5d-\x7e]' # see 3.2.3
CCONTENT = r'(?:' + CTEXT + r'|' + \
QUOTED_PAIR + r')' # see 3.2.3 (NB: The RFC includes COMMENT here
# as well, but that would be circular.)
COMMENT = r'\((?:' + FWS + r'?' + CCONTENT + \
r')*' + FWS + r'?\)' # see 3.2.3
CFWS = r'(?:' + FWS + r'?' + COMMENT + ')*(?:' + \
FWS + '?' + COMMENT + '|' + FWS + ')' # see 3.2.3
ATEXT = r'[\w!#$%&\'\*\+\-/=\?\^`\{\|\}~]' # see 3.2.4. Atom
ATOM = CFWS + r'?' + ATEXT + r'+' + CFWS + r'?' # see 3.2.4
DOT_ATOM_TEXT = ATEXT + r'+(?:\.' + ATEXT + r'+)*' # see 3.2.4
DOT_ATOM = CFWS + r'?' + DOT_ATOM_TEXT + CFWS + r'?' # see 3.2.4
QTEXT = r'[' + NO_WS_CTL + \
r'\x21\x23-\x5b\x5d-\x7e]' # see 3.2.5. Quoted strings
QCONTENT = r'(?:' + QTEXT + r'|' + \
QUOTED_PAIR + r')' # see 3.2.5
QUOTED_STRING = CFWS + r'?' + r'"(?:' + FWS + \
r'?' + QCONTENT + r')*' + FWS + \
r'?' + r'"' + CFWS + r'?'
LOCAL_PART = r'(?:' + DOT_ATOM + r'|' + \
QUOTED_STRING + r')' # see 3.4.1. Addr-spec specification
DTEXT = r'[' + NO_WS_CTL + r'\x21-\x5a\x5e-\x7e]' # see 3.4.1
DCONTENT = r'(?:' + DTEXT + r'|' + \
QUOTED_PAIR + r')' # see 3.4.1
DOMAIN_LITERAL = CFWS + r'?' + r'\[' + \
r'(?:' + FWS + r'?' + DCONTENT + \
r')*' + FWS + r'?\]' + CFWS + r'?' # see 3.4.1
DOMAIN = r'(?:' + DOT_ATOM + r'|' + \
DOMAIN_LITERAL + r')' # see 3.4.1
ADDR_SPEC = LOCAL_PART + r'@' + DOMAIN # see 3.4.1

# A valid address will match exactly the 3.4.1 addr-spec.
VALID_ADDRESS_REGEXP = '^' + ADDR_SPEC + '$'

def validate_email(email, check_mx=False,verify=False):

"""Indicate whether the given string is a valid email address
according to the 'addr-spec' portion of RFC 2822 (see section
3.4.1). Parts of the spec that are marked obsolete are *not*
included in this test, and certain arcane constructions that
depend on circular definitions in the spec may not pass, but in
general this should correctly identify any email address likely
to be in use as of 2011."""
try:
assert re.match(VALID_ADDRESS_REGEXP, email) is not None
check_mx |= verify
if check_mx:
if not DNS: raise Exception('For check the mx records or check if the email exists you must have installed pyDNS python package')
DNS.DiscoverNameServers()
hostname = email[email.find('@')+1:]
mx_hosts = DNS.mxlookup(hostname)
for mx in mx_hosts:
try:
smtp = smtplib.SMTP()
smtp.connect(mx[1])
if not verify: return True
status, _ = smtp.helo()
if status != 250: continue
smtp.mail('')
status, _ = smtp.rcpt(email)
if status != 250: return False
break
except smtplib.SMTPServerDisconnected: #Server not permits verify user
break
except smtplib.SMTPConnectError:
continue
except (AssertionError, ServerError):
return False
return True

# import sys

# sys.modules[__name__],sys.modules['validate_email_module'] = validate_email,sys.modules[__name__]
# from validate_email_module import *

Loading…
Cancel
Save