From 38acbe56254daa95cad4f1c1d992857d047b05f6 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 9 Jul 2025 00:10:47 +0200 Subject: [PATCH] [ADD] Add modules donation_api and partner_match_or_create The module partner_match_or_create is designed to mutualize code between stay_api and donation_api --- donation/models/donation.py | 23 +- donation/views/donation.xml | 6 +- donation_api/README.rst | 92 ++++ donation_api/__init__.py | 4 + donation_api/__manifest__.py | 24 + donation_api/data/mail_template.xml | 68 +++ donation_api/data/res_users.xml | 19 + donation_api/models/__init__.py | 2 + donation_api/models/donation_donation.py | 184 ++++++++ donation_api/models/fastapi_endpoint.py | 35 ++ donation_api/readme/CONTRIBUTORS.md | 1 + donation_api/readme/DESCRIPTION.md | 1 + donation_api/readme/INSTALL.md | 1 + donation_api/routers/__init__.py | 1 + donation_api/routers/donation.py | 144 ++++++ donation_api/schemas/__init__.py | 2 + donation_api/schemas/donation_create.py | 23 + donation_api/schemas/donation_created.py | 14 + donation_api/static/description/icon.png | Bin 0 -> 10254 bytes donation_api/static/description/index.html | 433 ++++++++++++++++++ donation_api/views/donation_donation.xml | 78 ++++ donation_api/wizards/__init__.py | 1 + donation_api/wizards/res_config_settings.py | 15 + .../wizards/res_config_settings_view.xml | 34 ++ partner_match_or_create/README.rst | 86 ++++ partner_match_or_create/__init__.py | 3 + partner_match_or_create/__manifest__.py | 22 + partner_match_or_create/i18n/fr.po | 333 ++++++++++++++ partner_match_or_create/models/__init__.py | 2 + partner_match_or_create/models/res_partner.py | 71 +++ .../models/res_partner_title.py | 21 + partner_match_or_create/post_install.py | 42 ++ .../readme/CONTRIBUTORS.md | 1 + partner_match_or_create/readme/DESCRIPTION.md | 1 + .../security/ir.model.access.csv | 2 + .../static/description/icon.png | Bin 0 -> 10254 bytes .../static/description/index.html | 427 +++++++++++++++++ .../views/res_partner_title.xml | 40 ++ partner_match_or_create/wizards/__init__.py | 1 + .../wizards/partner_match_or_create.py | 311 +++++++++++++ .../wizards/partner_match_or_create.xml | 143 ++++++ requirements.txt | 3 + setup/donation_api/odoo/addons/donation_api | 1 + setup/donation_api/setup.py | 6 + .../odoo/addons/partner_match_or_create | 1 + setup/partner_match_or_create/setup.py | 6 + 46 files changed, 2717 insertions(+), 11 deletions(-) create mode 100644 donation_api/README.rst create mode 100644 donation_api/__init__.py create mode 100644 donation_api/__manifest__.py create mode 100644 donation_api/data/mail_template.xml create mode 100644 donation_api/data/res_users.xml create mode 100644 donation_api/models/__init__.py create mode 100644 donation_api/models/donation_donation.py create mode 100644 donation_api/models/fastapi_endpoint.py create mode 100644 donation_api/readme/CONTRIBUTORS.md create mode 100644 donation_api/readme/DESCRIPTION.md create mode 100644 donation_api/readme/INSTALL.md create mode 100644 donation_api/routers/__init__.py create mode 100644 donation_api/routers/donation.py create mode 100644 donation_api/schemas/__init__.py create mode 100644 donation_api/schemas/donation_create.py create mode 100644 donation_api/schemas/donation_created.py create mode 100644 donation_api/static/description/icon.png create mode 100644 donation_api/static/description/index.html create mode 100644 donation_api/views/donation_donation.xml create mode 100644 donation_api/wizards/__init__.py create mode 100644 donation_api/wizards/res_config_settings.py create mode 100644 donation_api/wizards/res_config_settings_view.xml create mode 100644 partner_match_or_create/README.rst create mode 100644 partner_match_or_create/__init__.py create mode 100644 partner_match_or_create/__manifest__.py create mode 100644 partner_match_or_create/i18n/fr.po create mode 100644 partner_match_or_create/models/__init__.py create mode 100644 partner_match_or_create/models/res_partner.py create mode 100644 partner_match_or_create/models/res_partner_title.py create mode 100644 partner_match_or_create/post_install.py create mode 100644 partner_match_or_create/readme/CONTRIBUTORS.md create mode 100644 partner_match_or_create/readme/DESCRIPTION.md create mode 100644 partner_match_or_create/security/ir.model.access.csv create mode 100644 partner_match_or_create/static/description/icon.png create mode 100644 partner_match_or_create/static/description/index.html create mode 100644 partner_match_or_create/views/res_partner_title.xml create mode 100644 partner_match_or_create/wizards/__init__.py create mode 100644 partner_match_or_create/wizards/partner_match_or_create.py create mode 100644 partner_match_or_create/wizards/partner_match_or_create.xml create mode 100644 requirements.txt create mode 120000 setup/donation_api/odoo/addons/donation_api create mode 100644 setup/donation_api/setup.py create mode 120000 setup/partner_match_or_create/odoo/addons/partner_match_or_create create mode 100644 setup/partner_match_or_create/setup.py diff --git a/donation/models/donation.py b/donation/models/donation.py index 3f0cbf776..7c60e2978 100644 --- a/donation/models/donation.py +++ b/donation/models/donation.py @@ -75,7 +75,7 @@ def _compute_country_id(self): partner_id = fields.Many2one( "res.partner", string="Donor", - required=True, + required=False, # now only required on confirmation index=True, states={"done": [("readonly", True)]}, tracking=True, @@ -253,15 +253,16 @@ def _compute_country_id(self): ) ] - @api.model - def create(self, vals): - if "company_id" in vals: - self = self.with_company(vals["company_id"]) - if vals.get("number", _("New")) == _("New"): - vals["number"] = self.env["ir.sequence"].next_by_code( - "donation.donation", sequence_date=vals.get("donation_date") - ) or _("New") - return super().create(vals) + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if "company_id" in vals: + self = self.with_company(vals["company_id"]) + if vals.get("number", _("New")) == _("New"): + vals["number"] = self.env["ir.sequence"].next_by_code( + "donation.donation", sequence_date=vals.get("donation_date") + ) or _("New") + return super().create(vals_list) def _prepare_each_tax_receipt(self): self.ensure_one() @@ -395,6 +396,8 @@ def validate(self): "donation.group_donation_check_total" ) for donation in self: + if not donation.partner_id: + raise UserError(_("Donor is not set on donation %s.") % donation.number) if donation.donation_date > fields.Date.context_today(self): raise UserError( _( diff --git a/donation/views/donation.xml b/donation/views/donation.xml index 86cff4e42..af22e5027 100644 --- a/donation/views/donation.xml +++ b/donation/views/donation.xml @@ -214,7 +214,11 @@ donation.donation - + `__. + +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/donation_api/__init__.py b/donation_api/__init__.py new file mode 100644 index 000000000..d3a7b2ba2 --- /dev/null +++ b/donation_api/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import schemas +from . import routers +from . import wizards diff --git a/donation_api/__manifest__.py b/donation_api/__manifest__.py new file mode 100644 index 000000000..c0c4f45f0 --- /dev/null +++ b/donation_api/__manifest__.py @@ -0,0 +1,24 @@ +# 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": "Donation", + "version": "14.0.1.0.0", + "category": "Donation", + "license": "AGPL-3", + "summary": "API for donation module", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["alexis-via"], + "website": "https://github.com/OCA/donation", + "depends": ["donation", "fastapi", "partner_match_or_create"], + "external_dependencies": {"python": ["fastapi", "pydantic<2"]}, + "data": [ + "data/res_users.xml", + # "security/ir.model.access.csv", + "wizards/res_config_settings_view.xml", + "views/donation_donation.xml", + # "data/mail_template.xml", + ], + "installable": True, +} diff --git a/donation_api/data/mail_template.xml b/donation_api/data/mail_template.xml new file mode 100644 index 000000000..8aeb17e8b --- /dev/null +++ b/donation_api/data/mail_template.xml @@ -0,0 +1,68 @@ + + + + + + + Stay: Notify stay creation/update/cancel from web form + + ${object.company_id.email} + ${object.group_id and object.group_id.notify_user_ids and str(object.group_id.notify_user_ids.partner_id.ids)[1:-1] or (object.company_id.stay_notify_user_ids and str(object.company_id.stay_notify_user_ids.partner_id.ids)[1:-1])} + ${object.controller_email} + + Stay ${ctx.get('action_description')}: ${object.partner_name} ${format_date(object.arrival_date)} → ${format_date(object.departure_date)} x ${object.guest_qty} + ${object.company_id.partner_id.lang} + +
+

Stay ${object.name} has been ${ctx.get('action_description')} via the web form:

+
    +
  • Guest: ${object.partner_name}
  • +
  • Guest Qty: ${object.guest_qty}
  • +
  • Arrival: ${format_date(object.arrival_date)} ${dict(object.fields_get('arrival_time', 'selection')['arrival_time']['selection'])[object.arrival_time]}
  • + % if object.arrival_note: +
  • Arrival Note: ${object.arrival_note}
  • + % endif +
  • Departure: ${format_date(object.departure_date)} ${dict(object.fields_get('departure_time', 'selection')['departure_time']['selection'])[object.departure_time]}
  • + % if object.departure_note: +
  • Departure Note: ${object.departure_note}
  • + % endif + % if object.group_id: +
  • Group: ${object.group_id.display_name}
  • + % endif + % if object.controller_email: +
  • E-mail: ${object.controller_email}
  • + % endif + % if object.controller_phone: +
  • Phone: ${object.controller_phone}
  • + % endif + % if object.controller_mobile: +
  • Mobile: ${object.controller_mobile}
  • + % endif + % if object.controller_message: +
  • Guest message: ${object.controller_message}
  • + % endif + % if object.controller_notes: +
  • Other information: ${object.controller_notes}
  • + % endif + % if object.controller_country_id: +
  • Country: ${object.controller_country_id.name}
  • + % endif +
+
+
+
+ + +
diff --git a/donation_api/data/res_users.xml b/donation_api/data/res_users.xml new file mode 100644 index 000000000..749c6c8bb --- /dev/null +++ b/donation_api/data/res_users.xml @@ -0,0 +1,19 @@ + + + + + + Donation API User + donation_api_user + donation_api_user@example.org + + + + diff --git a/donation_api/models/__init__.py b/donation_api/models/__init__.py new file mode 100644 index 000000000..880800a0b --- /dev/null +++ b/donation_api/models/__init__.py @@ -0,0 +1,2 @@ +from . import fastapi_endpoint +from . import donation_donation diff --git a/donation_api/models/donation_donation.py b/donation_api/models/donation_donation.py new file mode 100644 index 000000000..73f2d0b6a --- /dev/null +++ b/donation_api/models/donation_donation.py @@ -0,0 +1,184 @@ +# 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 fastapi import HTTPException, status + +from odoo import _, api, fields, models + +logger = logging.getLogger(__name__) + + +UUID_VERSION = 4 + + +class DonationDonation(models.Model): + _inherit = "donation.donation" + + controller_mode = fields.Selection( + [ + ("created", "Created"), + ], + readonly=True, + string="Web Form Mode", + ) + controller_firstname = fields.Char(tracking=True, string="Firstname") + controller_lastname = fields.Char(tracking=True, string="Lastname") + controller_title_id = fields.Many2one( + "res.partner.title", + domain=[("api_code", "!=", False)], + string="Title", + tracking=True, + ) + controller_email = fields.Char(tracking=True, string="E-mail") + controller_phone = fields.Char(tracking=True, string="Phone") + controller_mobile = fields.Char(tracking=True, string="Mobile") + controller_message = fields.Char(string="Donor Message") + controller_notes = fields.Text(string="Web Form Other Information") + controller_street = fields.Char(string="Address Line 1") + controller_street2 = fields.Char(string="Address Line 2") + controller_zip = fields.Char(string="ZIP") + controller_city = fields.Char(string="City") + controller_country_id = fields.Many2one("res.country", string="Country") + + @api.model + def _controller_prepare_create_update(self, cobject, try_match_partner=True): + assert cobject + to_strip_fields = [ + "firstname", + "lastname", + "street", + "street2", + "zip", + "city", + "country_code", + "email", + "phone", + "mobile", + "departure_note", + "arrival_note", + ] + for to_strip_field in to_strip_fields: + ini_value = getattr(cobject, to_strip_field) + if isinstance(ini_value, str): + setattr(cobject, to_strip_field, ini_value.strip() or False) + time_values_allowed = ("morning", "afternoon", "evening") + arrival_time = cobject.arrival_time + if arrival_time not in time_values_allowed: + error_msg = ( + f"Wrong arrival time: {arrival_time}. " + f"Possible values: {', '.join(time_values_allowed)}." + ) + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + departure_time = cobject.departure_time + if departure_time not in time_values_allowed: + error_msg = ( + f"Wrong departure time: {departure_time}. " + f"Possible values: {', '.join(time_values_allowed)}." + ) + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + notes_list = cobject.notes_list + if not isinstance(notes_list, list): + notes_list = [] + lastname = cobject.lastname + if not lastname: # Should never happen because checked by fastapi + logger.error("Missing lastname in stay controller. Quitting.") + return False + partner_name = lastname + firstname = cobject.firstname + if firstname: + partner_name = f"{firstname} {partner_name}" + title_code = cobject.title + title_id = False + if title_code: + # TODO set lang + title = self.env["res.partner.title"].search( + [("stay_code", "=", title_code)], limit=1 + ) + if title: + title_id = title.id + partner_name = f"{title.shortcut or title.name} {partner_name}" + else: + avail_title_read = self.env["res.partner.title"].search_read( + [("stay_code", "!=", False)], ["stay_code"] + ) + avail_title_list = [x["stay_code"] for x in avail_title_read] + error_msg = ( + f"Wrong title: {title_code}. " + f"Possible values: {', '.join(avail_title_list)}." + ) + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + email = cobject.email + if not email: # Should never happen because defined as required + logger.error("Missing email in stay controller. Quitting.") + # country + country_id = False + phone = cobject.phone + mobile = cobject.mobile + if cobject.country_code: + country_code = cobject.country_code.upper() + country = self.env["res.country"].search( + [("code", "=", country_code)], limit=1 + ) + if country: + country_id = country.id + if phone: + phone = self.env["phone.validation.mixin"].phone_format( + phone, country=country + ) + logger.info( + "Phone number reformatted from %s to %s (country %s)", + cobject.phone, + phone, + country.name, + ) + if mobile: + mobile = self.env["phone.validation.mixin"].phone_format( + mobile, country=country + ) + logger.info( + "Mobile number reformatted from %s to %s (country %s)", + cobject.mobile, + mobile, + country.name, + ) + else: + logger.warning("Country code %s doesn't exist in Odoo.", country_code) + notes_list.append( + _("Country code %s doesn't exist in Odoo.") % country_code + ) + + vals = { + "partner_name": partner_name, + "arrival_time": arrival_time, + "arrival_note": cobject.arrival_note, + "departure_time": departure_time, + "departure_note": cobject.departure_note, + "controller_message": cobject.message, + "controller_firstname": firstname, + "controller_lastname": lastname, + "controller_email": email, + "controller_phone": phone, + "controller_mobile": mobile, + "controller_title_id": title_id, + "controller_street": cobject.street, + "controller_street2": cobject.street2, + "controller_zip": cobject.zip, + "controller_city": cobject.city, + "controller_country_id": country_id, + "controller_notes": "\n".join(notes_list), + } + if try_match_partner: + vals["partner_id"] = self._controller_try_match_partner(vals) + return vals diff --git a/donation_api/models/fastapi_endpoint.py b/donation_api/models/fastapi_endpoint.py new file mode 100644 index 000000000..1be45fd0a --- /dev/null +++ b/donation_api/models/fastapi_endpoint.py @@ -0,0 +1,35 @@ +# 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 fastapi import FastAPI + +from odoo import fields, models + +from odoo.addons.fastapi.dependencies import ( + authenticated_partner_from_basic_auth_user, + authenticated_partner_impl, +) + +from ..routers import donation_api_router + + +class FastapiEndpoint(models.Model): + _inherit = "fastapi.endpoint" + + app: str = fields.Selection( + selection_add=[("donation", "Donation")], ondelete={"donation": "cascade"} + ) + + def _get_fastapi_routers(self): + if self.app == "donation": + return [donation_api_router] + return super()._get_fastapi_routers() + + def _get_app(self) -> FastAPI: + app = super()._get_app() + if self.app == "donation": + app.dependency_overrides[ + authenticated_partner_impl + ] = authenticated_partner_from_basic_auth_user + return app diff --git a/donation_api/readme/CONTRIBUTORS.md b/donation_api/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..b61afe5d0 --- /dev/null +++ b/donation_api/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Alexis de Lattre \<\> diff --git a/donation_api/readme/DESCRIPTION.md b/donation_api/readme/DESCRIPTION.md new file mode 100644 index 000000000..4fad738e9 --- /dev/null +++ b/donation_api/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module provides a REST API to create new stays. Useful if you have a web form on your website and you want it to create a new draft stay upon validation. diff --git a/donation_api/readme/INSTALL.md b/donation_api/readme/INSTALL.md new file mode 100644 index 000000000..b3aa9c2ab --- /dev/null +++ b/donation_api/readme/INSTALL.md @@ -0,0 +1 @@ +This module depends on the OCA module **fastapi** from [rest-framework](https://github.com/OCA/rest-framework). diff --git a/donation_api/routers/__init__.py b/donation_api/routers/__init__.py new file mode 100644 index 000000000..89e532e33 --- /dev/null +++ b/donation_api/routers/__init__.py @@ -0,0 +1 @@ +from .donation import donation_api_router diff --git a/donation_api/routers/donation.py b/donation_api/routers/donation.py new file mode 100644 index 000000000..c1e8e1ccd --- /dev/null +++ b/donation_api/routers/donation.py @@ -0,0 +1,144 @@ +# 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 +import sys +from datetime import date, datetime, timedelta + +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status + +from odoo import _, api, tools + +from odoo.addons.base.models.res_partner import Partner +from odoo.addons.fastapi.dependencies import ( + authenticated_partner, + authenticated_partner_env, +) + +from ..schemas import DonationCreate, DonationCreated + +logger = logging.getLogger(__name__) + +donation_api_router = APIRouter() + + +@donation_api_router.post("/new", response_model=DonationCreated, status_code=201) +def donation_new( + env: Annotated[api.Environment, Depends(authenticated_partner_env)], + partner: Annotated[Partner, Depends(authenticated_partner)], + donationcreate: DonationCreate, +) -> DonationCreated: + logger.info("Donation controller /new called with staycreate=%s", donationcreate) + env["donation.donation"] + company_id = donationcreate.company_id + if not company_id: + company_str = ( + env["ir.config_parameter"] + .sudo() + .get_param("donation.controller.company_id", False) + ) + if company_str: + try: + company_id = int(company_str) + except Exception as e: + logger.warning( + "Failed to convert ir.config_parameter " + "stay.controller.company_id %s to int: %s", + company_str, + e, + ) + if not company_id: + company_id = env.ref("base.main_company").id + # protection for DoS attacks + limit_create_date = datetime.now() - timedelta(1) + recent_draft_stay = sso.search_count( + [ + ("company_id", "=", company_id), + ("create_date", ">=", limit_create_date), + ("state", "=", "draft"), + ("controller_mode", "=", "created"), + ] + ) + recent_draft_stay_limit_str = ( + env["ir.config_parameter"] + .sudo() + .get_param("stay.controller.max_requests_24h", 100) + ) + recent_draft_stay_limit = int(recent_draft_stay_limit_str) + logger.debug("recent_draft_stay=%d", recent_draft_stay) + if recent_draft_stay > recent_draft_stay_limit and not tools.config.get( + "test_enable" + ): + logger.error( + "stay controller: %d draft stays created during the last 24h. " + "Suspecting DoS attack. Request ignored.", + recent_draft_stay, + ) + raise HTTPException(status_code=status.HTTP_429_TOO_MANY_REQUESTS) + + vals = sso._controller_prepare_create_update(staycreate) + if not vals: + return False + + arrival_date = staycreate.arrival_date + departure_date = staycreate.departure_date + if arrival_date < date.today(): + error_msg = f"Arrival date {arrival_date} cannot be in the past" + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + if departure_date < arrival_date: + error_msg = ( + f"Departure date {departure_date} cannot be before " + f"arrival date {arrival_date}" + ) + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + guest_qty = staycreate.guest_qty + if guest_qty < 1: + error_msg = f"Guest quantity ({guest_qty}) must be strictly positive." + logger.error(error_msg) + raise HTTPException( + status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=error_msg + ) + + vals.update( + { + "controller_mode": "created", + "company_id": company_id, + "group_id": staycreate.group_id or False, + "guest_qty": guest_qty, + "arrival_date": arrival_date, + "departure_date": departure_date, + } + ) + logger.debug("Creating new stay with vals=%s", vals) + stay = sso.create(vals) + logger.info("Create stay %s ID %d from controller", stay.display_name, stay.id) + try: + env.ref("stay_api.stay_controller_notify").sudo().with_context( + action_description=_("created") + ).send_mail(stay.id) + logger.info("Mail sent for stay creation notification") + except Exception as e: + logger.error("Failed to generate stay creation email: %s", e) + answer_dict = { + "name": stay.name, + "id": stay.id, + "company_id": vals["company_id"], + "partner_id": vals["partner_id"], + "phone": vals["controller_phone"], + "mobile": vals["controller_mobile"], + "uuid": stay.controller_uuid, + } + logger.info("Stay controller /new answer: %s", answer_dict) + return StayCreated(**answer_dict) diff --git a/donation_api/schemas/__init__.py b/donation_api/schemas/__init__.py new file mode 100644 index 000000000..fab1f80bd --- /dev/null +++ b/donation_api/schemas/__init__.py @@ -0,0 +1,2 @@ +from .donation_create import DonationCreate +from .donation_created import DonationCreated diff --git a/donation_api/schemas/donation_create.py b/donation_api/schemas/donation_create.py new file mode 100644 index 000000000..a0741640c --- /dev/null +++ b/donation_api/schemas/donation_create.py @@ -0,0 +1,23 @@ +# 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 pydantic import BaseModel + + +class DonationCreate(BaseModel): + lastname: str + firstname: str = None + title: str = None + email: str + phone: str = None + mobile: str = None + company_id: int = None + message: str = None + notes_list: list = None + country_code: str = None + street: str = None + street2: str = None + zip: str = None + city: str = None diff --git a/donation_api/schemas/donation_created.py b/donation_api/schemas/donation_created.py new file mode 100644 index 000000000..1e679ade8 --- /dev/null +++ b/donation_api/schemas/donation_created.py @@ -0,0 +1,14 @@ +# 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 pydantic import BaseModel + + +class DonationCreated(BaseModel): + number: str + id: int + company_id: int + phone: str = None + mobile: str = None + partner_id: int = None diff --git a/donation_api/static/description/icon.png b/donation_api/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +Donation + + + +
+

Donation

+ + +

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

+

This module provides a REST API to create new stays. Useful if you have +a web form on your website and you want it to create a new draft stay +upon validation.

+

Table of contents

+ +
+

Installation

+

This module depends on the OCA module fastapi from +rest-framework.

+
+
+

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/donation_api/views/donation_donation.xml b/donation_api/views/donation_donation.xml new file mode 100644 index 000000000..3b5350dbe --- /dev/null +++ b/donation_api/views/donation_donation.xml @@ -0,0 +1,78 @@ + + + + + + donation.donation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + donation.donation + + + + + + + + + + +['|', '|', ('number', 'ilike', self), ('partner_id', 'ilike', self), ('partner_name', 'ilike', self)] + + + + + diff --git a/donation_api/wizards/__init__.py b/donation_api/wizards/__init__.py new file mode 100644 index 000000000..0deb68c46 --- /dev/null +++ b/donation_api/wizards/__init__.py @@ -0,0 +1 @@ +from . import res_config_settings diff --git a/donation_api/wizards/res_config_settings.py b/donation_api/wizards/res_config_settings.py new file mode 100644 index 000000000..5520309d2 --- /dev/null +++ b/donation_api/wizards/res_config_settings.py @@ -0,0 +1,15 @@ +# Copyright 2025 Akretion France (https://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + donation_controller_company_id = fields.Many2one( + "res.company", + config_parameter="donation.controller.company_id", + string="Default Company for Donations created from Web Form", + ) diff --git a/donation_api/wizards/res_config_settings_view.xml b/donation_api/wizards/res_config_settings_view.xml new file mode 100644 index 000000000..d0484b2b2 --- /dev/null +++ b/donation_api/wizards/res_config_settings_view.xml @@ -0,0 +1,34 @@ + + + + + + res.config.settings + + +
+
+
+
+
+ Default Company for Donations created from Web Form +
+ +
+
+
+
+
+ + + + diff --git a/partner_match_or_create/README.rst b/partner_match_or_create/README.rst new file mode 100644 index 000000000..4f1116c08 --- /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/14.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-14-0/donation-14-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=14.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 000000000..379593d08 --- /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 000000000..a51ff5679 --- /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": "14.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 000000000..89e75c202 --- /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 000000000..9873bb275 --- /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 000000000..253d2fca8 --- /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 000000000..b57b88264 --- /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 000000000..87da9e7ef --- /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 000000000..b61afe5d0 --- /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 000000000..2fc4c0267 --- /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 000000000..df2694d1c --- /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 0000000000000000000000000000000000000000..1dcc49c24f364e9adf0afbc6fc0bac6dbecdeb11 GIT binary patch literal 10254 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 + + + + +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 000000000..eb9fde3c4 --- /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 000000000..c0cbeac03 --- /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 000000000..f007e44c1 --- /dev/null +++ b/partner_match_or_create/wizards/partner_match_or_create.py @@ -0,0 +1,311 @@ +# 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, + ) + 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", + ) + update_phone = fields.Boolean( + compute="_compute_update_bool", readonly=False, store=True + ) + update_mobile = fields.Boolean( + compute="_compute_update_bool", readonly=False, store=True + ) + update_address = fields.Boolean( + compute="_compute_update_bool", readonly=False, store=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 000000000..8302bfea0 --- /dev/null +++ b/partner_match_or_create/wizards/partner_match_or_create.xml @@ -0,0 +1,143 @@ + + + + + + + partner.match.or.create + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Create or Update Contact + partner.match.or.create + form + new + + + +
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..5bd269480 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +# generated from manifests external_dependencies +fastapi +pydantic<2 diff --git a/setup/donation_api/odoo/addons/donation_api b/setup/donation_api/odoo/addons/donation_api new file mode 120000 index 000000000..cb02fe448 --- /dev/null +++ b/setup/donation_api/odoo/addons/donation_api @@ -0,0 +1 @@ +../../../../donation_api \ No newline at end of file diff --git a/setup/donation_api/setup.py b/setup/donation_api/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/donation_api/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) 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 000000000..43acf7f74 --- /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 000000000..28c57bb64 --- /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, +)