diff --git a/partner_match_or_create/README.rst b/partner_match_or_create/README.rst new file mode 100644 index 00000000..8bb35690 --- /dev/null +++ b/partner_match_or_create/README.rst @@ -0,0 +1,86 @@ +======================= +Partner Match or Create +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:9411adca8bbdcade1c4d70e5e297cfa4fb04b789e54f9343f8cb153bca414d7e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdonation-lightgray.png?logo=github + :target: https://github.com/OCA/donation/tree/16.0/partner_match_or_create + :alt: OCA/donation +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/donation-16-0/donation-16-0-partner_match_or_create + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/donation&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This is a technical module used by the OCA module **stay_api** and the +OCA module **donation_api**. It allows to share code between those 2 +modules. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion + +Contributors +------------ + +- Alexis de Lattre + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-alexis-via| image:: https://github.com/alexis-via.png?size=40px + :target: https://github.com/alexis-via + :alt: alexis-via + +Current `maintainer `__: + +|maintainer-alexis-via| + +This module is part of the `OCA/donation `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/partner_match_or_create/__init__.py b/partner_match_or_create/__init__.py new file mode 100644 index 00000000..379593d0 --- /dev/null +++ b/partner_match_or_create/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import wizards +from .post_install import res_partner_title_postinstall diff --git a/partner_match_or_create/__manifest__.py b/partner_match_or_create/__manifest__.py new file mode 100644 index 00000000..714ca516 --- /dev/null +++ b/partner_match_or_create/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2025 Akretion France (https://www.akretion.com) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Partner Match or Create", + "version": "16.0.1.0.0", + "category": "Tools", + "license": "AGPL-3", + "summary": "Create a new partner or match an existing partner", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["alexis-via"], + "website": "https://github.com/OCA/donation", + "depends": ["phone_validation"], + "data": [ + "security/ir.model.access.csv", + "views/res_partner_title.xml", + "wizards/partner_match_or_create.xml", + ], + "post_init_hook": "res_partner_title_postinstall", + "installable": True, +} diff --git a/partner_match_or_create/i18n/fr.po b/partner_match_or_create/i18n/fr.po new file mode 100644 index 00000000..89e75c20 --- /dev/null +++ b/partner_match_or_create/i18n/fr.po @@ -0,0 +1,333 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * partner_match_or_create +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-01-11 21:58+0000\n" +"PO-Revision-Date: 2026-01-11 21:58+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: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner_title__api_code +msgid "API Code" +msgstr "Code API" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.view_partner_title_form +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.view_partner_title_tree +msgid "API Code (do not modify)" +msgstr "Code API (ne pas modifier)" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Address" +msgstr "Adresse" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__street +msgid "Address Line 1" +msgstr "Adresse 1ère ligne" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__street2 +msgid "Address Line 2" +msgstr "Adresse 2ème ligne" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Cancel" +msgstr "Annuler" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__city +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "City" +msgstr "Ville" + +#. module: partner_match_or_create +#: model:ir.model,name:partner_match_or_create.model_res_partner +msgid "Contact" +msgstr "" + +#. module: partner_match_or_create +#: code:addons/partner_match_or_create/wizards/partner_match_or_create.py:0 +#, python-format +msgid "" +"Contact %(partner_name)s created from web form information." +msgstr "Contact %(partner_name)s créé à partir des informations du formulaire Web." + +#. module: partner_match_or_create +#: code:addons/partner_match_or_create/wizards/partner_match_or_create.py:0 +#, python-format +msgid "" +"Contact created by the wizard of the module " +"partner_match_or_create." +msgstr "" +"Contact créé par l'assistant du module " +"partner_match_or_create." + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_id +msgid "Contact to Update" +msgstr "Contact à mettre à jour" + +#. module: partner_match_or_create +#: code:addons/partner_match_or_create/wizards/partner_match_or_create.py:0 +#, python-format +msgid "" +"Contact updated by the wizard of the module " +"partner_match_or_create." +msgstr "" +"Contact mise à jour par l'assistant du module " +"partner_match_or_create." + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__country_id +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Country" +msgstr "Pays" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Create New Contact" +msgstr "Créer une nouveau contact" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__create_or_update +msgid "Create Or Update" +msgstr "Créer ou mettre à jour" + +#. module: partner_match_or_create +#: model:ir.actions.act_window,name:partner_match_or_create.partner_match_or_create_action +msgid "Create or Update Contact" +msgstr "Créer ou mettre à jour un contact" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__create_uid +msgid "Created by" +msgstr "Créé par" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__create_date +msgid "Created on" +msgstr "Créé le" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Current Address" +msgstr "Adresse actuelle" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_city +msgid "Current City" +msgstr "Ville actuelle" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_country_id +msgid "Current Country" +msgstr "Pays actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_email +msgid "Current E-mail" +msgstr "E-mail actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_mobile +msgid "Current Mobile" +msgstr "Portable actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_phone +msgid "Current Phone" +msgstr "Téléphone actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_state_id +msgid "Current State" +msgstr "État actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_street +msgid "Current Street" +msgstr "Rue actuelle" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_street2 +msgid "Current Street2" +msgstr "Rue2 actuelle" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_partner_zip +msgid "Current ZIP" +msgstr "Code postal actuel" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__display_name +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner__display_name +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner_title__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__email +msgid "E-mail" +msgstr "E-mail" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__firstname +msgid "Firstname" +msgstr "Prénom" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__id +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner__id +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner_title__id +msgid "ID" +msgstr "" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create____last_update +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner____last_update +#: model:ir.model.fields,field_description:partner_match_or_create.field_res_partner_title____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__write_uid +msgid "Last Updated by" +msgstr "Dernière mise à jour par" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__write_date +msgid "Last Updated on" +msgstr "Dernière mise à jour le" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__lastname +msgid "Lastname" +msgstr "Nom de famille" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__mobile +msgid "Mobile" +msgstr "Portable" + +#. module: partner_match_or_create +#: code:addons/partner_match_or_create/wizards/partner_match_or_create.py:0 +#, python-format +msgid "New Partner" +msgstr "Nouveau partenaire" + +#. module: partner_match_or_create +#: model:ir.model,name:partner_match_or_create.model_res_partner_title +msgid "Partner Title" +msgstr "Titre du partenaire" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__phone +msgid "Phone" +msgstr "Téléphone" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__res_id +msgid "Res" +msgstr "" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__res_model +msgid "Res Model" +msgstr "Modèle Res" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__state_id +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "State" +msgstr "État" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__suggested_partner_ids +msgid "Suggested Contacts" +msgstr "Contacts suggérés" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Suggested Existing Contacts" +msgstr "Contacts existants suggérés" + +#. module: partner_match_or_create +#: model:ir.model.fields,help:partner_match_or_create.field_res_partner_title__api_code +msgid "Technical code used by the API. Do not modify!" +msgstr "Code technique utilisé par l'API. Ne pas modifier !" + +#. module: partner_match_or_create +#: code:addons/partner_match_or_create/wizards/partner_match_or_create.py:0 +#, python-format +msgid "The partner to update is not set." +msgstr "Le partenaire à mettre à jour n'est pas défini." + +#. module: partner_match_or_create +#: model:ir.model.constraint,message:partner_match_or_create.constraint_res_partner_title_api_code_uniq +msgid "This API code already exists." +msgstr "Ce code API existe déjà." + +#. module: partner_match_or_create +#: model:ir.model.fields.selection,name:partner_match_or_create.selection__partner_match_or_create__create_or_update__update +msgid "This partner already exists in Odoo" +msgstr "Ce partenaire existe déjà dans Odoo" + +#. module: partner_match_or_create +#: model:ir.model.fields.selection,name:partner_match_or_create.selection__partner_match_or_create__create_or_update__create +msgid "This partner doesn't already exists in Odoo" +msgstr "Ce partenaire n'existe pas dans Odoo" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__title_id +msgid "Title" +msgstr "Titre" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_address +msgid "Update Address" +msgstr "Mettre à jour l'adresse" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_email +msgid "Update E-mail" +msgstr "Mettre à jour l'e-mail" + +#. module: partner_match_or_create +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "Update Existing Contact" +msgstr "Mettre à jour un contact existant" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_mobile +msgid "Update Mobile" +msgstr "Mettre à jour le portable" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__update_phone +msgid "Update Phone" +msgstr "Mettre à jour le téléphone" + +#. module: partner_match_or_create +#: model:ir.model,name:partner_match_or_create.model_partner_match_or_create +msgid "Wizard to match/update an existing contact or create a new contact" +msgstr "Assistant pour trouver/mettre à jour un contact existant ou créer un nouveau contact" + +#. module: partner_match_or_create +#: model:ir.model.fields,field_description:partner_match_or_create.field_partner_match_or_create__zip +#: model_terms:ir.ui.view,arch_db:partner_match_or_create.partner_match_or_create_form +msgid "ZIP" +msgstr "Code postal" diff --git a/partner_match_or_create/models/__init__.py b/partner_match_or_create/models/__init__.py new file mode 100644 index 00000000..9873bb27 --- /dev/null +++ b/partner_match_or_create/models/__init__.py @@ -0,0 +1,2 @@ +from . import res_partner +from . import res_partner_title diff --git a/partner_match_or_create/models/res_partner.py b/partner_match_or_create/models/res_partner.py new file mode 100644 index 00000000..253d2fca --- /dev/null +++ b/partner_match_or_create/models/res_partner.py @@ -0,0 +1,71 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import models + +logger = logging.getLogger(__name__) + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def _controller_try_match_partner(self, vals): + email = vals["controller_email"] + mobile = vals["controller_mobile"] + partner_id = None + if "res.partner.phone" in self.env: # module base_partner_one2many_phone + partner_phone = ( + self.env["res.partner.phone"] + .sudo() + .search_read( + [ + ("type", "in", ("1_email_primary", "2_email_secondary")), + ("email", "=ilike", email), + ("partner_id", "!=", False), + ], + ["partner_id"], + limit=1, + ) + ) + if partner_phone: + partner_id = partner_phone[0]["partner_id"][0] + else: + partner = self.env["res.partner"].search_read( + [("email", "=ilike", email)], ["id"], limit=1 + ) + if partner: + partner_id = partner[0]["id"] + if partner_id: + logger.info("Match on email %s with partner ID %d", email, partner_id) + # 'and vals['controller_country_id'] to make sure the mobile phone has been reformatted + if not partner_id and mobile and vals["controller_country_id"]: + if "res.partner.phone" in self.env: # module base_partner_one2many_phone + partner_phone = ( + self.env["res.partner.phone"] + .sudo() + .search_read( + [ + ("type", "in", ("5_mobile_primary", "6_mobile_secondary")), + ("phone", "=", mobile), + ("partner_id", "!=", False), + ], + ["partner_id"], + limit=1, + ) + ) + if partner_phone: + partner_id = partner_phone[0]["partner_id"][0] + else: + partner = self.env["res.partner"].search_read( + [("mobile", "=", mobile)], ["id"], limit=1 + ) + if partner: + partner_id = partner[0]["id"] + if partner_id: + logger.info("Match on mobile %s with partner ID %d", mobile, partner_id) + if not partner_id: + logger.info("No match on an existing partner") + return partner_id diff --git a/partner_match_or_create/models/res_partner_title.py b/partner_match_or_create/models/res_partner_title.py new file mode 100644 index 00000000..b57b8826 --- /dev/null +++ b/partner_match_or_create/models/res_partner_title.py @@ -0,0 +1,21 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResPartnerTitle(models.Model): + _inherit = "res.partner.title" + + # We need to have this code to make the API work for titles + # created by hand by the user that don't have any XMLID + api_code = fields.Char( + string="API Code", + copy=False, + help="Technical code used by the API. Do not modify!", + ) + + _sql_constraints = [ + ("api_code_uniq", "unique(api_code)", "This API code already exists.") + ] diff --git a/partner_match_or_create/post_install.py b/partner_match_or_create/post_install.py new file mode 100644 index 00000000..87da9e7e --- /dev/null +++ b/partner_match_or_create/post_install.py @@ -0,0 +1,42 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import SUPERUSER_ID, api + +logger = logging.getLogger(__name__) + + +def res_partner_title_postinstall(cr, registry): + with api.Environment.manage(): + env = api.Environment(cr, SUPERUSER_ID, {}) + # set api_code on res.partner.title + model_datas = env["ir.model.data"].search( + [ + ("module", "=", "base"), + ("model", "=", "res.partner.title"), + ("res_id", "!=", False), + ("name", "!=", False), + ] + ) + unique_code = set() + for model_data in model_datas: + api_code = model_data.name.split("_")[-1] + if api_code in unique_code: + logger.warning( + "Skipping XMLID %s.%s because the suffix is not unique", + model_data.module, + model_data.name, + ) + continue + unique_code.add(api_code) + title = env["res.partner.title"].browse(model_data.res_id) + title.write({"api_code": api_code}) + logger.info( + "Wrote api_code=%s on title %s ID %d", + api_code, + title.display_name, + title.id, + ) diff --git a/partner_match_or_create/readme/CONTRIBUTORS.md b/partner_match_or_create/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..b61afe5d --- /dev/null +++ b/partner_match_or_create/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Alexis de Lattre \<\> diff --git a/partner_match_or_create/readme/DESCRIPTION.md b/partner_match_or_create/readme/DESCRIPTION.md new file mode 100644 index 00000000..2fc4c026 --- /dev/null +++ b/partner_match_or_create/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This is a technical module used by the OCA module **stay\_api** and the OCA module **donation\_api**. It allows to share code between those 2 modules. diff --git a/partner_match_or_create/security/ir.model.access.csv b/partner_match_or_create/security/ir.model.access.csv new file mode 100644 index 00000000..df2694d1 --- /dev/null +++ b/partner_match_or_create/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_partner_match_or_create,Full access on partner.match.or.create wizard,model_partner_match_or_create,base.group_partner_manager,1,1,1,1 diff --git a/partner_match_or_create/static/description/icon.png b/partner_match_or_create/static/description/icon.png new file mode 100644 index 00000000..1dcc49c2 Binary files /dev/null and b/partner_match_or_create/static/description/icon.png differ diff --git a/partner_match_or_create/static/description/index.html b/partner_match_or_create/static/description/index.html new file mode 100644 index 00000000..e0001d1e --- /dev/null +++ b/partner_match_or_create/static/description/index.html @@ -0,0 +1,427 @@ + + + + + +Partner Match or Create + + + +
+

