forked from Yaltik/golem
[ADD]Partner merger spinoff when not using CRM module
This commit is contained in:
parent
dd2802da24
commit
6c2cd40020
18
odoo_partner_merge/__init__.py
Normal file
18
odoo_partner_merge/__init__.py
Normal 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
odoo_partner_merge/__manifest__.py
Normal file
30
odoo_partner_merge/__manifest__.py
Normal 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
odoo_partner_merge/i18n/fr.po
Normal file
328
odoo_partner_merge/i18n/fr.po
Normal 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
odoo_partner_merge/i18n/odoo_partner_merge.pot
Normal file
321
odoo_partner_merge/i18n/odoo_partner_merge.pot
Normal 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
odoo_partner_merge/wizard/__init__.py
Normal file
18
odoo_partner_merge/wizard/__init__.py
Normal 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
odoo_partner_merge/wizard/base_partner_merge.py
Normal file
654
odoo_partner_merge/wizard/base_partner_merge.py
Normal 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
odoo_partner_merge/wizard/base_partner_merge_views.xml
Normal file
115
odoo_partner_merge/wizard/base_partner_merge_views.xml
Normal 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
odoo_partner_merge/wizard/validate_email.py
Normal file
123
odoo_partner_merge/wizard/validate_email.py
Normal 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…
Reference in New Issue
Block a user