diff --git a/fsm_visit_confirmation/__init__.py b/fsm_visit_confirmation/__init__.py new file mode 100644 index 0000000..91c5580 --- /dev/null +++ b/fsm_visit_confirmation/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/fsm_visit_confirmation/__manifest__.py b/fsm_visit_confirmation/__manifest__.py new file mode 100644 index 0000000..5e4b76c --- /dev/null +++ b/fsm_visit_confirmation/__manifest__.py @@ -0,0 +1,54 @@ +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the GNU Lesser General Public License, +# version 3. +# +# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + "name": "FSM Visit Confirmation", + "version": "18.0.0.1.0", + "summary": "Enable client feedback workflow for field service tasks", + "description": """ + This module enhances the field service management workflow by leveraging Odoo's + built-in rating system for client task confirmations. Key features include: + + * Automated rating requests to work order contacts when tasks reach specific stages + * Configurable stages with rating email templates + * Client-facing rating interface with direct links in emails + * Support for task approval or change requests through ratings + * Integration with existing work order contacts from bemade_fsm + * Automatic task state updates based on client feedback + * Real-time feedback through website messages and notifications + * Chatter integration for client comments + + The module helps streamline communication between field service teams and clients + by providing a clear feedback workflow and maintaining a record of all client + interactions and approvals through Odoo's rating system. + """, + "category": "Services/Field Service", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["industry_fsm", "bemade_fsm", "rating", "http_routing"], + "data": [ + "data/mail_templates.xml", + "views/project_portal_project_task_templates.xml", + "views/project_task_type_views.xml", + ], + "installable": True, + "auto_install": False, + "application": False, +} diff --git a/fsm_visit_confirmation/controllers/__init__.py b/fsm_visit_confirmation/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/fsm_visit_confirmation/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/fsm_visit_confirmation/controllers/main.py b/fsm_visit_confirmation/controllers/main.py new file mode 100644 index 0000000..9794b24 --- /dev/null +++ b/fsm_visit_confirmation/controllers/main.py @@ -0,0 +1,270 @@ +# -*- coding: utf-8 -*- + +import logging +import werkzeug + +from odoo import http, _ +from odoo.http import request +from odoo.addons.base.models.ir_qweb import keep_query +from odoo.addons.portal.controllers.portal import CustomerPortal +from odoo.exceptions import AccessError, MissingError + +_logger = logging.getLogger(__name__) + + +class CustomerPortalExtended(CustomerPortal): + """Extend the customer portal controller to handle FSM visit confirmations.""" + + @http.route("/my/task/", type="http", auth="public", website=True) + def portal_my_task(self, task_id, access_token=None, **kw): + """Override to allow access with token for public users.""" + try: + task = request.env["project.task"].browse(task_id) + task.check_access_rights("read") + task.check_access_rights("write") + task.check_access_rule("read") + task.check_access_rule("write") + except AccessError: + task = None + values = self._get_portal_values(task, access_token=access_token, **kw) + if not task: + task = values.get("task", False) + if not task: + return self._render_error_page( + _("Task not found or invalid token"), task=None + ) + + # Pass the task to the original method + result = super(CustomerPortalExtended, self).portal_my_task( + task_id, access_token=access_token, **kw + ) + + # If the result is a Response object (like a redirect), return it directly + if isinstance(result, werkzeug.wrappers.Response): + return result + + # Otherwise, it's a rendered template, so we need to update the qcontext + if hasattr(result, "qcontext"): + result.qcontext.update(values) + + return result + + @http.route( + "/my/fsm_confirmation/", + type="http", + auth="public", + website=True, + ) + def fsm_confirmation_action(self, action, **kwargs): + """Custom landing page for FSM visit confirmations. + + Args: + action: Either 'approve' or 'change' + Kwargs: + access_token: The access token for the task + """ + values = self._get_portal_values(**kwargs) + task = values.get("task", False) + if not task: + return self._render_error_page( + _("Task not found or invalid token"), task=None + ) + # Store the language in kwargs for use in redirects + lang = values.get("lang") + + if action not in ("approve", "change"): + raise werkzeug.exceptions.BadRequest(_("Invalid action")) + + # If it's an approval, update the task state directly + if action == "approve": + try: + # Update task state to approved + task.write({"state": "03_approved"}) + + # Add a message to the chatter + task.message_post( + body=_("Visit approved by customer"), + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + # Always redirect to the task portal page with success message + redirect_url = "/my/task/%s?%s" % ( + task.id, + keep_query( + "access_token", + visit_confirmation_status="approved", + lang=lang, + ), + ) + return request.redirect(redirect_url) + except Exception as e: + _logger.exception(_("Error approving task: %s"), e) + return self._render_error_page( + _("Error approving task: %s") % str(e), task=task + ) + + # For change requests, redirect to the task portal page with change request form + redirect_url = "/my/task/%s?%s" % ( + task.id, + keep_query( + "access_token", + visit_confirmation_status="change", + lang=lang, + ), + ) + return request.redirect(redirect_url) + + @http.route( + "/my/fsm_confirmation/submit_change", + type="http", + auth="public", + methods=["post"], + website=True, + csrf=True, + ) + def fsm_confirmation_submit_change(self, **kwargs): + """Handle the submission of change request form.""" + values = self._get_portal_values(**kwargs) + + task = values.get("task", False) + lang = values.get("lang") + feedback = kwargs.get("feedback", "") + if not task: + return self._render_error_page( + _("Task not found or invalid token"), task=None + ) + if not feedback: + return self._render_error_page( + _("No feedback provided. Please try again!"), task=task + ) + + try: + # Update task state to changes requested + task.write({"state": "02_changes_requested"}) + + # Add the feedback as a message to the chatter + task.message_post( + body=_("Changes requested by customer: %s") % feedback, + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + + # Always redirect to the task portal page with success message + redirect_url = "/my/task/%s?%s" % ( + task.id, + keep_query( + access_token=kwargs.get("access_token"), + visit_confirmation_status="change_submitted", + lang=lang, + ), + ) + return request.redirect(redirect_url) + except Exception as e: + _logger.exception(_("Error submitting change request: %s"), e) + return self._render_error_page( + _("Error submitting change request: %s") % str(e), task=task + ) + + def _get_task_by_token(self, token): + """Get a task by its access token.""" + # First try to find the task directly by access token + task = ( + request.env["project.task"] + .sudo() + .search([("access_token", "=", token)], limit=1) + ) + if task: + return task + + # If not found, try to find it through the rating token + # This is for backward compatibility with existing tokens in emails + rating = ( + request.env["rating.rating"] + .sudo() + .search([("access_token", "=", token)], limit=1) + ) + if rating and rating.res_model == "project.task": + task = request.env["project.task"].sudo().browse(rating.res_id) + if task.exists(): + return task + + return None + + def _render_error_page(self, error_message, task=None, **kwargs): + """Render an error page.""" + values = { + "status_code": "400", + "status_message": error_message, + "lang": self._get_lang(task=task, lang=kwargs.get("lang")), + } + + return request.render("http_routing.http_error", values) + + def _get_portal_values(self, task=None, **kwargs): + """Get common values for portal templates. + + Args: + task: The task record if already loaded + **kwargs: Keyword arguments from the request + + Returns: + Dictionary with values for template rendering + """ + values = {"page_name": "task"} + if not task: + access_token = kwargs.get("access_token") + if access_token: + task = self._get_task_by_token(access_token) + if task: + values["task"] = task + + # Set language and update values dictionary + values["lang"] = self._get_lang(task=task, lang=kwargs.get("lang")) + + # Add any additional parameters from kwargs that might be needed in templates + for key in ["visit_confirmation_status", "error", "warning", "success"]: + if kwargs.get(key): + values[key] = kwargs.get(key) + + return values + + def _get_lang(self, task=None, lang=None): + """Helper method to set the language in the request environment. + + Args: + task: The task record (optional) + lang: Language code (e.g., 'en_US', 'fr_FR') (optional) + + Returns: + The language code that was set, or None if no language was set + """ + # If lang is not provided, try to get it from the task + if not lang and task: + lang_partner = ( + task.work_order_contacts + and task.work_order_contacts[0] + or task.partner_id + ) + if lang_partner and lang_partner.lang: + lang = lang_partner.lang + + if not lang: + return None + + # Get the language record + lang_code = lang.replace("_", "-") + lang_id = ( + request.env["res.lang"] + .sudo() + .search(["|", ("code", "=", lang_code), ("code", "=", lang)], limit=1) + ) + + if lang_id: + # Set the context language + request.env = request.env( + context=dict(request.env.context, lang=lang_id.code) + ) + return lang_id.code + + return None diff --git a/fsm_visit_confirmation/data/mail_templates.xml b/fsm_visit_confirmation/data/mail_templates.xml new file mode 100644 index 0000000..7cb22dc --- /dev/null +++ b/fsm_visit_confirmation/data/mail_templates.xml @@ -0,0 +1,93 @@ + + + + FSM Visit Confirmation + + Service Visit Confirmation: {{ object.name }} + {{ user.email_formatted }} + {{ object.work_order_contacts and object.work_order_contacts[0].id or object.partner_id.id }} + Send a visit confirmation email to the customer + +
+
+ Dear +, +
+
+ We would like to confirm the details of your upcoming service visit: +
+ + + + + + + + + + + + + + + + + +
Task Summary + +
Technician Arrival + + + +
+ (Timezone: +) +
+
Location + +
+ + +
+
+ +, + +
Assigned Technician(s) + + + +
+
+
+
+
+ Please take a moment to confirm this visit by clicking one of the following options: +
+ + + + + +
+ + + + Approve Visit + + + + Request Changes + +
+
+

