fsm_visit_confirmation: translate template, move away from ratings

This commit is contained in:
Marc Durepos 2025-02-26 20:58:14 -05:00
parent 95a95298bb
commit 81a4a904c7
6 changed files with 703 additions and 66 deletions

View file

@ -3,10 +3,9 @@
import logging
import werkzeug
from odoo import http
from odoo import http, _
from odoo.http import request
from odoo.tools.translate import _
from odoo.tools.misc import get_lang
from odoo.addons.base.models.ir_qweb import keep_query
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.exceptions import AccessError, MissingError
@ -20,65 +19,101 @@ class CustomerPortalExtended(CustomerPortal):
def portal_my_task(self, task_id, access_token=None, **kw):
"""Override to allow access with token for public users."""
try:
task_sudo = self._document_check_access(
"project.task", task_id, access_token
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
)
except (AccessError, MissingError):
return request.redirect("/my")
# Pass the task to the original method
return super(CustomerPortalExtended, self).portal_my_task(
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/<string:token>/<string:action>",
"/my/fsm_confirmation/<string:action>",
type="http",
auth="public",
website=True,
)
def fsm_confirmation_action(self, token, action, **kwargs):
def fsm_confirmation_action(self, action, **kwargs):
"""Custom landing page for FSM visit confirmations.
Args:
token: The access token for the task
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"))
# Get the task using the token
task_sudo = self._get_task_by_token(token)
if not task_sudo:
return self._render_error_page(_("Task not found or invalid token"))
# If it's an approval, update the task state directly
if action == "approve":
try:
# Update task state to approved
task_sudo.sudo().write({"state": "03_approved"})
task.write({"state": "03_approved"})
# Add a message to the chatter
task_sudo.sudo().message_post(
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
return request.redirect(
"/my/task/%s?visit_confirmation_status=approved&access_token=%s"
% (task_sudo.id, token)
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(str(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
return request.redirect(
"/my/task/%s?visit_confirmation_status=change&token=%s&access_token=%s"
% (task_sudo.id, token, token)
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",
@ -90,60 +125,146 @@ class CustomerPortalExtended(CustomerPortal):
)
def fsm_confirmation_submit_change(self, **kwargs):
"""Handle the submission of change request form."""
token = kwargs.get("token")
values = self._get_portal_values(**kwargs)
task = values.get("task", False)
lang = values.get("lang")
feedback = kwargs.get("feedback", "")
if not token or not feedback:
return request.redirect("/my")
# Get the task using the token
task_sudo = self._get_task_by_token(token)
if not task_sudo:
return self._render_error_page(_("Task not found or invalid token"))
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_sudo.sudo().write({"state": "02_changes_requested"})
task.write({"state": "02_changes_requested"})
# Add the feedback as a message to the chatter
task_sudo.sudo().message_post(
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
return request.redirect(
"/my/task/%s?visit_confirmation_status=change_submitted&access_token=%s"
% (task_sudo.id, token)
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(str(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 token
task = request.env["project.task"].sudo().search([("access_token", "=", token)], limit=1)
# 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)
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):
def _render_error_page(self, error_message, task=None, **kwargs):
"""Render an error page."""
return request.render(
"http_routing.http_error",
{
"status_code": "400",
"status_message": error_message,
},
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

View file

@ -10,7 +10,8 @@
<field name="body_html" type="html">
<div>
<div style="margin-bottom: 16px;">
Dear <t t-out="object.work_order_contacts and object.work_order_contacts[0].name or object.partner_id.name"/>,
Dear <t t-out="object.work_order_contacts and object.work_order_contacts[0].name or object.partner_id.name"/>
,
</div>
<div style="margin-bottom: 16px;">
We would like to confirm the details of your upcoming service visit:
@ -29,7 +30,8 @@
<t t-set="partner_tz" t-value="user.tz or object.company_id.partner_id.tz or 'America/Toronto'"/>
<span t-field="object.planned_date_begin" t-options="{'widget': 'datetime', 'timezone': partner_tz}"/>
<div style="font-size: 12px; color: #666;">
(Timezone: <t t-out="partner_tz"/>)
(Timezone: <t t-out="partner_tz"/>
)
</div>
</td>
</tr>
@ -42,7 +44,8 @@
<t t-out="object.partner_id.street2"/>
<br/>
</t>
<t t-out="object.partner_id.city"/>, <t t-out="object.partner_id.state_id.code"/>
<t t-out="object.partner_id.city"/>
, <t t-out="object.partner_id.state_id.code"/>
<t t-out="object.partner_id.zip"/>
</td>
</tr>
@ -65,12 +68,13 @@
<tr>
<td style="padding:10px;text-align:center;">
<t t-set="base_url" t-value="object.get_base_url()"/>
<a t-att-href="base_url + '/my/fsm_confirmation/' + object.access_token + '/approve'" style="background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
<t t-set="lang" t-value="object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang or 'en_US'"/>
<a t-att-href="base_url + '/my/fsm_confirmation/approve?' + keep_query(lang=lang,access_token=object.access_token)" style="background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
Approve Visit
</a>
</td>
<td style="padding:10px;text-align:center;">
<a t-att-href="base_url + '/my/fsm_confirmation/' + object.access_token + '/change'" style="background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
<a t-att-href="base_url + '/my/fsm_confirmation/change?' + keep_query(lang=lang,access_token=object.access_token)" style="background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
Request Changes
</a>
</td>
@ -78,7 +82,8 @@
</table>
<div style="margin-top: 16px;">
<p>Best regards,</p>
<p>The <t t-out="object.company_id.name"/> service management team</p>
<p>The <t t-out="object.company_id.name"/>
service management team</p>
</div>
</div>
</field>

View file

@ -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 <marc@bemade.org>\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 ""
"<div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" Dear <t t-out=\"object.work_order_contacts and object.work_order_contacts[0].name or object.partner_id.name\"></t>\n"
",\n"
" </div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" We would like to confirm the details of your upcoming service visit:\n"
" </div>\n"
" <table style=\"border-style:solid;box-sizing:border-box;border-left-color:#e7e7e7;border-bottom-color:#e7e7e7;border-right-color:#e7e7e7;border-top-color:#e7e7e7;border-left-width:1px;border-bottom-width:1px;border-right-width:1px;border-top-width:1px;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;border:1pxsolid#e7e7e7\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Task Summary</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.name\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Technician Arrival</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-set=\"partner\" t-value=\"object.work_order_contacts and object.work_order_contacts[0] or object.partner_id\"></t>\n"
" <t t-set=\"partner_tz\" t-value=\"user.tz or object.company_id.partner_id.tz or 'America/Toronto'\"></t>\n"
" <span t-field=\"object.planned_date_begin\" t-options=\"{'widget': 'datetime', 'timezone': partner_tz}\"></span>\n"
" <div style=\"font-size: 12px; color: #666;\">\n"
" (Timezone: <t t-out=\"partner_tz\"></t>\n"
")\n"
" </div>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Location</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.partner_id.street\"></t>\n"
" <br>\n"
" <t t-if=\"object.partner_id.street2\">\n"
" <t t-out=\"object.partner_id.street2\"></t>\n"
" <br>\n"
" </t>\n"
" <t t-out=\"object.partner_id.city\"></t>\n"
", <t t-out=\"object.partner_id.state_id.code\"></t>\n"
" <t t-out=\"object.partner_id.zip\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa\">Assigned Technician(s)</td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px\">\n"
" <t t-foreach=\"object.user_ids\" t-as=\"user\">\n"
" <t t-out=\"user.name\"></t>\n"
" <t t-if=\"not user_last\">\n"
" <br>\n"
" </t>\n"
" </t>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin: 16px 0px 16px 0px;\">\n"
" Please take a moment to confirm this visit by clicking one of the following options:\n"
" </div>\n"
" <table style=\"box-sizing:border-box;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <t t-set=\"base_url\" t-value=\"object.get_base_url()\"></t>\n"
" <t t-set=\"lang\" t-value=\"object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang or 'en_US'\"></t>\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/approve?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Approve Visit\n"
" </a>\n"
" </td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/change?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Request Changes\n"
" </a>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin-top: 16px;\">\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">Best regards,</p>\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">The <t t-out=\"object.company_id.name\"></t>\n"
" service management team</p>\n"
" </div>\n"
" </div>\n"
" "
msgstr ""
"<div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" Cher/chère<t t-out=\"object.work_order_contacts and object.work_order_contacts[0].name or object.partner_id.name\"></t>\n"
",\n"
" </div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" Nous aimerions confirmer les détails de votre visite de service à venir.\n"
" </div>\n"
" <table style=\"border-style:solid;box-sizing:border-box;border-left-color:#e7e7e7;border-bottom-color:#e7e7e7;border-right-color:#e7e7e7;border-top-color:#e7e7e7;border-left-width:1px;border-bottom-width:1px;border-right-width:1px;border-top-width:1px;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;border:1pxsolid#e7e7e7\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Visite</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.name\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Début</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-set=\"partner\" t-value=\"object.work_order_contacts and object.work_order_contacts[0] or object.partner_id\"></t>\n"
" <t t-set=\"partner_tz\" t-value=\"user.tz or object.company_id.partner_id.tz or 'America/Toronto'\"></t>\n"
" <span t-field=\"object.planned_date_begin\" t-options=\"{'widget': 'datetime', 'timezone': partner_tz}\"></span>\n"
" <div style=\"font-size: 12px; color: #666;\">\n"
" (Fuseau horaire: <t t-out=\"partner_tz\"></t>\n"
")\n"
" </div>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Lieu</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.partner_id.street\"></t>\n"
" <br>\n"
" <t t-if=\"object.partner_id.street2\">\n"
" <t t-out=\"object.partner_id.street2\"></t>\n"
" <br>\n"
" </t>\n"
" <t t-out=\"object.partner_id.city\"></t>\n"
", <t t-out=\"object.partner_id.state_id.code\"></t>\n"
" <t t-out=\"object.partner_id.zip\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa\">Technicien(s) assigné(s)</td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px\">\n"
" <t t-foreach=\"object.user_ids\" t-as=\"user\">\n"
" <t t-out=\"user.name\"></t>\n"
" <t t-if=\"not user_last\">\n"
" <br>\n"
" </t>\n"
" </t>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin: 16px 0px 16px 0px;\">\n"
" Veuillez prendre un moment pour confirmer cette visite en cliquant sur une des options suivantes :\n"
" </div>\n"
" <table style=\"box-sizing:border-box;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <t t-set=\"base_url\" t-value=\"object.get_base_url()\"></t>\n"
" <t t-set=\"lang\" t-value=\"object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang or 'en_US'\"></t>\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/approve?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Confirmer la visite\n"
" </a>\n"
" </td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/change?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Demander des changements\n"
" </a>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin-top: 16px;\">\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">Cordialement,</p>\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">L'équipe de gestion des services de <t t-out=\"object.company_id.name\"></t></p>\n"
" </div>\n"
" </div>\n"
" "
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid ""
"<i class=\"fa fa-check-circle me-2\"/>\n"
" <strong>Success!</strong> Thank you for approving this service visit."
msgstr ""
"<i class=\"fa fa-check-circle me-2\"/>\n"
" <strong>Succès!</strong> 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 ""
"<i class=\"fa fa-exclamation-triangle me-2\"/>\n"
" Request Changes to Service Visit"
msgstr ""
"<i class=\"fa fa-exclamation-triangle me-2\"/>\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 ""
"<i class=\"fa fa-info-circle me-2\"/>\n"
" <strong>Thank you!</strong> Your change request has been submitted. Our team will review it shortly."
msgstr ""
"<i class=\"fa fa-info-circle me-2\"/>\n"
" <strong>Merci!</strong> 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 :"

View file

@ -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 ""
"<div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" Dear <t t-out=\"object.work_order_contacts and object.work_order_contacts[0].name or object.partner_id.name\"></t>\n"
",\n"
" </div>\n"
" <div style=\"margin-bottom: 16px;\">\n"
" We would like to confirm the details of your upcoming service visit:\n"
" </div>\n"
" <table style=\"border-style:solid;box-sizing:border-box;border-left-color:#e7e7e7;border-bottom-color:#e7e7e7;border-right-color:#e7e7e7;border-top-color:#e7e7e7;border-left-width:1px;border-bottom-width:1px;border-right-width:1px;border-top-width:1px;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;border:1pxsolid#e7e7e7\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Task Summary</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.name\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Technician Arrival</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-set=\"partner\" t-value=\"object.work_order_contacts and object.work_order_contacts[0] or object.partner_id\"></t>\n"
" <t t-set=\"partner_tz\" t-value=\"user.tz or object.company_id.partner_id.tz or 'America/Toronto'\"></t>\n"
" <span t-field=\"object.planned_date_begin\" t-options=\"{'widget': 'datetime', 'timezone': partner_tz}\"></span>\n"
" <div style=\"font-size: 12px; color: #666;\">\n"
" (Timezone: <t t-out=\"partner_tz\"></t>\n"
")\n"
" </div>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa;border-bottom:1pxsolid#e7e7e7\">Location</td>\n"
" <td style=\"border-style:solid;box-sizing:border-box;border-bottom-color:#e7e7e7;border-left-width:0px;border-bottom-width:1px;border-right-width:0px;border-top-width:0px;padding:10px;border-bottom:1pxsolid#e7e7e7\">\n"
" <t t-out=\"object.partner_id.street\"></t>\n"
" <br>\n"
" <t t-if=\"object.partner_id.street2\">\n"
" <t t-out=\"object.partner_id.street2\"></t>\n"
" <br>\n"
" </t>\n"
" <t t-out=\"object.partner_id.city\"></t>\n"
", <t t-out=\"object.partner_id.state_id.code\"></t>\n"
" <t t-out=\"object.partner_id.zip\"></t>\n"
" </td>\n"
" </tr>\n"
" <tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;background-color:#f8f9fa\">Assigned Technician(s)</td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px\">\n"
" <t t-foreach=\"object.user_ids\" t-as=\"user\">\n"
" <t t-out=\"user.name\"></t>\n"
" <t t-if=\"not user_last\">\n"
" <br>\n"
" </t>\n"
" </t>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin: 16px 0px 16px 0px;\">\n"
" Please take a moment to confirm this visit by clicking one of the following options:\n"
" </div>\n"
" <table style=\"box-sizing:border-box;-webkit-border-vertical-spacing:0px;-webkit-border-horizontal-spacing:0px;border-collapse:collapse;caption-side:bottom;width:100%;border-spacing:0;\" width=\"100%\">\n"
" <tbody style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\"><tr style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px\">\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <t t-set=\"base_url\" t-value=\"object.get_base_url()\"></t>\n"
" <t t-set=\"lang\" t-value=\"object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang or 'en_US'\"></t>\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/approve?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Approve Visit\n"
" </a>\n"
" </td>\n"
" <td style=\"border-style:none;box-sizing:border-box;border-left-width:0px;border-bottom-width:0px;border-right-width:0px;border-top-width:0px;padding:10px;text-align:center\">\n"
" <a t-att-href=\"base_url + '/my/fsm_confirmation/change?access_token=' + object.access_token + '&amp;lang=' + lang\" style=\"box-sizing:border-box;background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;\">\n"
" Request Changes\n"
" </a>\n"
" </td>\n"
" </tr>\n"
" </tbody></table>\n"
" <div style=\"margin-top: 16px;\">\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">Best regards,</p>\n"
" <p style=\"margin:0px 0 16px 0;box-sizing:border-box;\">The <t t-out=\"object.company_id.name\"></t>\n"
" service management team</p>\n"
" </div>\n"
" </div>\n"
" "
msgstr ""
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid ""
"<i class=\"fa fa-check-circle me-2\"/>\n"
" <strong>Success!</strong> Thank you for approving this service visit."
msgstr ""
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid ""
"<i class=\"fa fa-exclamation-triangle me-2\"/>\n"
" Request Changes to Service Visit"
msgstr ""
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid ""
"<i class=\"fa fa-info-circle me-2\"/>\n"
" <strong>Thank you!</strong> Your change request has been submitted. Our team will review it shortly."
msgstr ""
#. module: fsm_visit_confirmation
#. odoo-python
#: code:addons/fsm_visit_confirmation/controllers/main.py:0
#, python-format
msgid "Changes requested by customer: %s"
msgstr ""
#. 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 ""
#. 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 ""
#. module: fsm_visit_confirmation
#: model:mail.template,name:fsm_visit_confirmation.fsm_visit_confirmation_email_template
msgid "FSM Visit Confirmation"
msgstr ""
#. module: fsm_visit_confirmation
#. odoo-python
#: code:addons/fsm_visit_confirmation/controllers/main.py:0
#, python-format
msgid "Invalid action"
msgstr ""
#. 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 ""
#. 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 ""
#. 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 ""
#. 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 ""
#. module: fsm_visit_confirmation
#: model:mail.template,subject:fsm_visit_confirmation.fsm_visit_confirmation_email_template
msgid "Service Visit Confirmation: {{ object.name }}"
msgstr ""
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid "Submit Request"
msgstr ""
#. 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 ""
#. module: fsm_visit_confirmation
#. odoo-python
#: code:addons/fsm_visit_confirmation/controllers/main.py:0
#, python-format
msgid "Visit approved by customer"
msgstr ""
#. module: fsm_visit_confirmation
#: model_terms:ir.ui.view,arch_db:fsm_visit_confirmation.portal_my_task
msgid "Your Comments:"
msgstr ""

View file

@ -120,7 +120,7 @@ class TestFSMVisitConfirmation(HttpCase):
"""Test the complete task approval flow"""
# Test the FSM confirmation approve endpoint
self.authenticate(None, None)
url = f"/my/fsm_confirmation/{self.token}/approve"
url = f"/my/fsm_confirmation/approve?access_token={self.token}"
response = self.url_open(url)
self.assertEqual(
response.status_code, 200, "FSM confirmation approve should succeed"
@ -146,7 +146,7 @@ class TestFSMVisitConfirmation(HttpCase):
"""Test the complete task request changes flow"""
# Test the FSM confirmation change endpoint
self.authenticate(None, None)
url = f"/my/fsm_confirmation/{self.token}/change"
url = f"/my/fsm_confirmation/change?access_token={self.token}"
response = self.url_open(url)
self.assertEqual(
response.status_code, 200, "FSM confirmation change should succeed"
@ -157,7 +157,7 @@ class TestFSMVisitConfirmation(HttpCase):
response = self.url_open(
url,
data={
"token": self.token,
"access_token": self.token,
"feedback": "Need some changes",
"csrf_token": http.Request.csrf_token(self),
},

View file

@ -27,7 +27,8 @@
<p>Please provide details about the changes you would like to make to this service visit:</p>
<form action="/my/fsm_confirmation/submit_change" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" name="token" t-att-value="request.params.get('token')"/>
<input type="hidden" name="access_token" t-att-value="request.params.get('access_token')"/>
<input type="hidden" name="lang" t-att-value="request.context.get('lang') or request.params.get('lang')"/>
<div class="form-group mb-3">
<label for="feedback" class="form-label">Your Comments:</label>