Partner Match or Create

+ + +

Beta License: AGPL-3 OCA/donation Translate me on Weblate Try me on Runboat

+

This is a technical module used by the OCA module stay_api and the +OCA module donation_api. It allows to share code between those 2 +modules.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

alexis-via

+

This module is part of the OCA/donation project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/partner_match_or_create/views/res_partner_title.xml b/partner_match_or_create/views/res_partner_title.xml new file mode 100644 index 00000000..eb9fde3c --- /dev/null +++ b/partner_match_or_create/views/res_partner_title.xml @@ -0,0 +1,40 @@ + + + + + + + res.partner.title + + + + + + + + + + res.partner.title + + + + + + + + + + diff --git a/partner_match_or_create/wizards/__init__.py b/partner_match_or_create/wizards/__init__.py new file mode 100644 index 00000000..c0cbeac0 --- /dev/null +++ b/partner_match_or_create/wizards/__init__.py @@ -0,0 +1 @@ +from . import partner_match_or_create diff --git a/partner_match_or_create/wizards/partner_match_or_create.py b/partner_match_or_create/wizards/partner_match_or_create.py new file mode 100644 index 00000000..e99e826e --- /dev/null +++ b/partner_match_or_create/wizards/partner_match_or_create.py @@ -0,0 +1,322 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.osv import expression + +logger = logging.getLogger(__name__) + + +class PartnerMatchOrCreate(models.TransientModel): + _name = "partner.match.or.create" + _description = "Wizard to match/update an existing contact or create a new contact" + + res_model = fields.Char(required=True, readonly=True) + res_id = fields.Integer(required=True, readonly=True) + firstname = fields.Char() + lastname = fields.Char(required=True) + title_id = fields.Many2one("res.partner.title") + email = fields.Char(string="E-mail") + phone = fields.Char() + mobile = fields.Char() + street = fields.Char(string="Address Line 1") + street2 = fields.Char(string="Address Line 2") + zip = fields.Char(string="ZIP") + city = fields.Char() + state_id = fields.Many2one("res.country.state") + country_id = fields.Many2one("res.country") + update_partner_id = fields.Many2one( + "res.partner", + string="Contact to Update", + compute="_compute_update_partner_id", + store=True, + readonly=False, + precompute=True, + ) + update_partner_email = fields.Char( + related="update_partner_id.email", string="Current E-mail" + ) + update_partner_phone = fields.Char( + related="update_partner_id.phone", string="Current Phone" + ) + update_partner_mobile = fields.Char( + related="update_partner_id.mobile", string="Current Mobile" + ) + # I can't use a related on update_partner_id because the full address + # is not displayed any more when update_partner_id is changed + update_partner_street = fields.Char( + related="update_partner_id.street", string="Current Street" + ) + update_partner_street2 = fields.Char( + related="update_partner_id.street2", string="Current Street2" + ) + update_partner_zip = fields.Char( + related="update_partner_id.zip", string="Current ZIP" + ) + update_partner_city = fields.Char( + related="update_partner_id.city", string="Current City" + ) + update_partner_state_id = fields.Many2one( + related="update_partner_id.state_id", string="Current State" + ) + update_partner_country_id = fields.Many2one( + related="update_partner_id.country_id", string="Current Country" + ) + update_email = fields.Boolean( + compute="_compute_update_bool", + readonly=False, + store=True, + string="Update E-mail", + precompute=True, + ) + update_phone = fields.Boolean( + compute="_compute_update_bool", + readonly=False, + store=True, + precompute=True, + ) + update_mobile = fields.Boolean( + compute="_compute_update_bool", + readonly=False, + store=True, + precompute=True, + ) + update_address = fields.Boolean( + compute="_compute_update_bool", + readonly=False, + store=True, + precompute=True, + ) + suggested_partner_ids = fields.Many2many( + "res.partner", readonly=True, string="Suggested Contacts" + ) + create_or_update = fields.Selection( + [ + ("create", "This partner doesn't already exists in Odoo"), + ("update", "This partner already exists in Odoo"), + ], + required=True, + ) + + @api.depends( + "update_partner_id", + "email", + "phone", + "mobile", + "street", + "street2", + "city", + "state_id", + "zip", + "country_id", + ) + def _compute_update_bool(self): + for wiz in self: + update_email = False + update_phone = False + update_mobile = False + update_address = False + upartner = wiz.update_partner_id + if upartner: + if wiz.email and wiz.email != upartner.email: + update_email = True + if wiz.phone and wiz.phone != upartner.phone: + update_phone = True + if wiz.mobile and wiz.mobile != upartner.mobile: + update_mobile = True + if ( + wiz.street + and wiz.city + and wiz.zip + and wiz.country_id + and any( + [ + wiz.street != upartner.street, + wiz.street2 != upartner.street2, + wiz.city != upartner.city, + wiz.state_id != upartner.state_id, + wiz.zip != upartner.zip, + wiz.country_id != upartner.country_id, + ] + ) + ): + update_address = True + wiz.update_email = update_email + wiz.update_phone = update_phone + wiz.update_mobile = update_mobile + wiz.update_address = update_address + + @api.depends("create_or_update") + def _compute_update_partner_id(self): + for wiz in self: + if wiz.create_or_update == "create": + wiz.update_partner_id = False + + def _prepare_suggested_partner_domain(self, vals): + rpo = self.env["res.partner"] + # similar lastname + domain_or_list = [] + if vals.get("lastname"): + max_lastname_split = max(vals["lastname"].split(" "), key=len) + logger.info( + "Populating suggested partners with max_lastname_split=%s", + max_lastname_split, + ) + if hasattr(rpo, "lastname"): + domain_or_list.append([("lastname", "ilike", max_lastname_split)]) + else: + domain_or_list.append([("name", "ilike", max_lastname_split)]) + elif vals.get("zip"): + domain_or_list.append([("zip", "=", vals["zip"])]) + # same country + if vals.get("country_id"): + domain_or_list.append([("country_id", "=", vals["country_id"])]) + domain = expression.AND(domain_or_list) + return domain + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + res_model = self._context.get("active_model") + assert res_model + res_id = self._context.get("active_id") + assert res_id + record = self.env[res_model].browse(res_id) + # partner may have been created in the meantime + update_partner = False + if hasattr(record, "controller_email") and record.controller_email: + update_partner = self.env["res.partner"].search( + [("email", "=ilike", record.controller_email)], limit=1 + ) + res.update( + { + "res_model": res_model, + "res_id": res_id, + "create_or_update": update_partner and "update" or "create", + "update_partner_id": update_partner and update_partner.id or False, + } + ) + for rfield in [ + "firstname", + "lastname", + "title_id", + "email", + "phone", + "mobile", + "street", + "street2", + "zip", + "city", + "state_id", + "country_id", + ]: + controller_field = f"controller_{rfield}" + if hasattr(record, controller_field): + value = record[controller_field] + if rfield == "email": + value = value and value.lower() or False + elif rfield.endswith("_id"): + value = value and value.id or False + res[rfield] = value + suggested_partner_domain = self._prepare_suggested_partner_domain(res) + suggested_partner_ids = ( + self.env["res.partner"].search(suggested_partner_domain).ids + ) + res["suggested_partner_ids"] = [(6, 0, suggested_partner_ids)] + if suggested_partner_ids: + res["create_or_update"] = "update" + else: + res["create_or_update"] = "create" + return res + + def create_partner(self): + self.ensure_one() + rpo = self.env["res.partner"] + vals = { + "email": self.email, + "phone": self.phone, + "mobile": self.mobile, + "street": self.street, + "street2": self.street2, + "zip": self.zip, + "city": self.city, + "state_id": self.state_id.id or False, + "country_id": self.country_id.id or False, + "title": self.title_id.id or False, + } + # if OCA module partner_firstname is installed + if hasattr(rpo, "firstname") and hasattr(rpo, "lastname"): + vals.update( + { + "firstname": self.firstname, + "lastname": self.lastname, + } + ) + else: + name = self.lastname + if self.firstname: + name = f"{self.firstname} {name}" + vals["name"] = name + partner = self.env["res.partner"].create(vals) + model = self.env[self.res_model] + partner.message_post( + body=_( + "Contact created by the wizard of the module partner_match_or_create." + ) + ) + record = model.browse(self.res_id) + record.write({"partner_id": partner.id}) + record.message_post( + body=_( + "Contact " + "%(partner_name)s created from web form information.", + partner_id=partner.id, + partner_name=partner.display_name, + ) + ) + action = { + "type": "ir.actions.act_window", + "name": _("New Partner"), + "res_model": "res.partner", + "view_mode": "form", + "res_id": partner.id, + } + return action + + def update_partner(self): + self.ensure_one() + if not self.update_partner_id: + raise UserError(_("The partner to update is not set.")) + vals = {} + if self.update_phone: + vals["phone"] = self.phone + if self.update_mobile: + vals["mobile"] = self.mobile + if self.update_email: + vals["email"] = self.email + if self.update_address: + vals.update( + { + "street": self.street, + "street2": self.street2, + "zip": self.zip, + "city": self.city, + "state_id": self.state_id.id or False, + "country_id": self.country_id.id or False, + } + ) + model = self.env[self.res_model] + if vals: + self.update_partner_id.write(vals) + msg = _( + "Contact updated by the wizard of the module " + "partner_match_or_create." + ) + self.update_partner_id.message_post(body=msg) + record = model.browse(self.res_id) + record.write({"partner_id": self.update_partner_id.id}) + record.message_post(body=msg) diff --git a/partner_match_or_create/wizards/partner_match_or_create.xml b/partner_match_or_create/wizards/partner_match_or_create.xml new file mode 100644 index 00000000..357032ba --- /dev/null +++ b/partner_match_or_create/wizards/partner_match_or_create.xml @@ -0,0 +1,152 @@ + + + + + + + partner.match.or.create + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Create or Update Contact + partner.match.or.create + form + new + + + +
diff --git a/setup/partner_match_or_create/odoo/addons/partner_match_or_create b/setup/partner_match_or_create/odoo/addons/partner_match_or_create new file mode 120000 index 00000000..43acf7f7 --- /dev/null +++ b/setup/partner_match_or_create/odoo/addons/partner_match_or_create @@ -0,0 +1 @@ +../../../../partner_match_or_create \ No newline at end of file diff --git a/setup/partner_match_or_create/setup.py b/setup/partner_match_or_create/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/partner_match_or_create/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)