Best regards,

+

The + service management team

+
+
+
+ {{ object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang }} + +
+
\ No newline at end of file diff --git a/fsm_visit_confirmation/i18n/fr.po b/fsm_visit_confirmation/i18n/fr.po new file mode 100644 index 0000000..90a2ee3 --- /dev/null +++ b/fsm_visit_confirmation/i18n/fr.po @@ -0,0 +1,299 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fsm_visit_confirmation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-02-27 00:31+0000\n" +"PO-Revision-Date: 2025-02-27 00:31+0000\n" +"Last-Translator: Marc Durepos \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: fsm_visit_confirmation +#: model:mail.template,body_html:fsm_visit_confirmation.fsm_visit_confirmation_email_template +msgid "" +"
\n" +"
\n" +" Dear \n" +",\n" +"
\n" +"
\n" +" We would like to confirm the details of your upcoming service visit:\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
Task Summary\n" +" \n" +"
Technician Arrival\n" +" \n" +" \n" +" \n" +"
\n" +" (Timezone: \n" +")\n" +"
\n" +"
Location\n" +" \n" +"
\n" +" \n" +" \n" +"
\n" +"
\n" +" \n" +", \n" +" \n" +"
Assigned Technician(s)\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +" Please take a moment to confirm this visit by clicking one of the following options:\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
\n" +" \n" +" \n" +" \n" +" \n" +"
\n" +" (Fuseau horaire: \n" +")\n" +"
\n" +"
Lieu\n" +" \n" +"
\n" +" \n" +" \n" +"
\n" +"
\n" +" \n" +", \n" +" \n" +"
Technicien(s) assigné(s)\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +" Veuillez prendre un moment pour confirmer cette visite en cliquant sur une des options suivantes :\n" +"
\n" +" \n" +" \n" +"
\n" +" \n" +" \n" +"

