fsm_visit_confirmation: new module good to go
This commit is contained in:
parent
f9f32aab34
commit
95a95298bb
7 changed files with 328 additions and 168 deletions
|
|
@ -0,0 +1 @@
|
||||||
|
from . import controllers
|
||||||
|
|
@ -42,8 +42,11 @@
|
||||||
"author": "Bemade Inc.",
|
"author": "Bemade Inc.",
|
||||||
"website": "http://www.bemade.org",
|
"website": "http://www.bemade.org",
|
||||||
"license": "LGPL-3",
|
"license": "LGPL-3",
|
||||||
"depends": ["industry_fsm", "bemade_fsm", "rating"],
|
"depends": ["industry_fsm", "bemade_fsm", "rating", "http_routing"],
|
||||||
"data": ["data/mail_templates.xml"],
|
"data": [
|
||||||
|
"data/mail_templates.xml",
|
||||||
|
"views/project_portal_project_task_templates.xml",
|
||||||
|
],
|
||||||
"installable": True,
|
"installable": True,
|
||||||
"auto_install": False,
|
"auto_install": False,
|
||||||
"application": False,
|
"application": False,
|
||||||
|
|
|
||||||
1
fsm_visit_confirmation/controllers/__init__.py
Normal file
1
fsm_visit_confirmation/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
149
fsm_visit_confirmation/controllers/main.py
Normal file
149
fsm_visit_confirmation/controllers/main.py
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
from odoo.tools.translate import _
|
||||||
|
from odoo.tools.misc import get_lang
|
||||||
|
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/<int:task_id>", 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_sudo = self._document_check_access(
|
||||||
|
"project.task", task_id, access_token
|
||||||
|
)
|
||||||
|
except (AccessError, MissingError):
|
||||||
|
return request.redirect("/my")
|
||||||
|
|
||||||
|
# Pass the task to the original method
|
||||||
|
return super(CustomerPortalExtended, self).portal_my_task(
|
||||||
|
task_id, access_token=access_token, **kw
|
||||||
|
)
|
||||||
|
|
||||||
|
@http.route(
|
||||||
|
"/my/fsm_confirmation/<string:token>/<string:action>",
|
||||||
|
type="http",
|
||||||
|
auth="public",
|
||||||
|
website=True,
|
||||||
|
)
|
||||||
|
def fsm_confirmation_action(self, token, action, **kwargs):
|
||||||
|
"""Custom landing page for FSM visit confirmations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
token: The access token for the task
|
||||||
|
action: Either 'approve' or 'change'
|
||||||
|
"""
|
||||||
|
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"})
|
||||||
|
|
||||||
|
# Add a message to the chatter
|
||||||
|
task_sudo.sudo().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)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error approving task: %s", e)
|
||||||
|
return self._render_error_page(str(e))
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
@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."""
|
||||||
|
token = kwargs.get("token")
|
||||||
|
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"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Update task state to changes requested
|
||||||
|
task_sudo.sudo().write({"state": "02_changes_requested"})
|
||||||
|
|
||||||
|
# Add the feedback as a message to the chatter
|
||||||
|
task_sudo.sudo().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)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
_logger.exception("Error submitting change request: %s", e)
|
||||||
|
return self._render_error_page(str(e))
|
||||||
|
|
||||||
|
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)
|
||||||
|
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):
|
||||||
|
"""Render an error page."""
|
||||||
|
return request.render(
|
||||||
|
"http_routing.http_error",
|
||||||
|
{
|
||||||
|
"status_code": "400",
|
||||||
|
"status_message": error_message,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -1,73 +1,88 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="rating_project_request_email_template" model="mail.template">
|
<record id="fsm_visit_confirmation_email_template" model="mail.template">
|
||||||
<field name="name">Field Service Task: Rating Request</field>
|
<field name="name">FSM Visit Confirmation</field>
|
||||||
<field name="model_id" ref="project.model_project_task"/>
|
<field name="model_id" ref="industry_fsm.model_project_task"/>
|
||||||
<field name="subject">Please confirm your upcoming service visit with {{ object.company_id.name }}</field>
|
<field name="subject">Service Visit Confirmation: {{ object.name }}</field>
|
||||||
<field name="email_from">{{ user.email_formatted }}</field>
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
<field name="partner_to">{{ object.work_order_contacts and object.work_order_contacts[0].id or object.partner_id.id }}</field>
|
<field name="partner_to">{{ object.work_order_contacts and object.work_order_contacts[0].id or object.partner_id.id }}</field>
|
||||||
<field name="description">Sent to request a client rating/confirmation for a field service task</field>
|
<field name="description">Send a visit confirmation email to the customer</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<div>
|
<div>
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="margin-bottom: 16px;">
|
||||||
Dear {{ 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>
|
||||||
<div style="margin-bottom: 16px;">
|
<div style="margin-bottom: 16px;">
|
||||||
We would like to confirm the details of your upcoming service visit:
|
We would like to confirm the details of your upcoming service visit:
|
||||||
</div>
|
</div>
|
||||||
<table style="width:100%;border-spacing:0;border:1px solid #e7e7e7;">
|
<table style="width:100%;border-spacing:0;border:1px solid #e7e7e7;">
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Task Summary</td>
|
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Task Summary</td>
|
||||||
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">{{ object.name }}</td>
|
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">
|
||||||
|
<t t-out="object.name"/>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Technician Arrival</td>
|
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Technician Arrival</td>
|
||||||
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">{{ object.planned_date_begin }}</td>
|
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">
|
||||||
|
<t t-set="partner" t-value="object.work_order_contacts and object.work_order_contacts[0] or object.partner_id"/>
|
||||||
|
<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"/>)
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Location</td>
|
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Location</td>
|
||||||
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">
|
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">
|
||||||
{{ object.partner_id.street }}<br/>
|
<t t-out="object.partner_id.street"/>
|
||||||
{% if object.partner_id.street2 %}
|
<br/>
|
||||||
{{ object.partner_id.street2 }}<br/>
|
<t t-if="object.partner_id.street2">
|
||||||
{% endif %}
|
<t t-out="object.partner_id.street2"/>
|
||||||
{{ object.partner_id.city }}, {{ object.partner_id.state_id.code }} {{ object.partner_id.zip }}
|
<br/>
|
||||||
</td>
|
</t>
|
||||||
</tr>
|
<t t-out="object.partner_id.city"/>, <t t-out="object.partner_id.state_id.code"/>
|
||||||
<tr>
|
<t t-out="object.partner_id.zip"/>
|
||||||
<td style="padding:10px;background-color:#f8f9fa;">Assigned Technician(s)</td>
|
</td>
|
||||||
<td style="padding:10px;">
|
</tr>
|
||||||
{% for user in object.user_ids %}
|
<tr>
|
||||||
{{ user.name }}{% if not loop.last %}<br/>
|
<td style="padding:10px;background-color:#f8f9fa;">Assigned Technician(s)</td>
|
||||||
{% endif %}
|
<td style="padding:10px;">
|
||||||
{% endfor %}
|
<t t-foreach="object.user_ids" t-as="user">
|
||||||
</td>
|
<t t-out="user.name"/>
|
||||||
</tr>
|
<t t-if="not user_last">
|
||||||
</table>
|
<br/>
|
||||||
<div style="margin: 16px 0px 16px 0px;">
|
</t>
|
||||||
Please take a moment to confirm this visit by clicking one of the following options:
|
</t>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="margin: 16px 0px 16px 0px;">
|
||||||
|
Please take a moment to confirm this visit by clicking one of the following options:
|
||||||
|
</div>
|
||||||
|
<table style="width:100%;border-spacing:0;">
|
||||||
|
<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;">
|
||||||
|
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;">
|
||||||
|
Request Changes
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<p>Best regards,</p>
|
||||||
|
<p>The <t t-out="object.company_id.name"/> service management team</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table style="width:100%;border-spacing:0;">
|
</field>
|
||||||
<tr>
|
<field name="lang">{{ object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang }}</field>
|
||||||
<td style="padding:10px;text-align:center;">
|
<field name="auto_delete" eval="True"/>
|
||||||
<a t-attf-href="/rate/{{ object._rating_get_access_token(partner=object.work_order_contacts and object.work_order_contacts[0] or object.partner_id) }}/5" style="background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
|
</record>
|
||||||
Confirm Visit
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td style="padding:10px;text-align:center;">
|
|
||||||
<a t-attf-href="/rate/{{ object._rating_get_access_token(partner=object.work_order_contacts and object.work_order_contacts[0] or object.partner_id) }}/1" style="background-color:#dc3545;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
|
|
||||||
Request Changes
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<div style="margin-top: 16px;">
|
|
||||||
<p>Best regards,</p>
|
|
||||||
<p>The {{ object.company_id.name }} service management team</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</field>
|
|
||||||
<field name="lang">{{ object.work_order_contacts and object.work_order_contacts[0].lang or object.partner_id.lang }}</field>
|
|
||||||
<field name="auto_delete" eval="True"/>
|
|
||||||
</record>
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -4,7 +4,6 @@ import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.tests import HttpCase, tagged
|
from odoo.tests import HttpCase, tagged
|
||||||
from odoo.addons.rating.models import rating_data
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -35,7 +34,6 @@ class TestFSMVisitConfirmation(HttpCase):
|
||||||
0,
|
0,
|
||||||
[
|
[
|
||||||
self.env.ref("industry_fsm.group_fsm_user").id,
|
self.env.ref("industry_fsm.group_fsm_user").id,
|
||||||
self.env.ref("project.group_project_rating").id,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
@ -43,11 +41,13 @@ class TestFSMVisitConfirmation(HttpCase):
|
||||||
)
|
)
|
||||||
# Set email on user's partner
|
# Set email on user's partner
|
||||||
self.fsm_user.partner_id.email = "fsm@example.com"
|
self.fsm_user.partner_id.email = "fsm@example.com"
|
||||||
|
self.fsm_user.partner_id.lang = "en_US" # Set language explicitly
|
||||||
|
|
||||||
self.customer = self.env["res.partner"].create(
|
self.customer = self.env["res.partner"].create(
|
||||||
{
|
{
|
||||||
"name": "Customer",
|
"name": "Customer",
|
||||||
"email": "customer@example.com",
|
"email": "customer@example.com",
|
||||||
|
"lang": "en_US", # Set language explicitly
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -56,8 +56,6 @@ class TestFSMVisitConfirmation(HttpCase):
|
||||||
{
|
{
|
||||||
"name": "Test FSM Project",
|
"name": "Test FSM Project",
|
||||||
"is_fsm": True,
|
"is_fsm": True,
|
||||||
"rating_active": True,
|
|
||||||
"rating_status": "stage", # Rating requests will be sent when tasks reach stages with rating templates
|
|
||||||
"company_id": self.env.user.company_id.id,
|
"company_id": self.env.user.company_id.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -76,10 +74,22 @@ class TestFSMVisitConfirmation(HttpCase):
|
||||||
"name": "Need Confirmation",
|
"name": "Need Confirmation",
|
||||||
"sequence": 1,
|
"sequence": 1,
|
||||||
"project_ids": [(4, self.project.id)],
|
"project_ids": [(4, self.project.id)],
|
||||||
"rating_template_id": self.env.ref(
|
}
|
||||||
"fsm_visit_confirmation.rating_project_request_email_template"
|
)
|
||||||
).id,
|
|
||||||
"auto_validation_state": True,
|
self.stage_approved = self.env["project.task.type"].create(
|
||||||
|
{
|
||||||
|
"name": "Approved",
|
||||||
|
"sequence": 2,
|
||||||
|
"project_ids": [(4, self.project.id)],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stage_changes_requested = self.env["project.task.type"].create(
|
||||||
|
{
|
||||||
|
"name": "Changes Requested",
|
||||||
|
"sequence": 3,
|
||||||
|
"project_ids": [(4, self.project.id)],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -93,140 +103,85 @@ class TestFSMVisitConfirmation(HttpCase):
|
||||||
"user_ids": [(4, self.fsm_user.id)],
|
"user_ids": [(4, self.fsm_user.id)],
|
||||||
"planned_date_begin": datetime.now() + timedelta(days=1),
|
"planned_date_begin": datetime.now() + timedelta(days=1),
|
||||||
"stage_id": self.stage_new.id,
|
"stage_id": self.stage_new.id,
|
||||||
|
"state": "01_in_progress", # Using valid state value
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.task.stage_id, self.stage_new, "Task should start in New stage"
|
self.task.stage_id, self.stage_new, "Task should start in New stage"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_01_task_rating_flow(self):
|
# Ensure the task has an access token
|
||||||
"""Test the complete task rating flow"""
|
if not self.task.access_token:
|
||||||
# Move task to confirmation stage
|
self.task._portal_ensure_token()
|
||||||
_logger.info("Project rating_active: %s", self.project.rating_active)
|
self.token = self.task.access_token
|
||||||
_logger.info(
|
self.assertTrue(self.token, "Task should have an access token")
|
||||||
"Stage rating_template_id: %s",
|
|
||||||
self.stage_needs_confirmation.rating_template_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Verify project rating settings
|
def test_01_task_approve_flow(self):
|
||||||
self.assertTrue(self.project.rating_active, "Project rating should be active")
|
"""Test the complete task approval flow"""
|
||||||
|
# Test the FSM confirmation approve endpoint
|
||||||
|
self.authenticate(None, None)
|
||||||
|
url = f"/my/fsm_confirmation/{self.token}/approve"
|
||||||
|
response = self.url_open(url)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.project.rating_status,
|
response.status_code, 200, "FSM confirmation approve should succeed"
|
||||||
"stage",
|
|
||||||
"Project rating status should be 'stage'",
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
self.stage_needs_confirmation.rating_template_id,
|
|
||||||
"Stage should have a rating template",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Move task to confirmation stage
|
# Now check that the task state was updated
|
||||||
self.task.write({"stage_id": self.stage_needs_confirmation.id})
|
self.task.invalidate_recordset()
|
||||||
|
self.assertEqual(
|
||||||
|
self.task.state, "03_approved", "Task state should be updated to approved"
|
||||||
|
)
|
||||||
|
|
||||||
# # Force a small delay to allow email processing
|
# Check that a message was posted
|
||||||
# import time
|
messages = self.env["mail.message"].search(
|
||||||
# time.sleep(1)
|
|
||||||
|
|
||||||
# Get the token directly from the task, passing the customer partner
|
|
||||||
rating = self.env["rating.rating"].search(
|
|
||||||
[
|
[
|
||||||
|
("model", "=", "project.task"),
|
||||||
("res_id", "=", self.task.id),
|
("res_id", "=", self.task.id),
|
||||||
("res_model", "=", "project.task"),
|
("body", "ilike", "Visit approved by customer"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.assertTrue(rating, "Rating should be created")
|
self.assertTrue(messages, "A message should be posted on the task")
|
||||||
self.assertEqual(
|
|
||||||
rating.partner_id, self.customer, "Rating should be for the customer"
|
|
||||||
)
|
|
||||||
token = rating.access_token
|
|
||||||
self.assertTrue(token, "Rating token should exist")
|
|
||||||
_logger.info("Rating token: %s", token)
|
|
||||||
|
|
||||||
# Simulate customer confirming the visit (rating = 5)
|
def test_02_task_request_changes_flow(self):
|
||||||
|
"""Test the complete task request changes flow"""
|
||||||
|
# Test the FSM confirmation change endpoint
|
||||||
self.authenticate(None, None)
|
self.authenticate(None, None)
|
||||||
|
url = f"/my/fsm_confirmation/{self.token}/change"
|
||||||
# First open the rating page
|
|
||||||
url = f"/rate/{rating.access_token}/5"
|
|
||||||
response = self.url_open(url)
|
response = self.url_open(url)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.status_code, 200, "Opening rating page should succeed"
|
response.status_code, 200, "FSM confirmation change should succeed"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Then submit the feedback
|
# Now submit the change request form
|
||||||
url = f"/rate/{rating.access_token}/submit_feedback"
|
url = f"/my/fsm_confirmation/submit_change"
|
||||||
response = self.url_open(
|
response = self.url_open(
|
||||||
url,
|
url,
|
||||||
data={
|
data={
|
||||||
"rate": "5",
|
"token": self.token,
|
||||||
"feedback": "Great service!",
|
|
||||||
"csrf_token": http.Request.csrf_token(self),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.assertEqual(response.status_code, 200, "Rating submission should succeed")
|
|
||||||
|
|
||||||
# Now check that the rating was created and consumed
|
|
||||||
rating.invalidate_recordset()
|
|
||||||
self.assertTrue(rating.consumed, "Rating should be consumed")
|
|
||||||
self.assertEqual(
|
|
||||||
rating.partner_id,
|
|
||||||
self.customer,
|
|
||||||
"Rating should be from work order contact",
|
|
||||||
)
|
|
||||||
self.assertEqual(rating.rating, 5, "Rating should be 5")
|
|
||||||
self.assertEqual(rating.feedback, "Great service!", "Feedback should be saved")
|
|
||||||
self.assertEqual(self.task.state, "03_approved", "Task should be approved")
|
|
||||||
|
|
||||||
def test_02_task_rating_flow_request_changes(self):
|
|
||||||
"""Test the complete task rating flow in the second test case."""
|
|
||||||
# Move task to confirmation stage
|
|
||||||
self.task.write({"stage_id": self.stage_needs_confirmation.id})
|
|
||||||
|
|
||||||
# Check that a rating request was created
|
|
||||||
rating = self.env["rating.rating"].search(
|
|
||||||
[("res_id", "=", self.task.id), ("res_model", "=", "project.task")]
|
|
||||||
)
|
|
||||||
self.assertTrue(rating, "Rating request should be created")
|
|
||||||
self.assertEqual(
|
|
||||||
rating.partner_id,
|
|
||||||
self.customer,
|
|
||||||
"Rating should be requested from work order contact",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Simulate customer refusing the visit (rating = 1)
|
|
||||||
self.authenticate(None, None)
|
|
||||||
|
|
||||||
# First open the rating page
|
|
||||||
url = f"/rate/{rating.access_token}/1"
|
|
||||||
response = self.url_open(url)
|
|
||||||
self.assertEqual(
|
|
||||||
response.status_code, 200, "Rating page should load successfully"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Then submit the feedback
|
|
||||||
url = f"/rate/{rating.access_token}/submit_feedback"
|
|
||||||
response = self.url_open(
|
|
||||||
url,
|
|
||||||
data={
|
|
||||||
"rate": "1",
|
|
||||||
"feedback": "Need some changes",
|
"feedback": "Need some changes",
|
||||||
"csrf_token": http.Request.csrf_token(self),
|
"csrf_token": http.Request.csrf_token(self),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
response.status_code, 200, "Feedback submission should succeed"
|
response.status_code, 200, "Change request submission should succeed"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Refresh the rating record
|
# Check that the task state was updated
|
||||||
rating.invalidate_recordset()
|
self.task.invalidate_recordset()
|
||||||
self.assertTrue(rating.consumed, "Rating should be consumed")
|
|
||||||
self.assertEqual(rating.rating, 1, "Rating should be updated to 1")
|
|
||||||
self.assertEqual(
|
|
||||||
rating.feedback,
|
|
||||||
"Need some changes",
|
|
||||||
"Feedback should be saved",
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.task.state,
|
self.task.state,
|
||||||
"02_changes_requested",
|
"02_changes_requested",
|
||||||
"Task should be in changes requested state",
|
"Task state should be updated to changes requested",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that a message was posted with the feedback
|
||||||
|
messages = self.env["mail.message"].search(
|
||||||
|
[
|
||||||
|
("model", "=", "project.task"),
|
||||||
|
("res_id", "=", self.task.id),
|
||||||
|
("body", "ilike", "Need some changes"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
messages, "A message with feedback should be posted on the task"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,45 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<template id="portal_my_task" inherit_id="project.portal_my_task">
|
<template id="portal_my_task" inherit_id="project.portal_my_task">
|
||||||
<xpath expr="//div[@id='task_content']" position="before">
|
<!-- Add success message when visit is approved -->
|
||||||
<div t-if="visit_confirmation_accepted" class="alert alert-success" role="alert">
|
<xpath expr="//div[@id='task_content']" position="inside">
|
||||||
<strong>Success!</strong> We've received your confirmation. Thank you!
|
<div t-if="request.params.get('visit_confirmation_status') == 'approved'" class="alert alert-success" role="alert">
|
||||||
|
<i class="fa fa-check-circle me-2"></i>
|
||||||
|
<strong>Success!</strong> Thank you for approving this service visit.
|
||||||
|
</div>
|
||||||
|
<div t-if="request.params.get('visit_confirmation_status') == 'change_submitted'" class="alert alert-info" role="alert">
|
||||||
|
<i class="fa fa-info-circle me-2"></i>
|
||||||
|
<strong>Thank you!</strong> Your change request has been submitted. Our team will review it shortly.
|
||||||
|
</div>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<!-- Add change request form when needed -->
|
||||||
|
<xpath expr="//div[@id='task_chat']" position="before">
|
||||||
|
<div t-if="request.params.get('visit_confirmation_status') == 'change'" class="mb-4 mt-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-warning text-white">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fa fa-exclamation-triangle me-2"></i>
|
||||||
|
Request Changes to Service Visit
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<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')"/>
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="feedback" class="form-label">Your Comments:</label>
|
||||||
|
<textarea class="form-control" name="feedback" id="feedback" rows="4" required="required" placeholder="Please explain what changes you need..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-warning">
|
||||||
|
Submit Request
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue