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.",
|
||||
"website": "http://www.bemade.org",
|
||||
"license": "LGPL-3",
|
||||
"depends": ["industry_fsm", "bemade_fsm", "rating"],
|
||||
"data": ["data/mail_templates.xml"],
|
||||
"depends": ["industry_fsm", "bemade_fsm", "rating", "http_routing"],
|
||||
"data": [
|
||||
"data/mail_templates.xml",
|
||||
"views/project_portal_project_task_templates.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"auto_install": 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>
|
||||
<record id="rating_project_request_email_template" model="mail.template">
|
||||
<field name="name">Field Service Task: Rating Request</field>
|
||||
<field name="model_id" ref="project.model_project_task"/>
|
||||
<field name="subject">Please confirm your upcoming service visit with {{ object.company_id.name }}</field>
|
||||
<record id="fsm_visit_confirmation_email_template" model="mail.template">
|
||||
<field name="name">FSM Visit Confirmation</field>
|
||||
<field name="model_id" ref="industry_fsm.model_project_task"/>
|
||||
<field name="subject">Service Visit Confirmation: {{ object.name }}</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="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">
|
||||
<div>
|
||||
<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 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>
|
||||
<table style="width:100%;border-spacing:0;border:1px solid #e7e7e7;">
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
<td style="padding:10px;background-color:#f8f9fa;border-bottom:1px solid #e7e7e7;">Location</td>
|
||||
<td style="padding:10px;border-bottom:1px solid #e7e7e7;">
|
||||
{{ object.partner_id.street }}<br/>
|
||||
{% if object.partner_id.street2 %}
|
||||
{{ object.partner_id.street2 }}<br/>
|
||||
{% endif %}
|
||||
{{ object.partner_id.city }}, {{ object.partner_id.state_id.code }} {{ object.partner_id.zip }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;background-color:#f8f9fa;">Assigned Technician(s)</td>
|
||||
<td style="padding:10px;">
|
||||
{% for user in object.user_ids %}
|
||||
{{ user.name }}{% if not loop.last %}<br/>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</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:
|
||||
<t t-out="object.partner_id.street"/>
|
||||
<br/>
|
||||
<t t-if="object.partner_id.street2">
|
||||
<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.zip"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:10px;background-color:#f8f9fa;">Assigned Technician(s)</td>
|
||||
<td style="padding:10px;">
|
||||
<t t-foreach="object.user_ids" t-as="user">
|
||||
<t t-out="user.name"/>
|
||||
<t t-if="not user_last">
|
||||
<br/>
|
||||
</t>
|
||||
</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>
|
||||
<table style="width:100%;border-spacing:0;">
|
||||
<tr>
|
||||
<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) }}/5" style="background-color:#28a745;padding:8px 16px;text-decoration:none;color:#fff;border-radius:5px;font-size:13px;">
|
||||
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>
|
||||
</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>
|
||||
|
|
@ -4,7 +4,6 @@ import logging
|
|||
from datetime import datetime, timedelta
|
||||
from odoo import http
|
||||
from odoo.tests import HttpCase, tagged
|
||||
from odoo.addons.rating.models import rating_data
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -35,7 +34,6 @@ class TestFSMVisitConfirmation(HttpCase):
|
|||
0,
|
||||
[
|
||||
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
|
||||
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(
|
||||
{
|
||||
"name": "Customer",
|
||||
"email": "customer@example.com",
|
||||
"lang": "en_US", # Set language explicitly
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -56,8 +56,6 @@ class TestFSMVisitConfirmation(HttpCase):
|
|||
{
|
||||
"name": "Test FSM Project",
|
||||
"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,
|
||||
}
|
||||
)
|
||||
|
|
@ -76,10 +74,22 @@ class TestFSMVisitConfirmation(HttpCase):
|
|||
"name": "Need Confirmation",
|
||||
"sequence": 1,
|
||||
"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)],
|
||||
"planned_date_begin": datetime.now() + timedelta(days=1),
|
||||
"stage_id": self.stage_new.id,
|
||||
"state": "01_in_progress", # Using valid state value
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
self.task.stage_id, self.stage_new, "Task should start in New stage"
|
||||
)
|
||||
|
||||
def test_01_task_rating_flow(self):
|
||||
"""Test the complete task rating flow"""
|
||||
# Move task to confirmation stage
|
||||
_logger.info("Project rating_active: %s", self.project.rating_active)
|
||||
_logger.info(
|
||||
"Stage rating_template_id: %s",
|
||||
self.stage_needs_confirmation.rating_template_id,
|
||||
)
|
||||
# Ensure the task has an access token
|
||||
if not self.task.access_token:
|
||||
self.task._portal_ensure_token()
|
||||
self.token = self.task.access_token
|
||||
self.assertTrue(self.token, "Task should have an access token")
|
||||
|
||||
# Verify project rating settings
|
||||
self.assertTrue(self.project.rating_active, "Project rating should be active")
|
||||
def test_01_task_approve_flow(self):
|
||||
"""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.project.rating_status,
|
||||
"stage",
|
||||
"Project rating status should be 'stage'",
|
||||
)
|
||||
self.assertTrue(
|
||||
self.stage_needs_confirmation.rating_template_id,
|
||||
"Stage should have a rating template",
|
||||
response.status_code, 200, "FSM confirmation approve should succeed"
|
||||
)
|
||||
|
||||
# Move task to confirmation stage
|
||||
self.task.write({"stage_id": self.stage_needs_confirmation.id})
|
||||
# Now check that the task state was updated
|
||||
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
|
||||
# import time
|
||||
# time.sleep(1)
|
||||
|
||||
# Get the token directly from the task, passing the customer partner
|
||||
rating = self.env["rating.rating"].search(
|
||||
# Check that a message was posted
|
||||
messages = self.env["mail.message"].search(
|
||||
[
|
||||
("model", "=", "project.task"),
|
||||
("res_id", "=", self.task.id),
|
||||
("res_model", "=", "project.task"),
|
||||
("body", "ilike", "Visit approved by customer"),
|
||||
]
|
||||
)
|
||||
self.assertTrue(rating, "Rating should be created")
|
||||
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)
|
||||
self.assertTrue(messages, "A message should be posted on the task")
|
||||
|
||||
# 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)
|
||||
|
||||
# First open the rating page
|
||||
url = f"/rate/{rating.access_token}/5"
|
||||
url = f"/my/fsm_confirmation/{self.token}/change"
|
||||
response = self.url_open(url)
|
||||
self.assertEqual(
|
||||
response.status_code, 200, "Opening rating page should succeed"
|
||||
response.status_code, 200, "FSM confirmation change should succeed"
|
||||
)
|
||||
|
||||
# Then submit the feedback
|
||||
url = f"/rate/{rating.access_token}/submit_feedback"
|
||||
# Now submit the change request form
|
||||
url = f"/my/fsm_confirmation/submit_change"
|
||||
response = self.url_open(
|
||||
url,
|
||||
data={
|
||||
"rate": "5",
|
||||
"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",
|
||||
"token": self.token,
|
||||
"feedback": "Need some changes",
|
||||
"csrf_token": http.Request.csrf_token(self),
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
response.status_code, 200, "Feedback submission should succeed"
|
||||
response.status_code, 200, "Change request submission should succeed"
|
||||
)
|
||||
|
||||
# Refresh the rating record
|
||||
rating.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",
|
||||
)
|
||||
# Check that the task state was updated
|
||||
self.task.invalidate_recordset()
|
||||
self.assertEqual(
|
||||
self.task.state,
|
||||
"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,10 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<template id="portal_my_task" inherit_id="project.portal_my_task">
|
||||
<xpath expr="//div[@id='task_content']" position="before">
|
||||
<div t-if="visit_confirmation_accepted" class="alert alert-success" role="alert">
|
||||
<strong>Success!</strong> We've received your confirmation. Thank you!
|
||||
<!-- Add success message when visit is approved -->
|
||||
<xpath expr="//div[@id='task_content']" position="inside">
|
||||
<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>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
|
|
|||
Loading…
Reference in a new issue