\n" +" \n" +" \n" +" " + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "" +"\n" +" Success! Thank you for approving this service visit." +msgstr "" +"\n" +" Succès! Merci d'avoir approuvé cette visite de service." + + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "" +"\n" +" Request Changes to Service Visit" +msgstr "" +"\n" +" Demander des changements à la visite de service" + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "" +"\n" +" Thank you! Your change request has been submitted. Our team will review it shortly." +msgstr "" +"\n" +" Merci! Votre demande de changement a été soumise. Notre équipe examinerait cette demande rapidement." + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Changes requested by customer: %s" +msgstr "Changements demandés par client: %s" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Error approving task: %s" +msgstr "Erreur lors de l'approbation de la tâche: %s" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Error submitting change request: %s" +msgstr "Erreur lors de la soumission de la demande de changement: %s" + +#. module: fsm_visit_confirmation +#: model:mail.template,name:fsm_visit_confirmation.fsm_visit_confirmation_email_template +msgid "FSM Visit Confirmation" +msgstr "Confirmation de visite de service" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Invalid action" +msgstr "Action invalide" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "No feedback provided. Please try again!" +msgstr "Aucun commentaire fourni. Veuillez réessayer!" + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "Please explain what changes you need..." +msgstr "Veuillez expliquer les modifications nécessaires..." + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "" +"Please provide details about the changes you would like to make to this " +"service visit:" +msgstr "" +"Veuillez fournir des informations sur les modifications que vous souhaitez " +"apporter à cette visite de service :" + +#. module: fsm_visit_confirmation +#: model:mail.template,description:fsm_visit_confirmation.fsm_visit_confirmation_email_template +msgid "Send a visit confirmation email to the customer" +msgstr "Envoyer un email de confirmation de visite au client" + +#. module: fsm_visit_confirmation +#: model:mail.template,subject:fsm_visit_confirmation.fsm_visit_confirmation_email_template +msgid "Service Visit Confirmation: {{ object.name }}" +msgstr "Confirmation de visite de service: {{ object.name }}" + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "Submit Request" +msgstr "Soumettre la demande" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Task not found or invalid token" +msgstr "Tâche introuvable ou jeton invalide" + +#. module: fsm_visit_confirmation +#. odoo-python +#: code:addons/fsm_visit_confirmation/controllers/main.py:0 +#, python-format +msgid "Visit approved by customer" +msgstr "Visite approuvée par le client" + +#. module: fsm_visit_confirmation +#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task +msgid "Your Comments:" +msgstr "Vos commentaires :" diff --git a/fsm_visit_confirmation/i18n/fsm_visit_confirmation.pot b/fsm_visit_confirmation/i18n/fsm_visit_confirmation.pot new file mode 100644 index 0000000..0b790cc --- /dev/null +++ b/fsm_visit_confirmation/i18n/fsm_visit_confirmation.pot @@ -0,0 +1,211 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * fsm_visit_confirmation +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-02-27 00:31+0000\n" +"PO-Revision-Date: 2025-02-27 00:31+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: fsm_visit_confirmation +#: model:mail.template,body_html:fsm_visit_confirmation.fsm_visit_confirmation_email_template +msgid "" +"
\n" +"
\n" +" Dear \n" +",\n" +"
\n" +"
\n" +" We would like to confirm the details of your upcoming service visit:\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
Task Summary\n" +" \n" +"
Technician Arrival\n" +" \n" +" \n" +" \n" +"
\n" +" (Timezone: \n" +")\n" +"
\n" +"
Location\n" +" \n" +"
\n" +" \n" +" \n" +"
\n" +"
\n" +" \n" +", \n" +" \n" +"
Assigned Technician(s)\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +" Please take a moment to confirm this visit by clicking one of the following options:\n" +"
\n" +" \n" +" \n" +"
\n" +" \n" +" \n" +" + + + diff --git a/fsm_visit_confirmation/views/project_task_type_views.xml b/fsm_visit_confirmation/views/project_task_type_views.xml new file mode 100644 index 0000000..86ba4fe --- /dev/null +++ b/fsm_visit_confirmation/views/project_task_type_views.xml @@ -0,0 +1,13 @@ + + + + project.task.type.form.inherit.fsm.visit.confirmation + project.task.type + + + + + + + +