CHANGES
This commit is contained in:
1
fusion_projects/fusion_project_portal/__init__.py
Normal file
1
fusion_projects/fusion_project_portal/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
24
fusion_projects/fusion_project_portal/__manifest__.py
Normal file
24
fusion_projects/fusion_project_portal/__manifest__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
'name': 'Fusion Project Portal',
|
||||
'version': '19.0.3.0.0',
|
||||
'category': 'Project',
|
||||
'summary': 'Customer portal hierarchy + task creation, plus surfaces logged timesheets on the portal',
|
||||
'description': """
|
||||
- Hides sub-tasks from /my/projects/<id> so portal lists only top-level tasks.
|
||||
- Adds + New Task / + New Sub-task buttons on portal (inline forms).
|
||||
- Surfaces logged timesheets on the customer portal task page.
|
||||
- Light UX inherit on the standard hr_timesheet stop-timer wizard so the
|
||||
description prompt is mandatory and a bigger textarea is shown. The
|
||||
Start/Pause/Stop button itself is the built-in one provided by
|
||||
hr_timesheet/timesheet_grid -- we no longer ship a parallel timer.
|
||||
""",
|
||||
'depends': ['project', 'hr_timesheet', 'timesheet_grid'],
|
||||
'data': [
|
||||
'views/portal_templates.xml',
|
||||
'views/wizard_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'license': 'LGPL-3',
|
||||
'author': 'Fusion Central',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import portal
|
||||
244
fusion_projects/fusion_project_portal/controllers/portal.py
Normal file
244
fusion_projects/fusion_project_portal/controllers/portal.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from odoo import http, _
|
||||
from odoo.exceptions import AccessError, MissingError
|
||||
from odoo.http import request
|
||||
|
||||
from odoo.addons.project.controllers.portal import ProjectCustomerPortal
|
||||
|
||||
|
||||
class FusionProjectCustomerPortal(ProjectCustomerPortal):
|
||||
|
||||
def _task_get_searchbar_sortings(self, milestones_allowed, project=False):
|
||||
values = super()._task_get_searchbar_sortings(milestones_allowed, project)
|
||||
values['sequence, id'] = {
|
||||
'label': _('Manual'), 'order': 'sequence, id', 'sequence': 5,
|
||||
}
|
||||
return values
|
||||
|
||||
def _project_get_page_view_values(self, project, access_token, page=1, date_begin=None,
|
||||
date_end=None, sortby=None, search=None,
|
||||
search_in='content', groupby=None, **kwargs):
|
||||
values = super()._project_get_page_view_values(
|
||||
project, access_token, page, date_begin, date_end,
|
||||
sortby, search, search_in, groupby, **kwargs,
|
||||
)
|
||||
values['can_create_task'] = self._fp_can_create_task(project)
|
||||
|
||||
groups = values.get('grouped_tasks')
|
||||
depth_map = {}
|
||||
if isinstance(groups, list):
|
||||
new_groups = []
|
||||
for tasks in groups:
|
||||
if not tasks:
|
||||
new_groups.append(tasks)
|
||||
continue
|
||||
ordered_ids = self._fp_hierarchical_order(tasks, depth_map)
|
||||
new_groups.append(tasks.browse(ordered_ids))
|
||||
values['grouped_tasks'] = new_groups
|
||||
values['fp_task_depth'] = depth_map
|
||||
return values
|
||||
|
||||
def _fp_hierarchical_order(self, tasks, depth_map):
|
||||
task_ids = set(tasks.ids)
|
||||
children = {tid: [] for tid in task_ids}
|
||||
roots = []
|
||||
for t in tasks:
|
||||
if t.parent_id and t.parent_id.id in task_ids:
|
||||
children[t.parent_id.id].append(t.id)
|
||||
else:
|
||||
roots.append((t.sequence, t.id))
|
||||
roots.sort()
|
||||
|
||||
all_tasks_by_id = {t.id: t for t in tasks}
|
||||
for tid in children:
|
||||
children[tid].sort(key=lambda cid: (all_tasks_by_id[cid].sequence, cid))
|
||||
|
||||
order = []
|
||||
|
||||
def walk(tid, depth):
|
||||
order.append(tid)
|
||||
depth_map[tid] = depth
|
||||
for child in children.get(tid, []):
|
||||
walk(child, depth + 1)
|
||||
|
||||
for _seq, r in roots:
|
||||
walk(r, 0)
|
||||
return order
|
||||
|
||||
def _task_get_page_view_values(self, task, access_token, **kwargs):
|
||||
values = super()._task_get_page_view_values(task, access_token, **kwargs)
|
||||
descendants = []
|
||||
depth_map = {}
|
||||
Task = request.env['project.task'].sudo()
|
||||
|
||||
def walk(parent_id, depth):
|
||||
children = Task.search(
|
||||
[('parent_id', '=', parent_id)],
|
||||
order='sequence, id',
|
||||
)
|
||||
for c in children:
|
||||
descendants.append(c)
|
||||
depth_map[c.id] = depth
|
||||
walk(c.id, depth + 1)
|
||||
|
||||
walk(task.id, 0)
|
||||
values['fp_task_descendants'] = descendants
|
||||
values['fp_task_depth_inside'] = depth_map
|
||||
values['fp_can_create_task'] = self._fp_can_create_task(task.project_id)
|
||||
values['fp_priority_options'] = request.env['project.task']._fields['priority']._description_selection(request.env)
|
||||
timesheets = request.env['account.analytic.line'].sudo().search(
|
||||
[('task_id', '=', task.id)],
|
||||
order='date desc, id desc',
|
||||
)
|
||||
values['fp_timesheets'] = timesheets
|
||||
values['fp_timesheets_total_hours'] = sum(timesheets.mapped('unit_amount'))
|
||||
values['fp_state_options'] = task._fields['state']._description_selection(request.env)
|
||||
values['fp_state_label'] = dict(values['fp_state_options']).get(task.state, task.state)
|
||||
return values
|
||||
|
||||
def _fp_can_create_task(self, project):
|
||||
user = request.env.user
|
||||
if not project or not project.exists():
|
||||
return False
|
||||
if not user or user.id == request.env.ref('base.public_user').id:
|
||||
return False
|
||||
if user._is_internal():
|
||||
return True
|
||||
partner = user.partner_id
|
||||
if not partner:
|
||||
return False
|
||||
if project.partner_id and (
|
||||
partner == project.partner_id
|
||||
or partner.parent_id == project.partner_id
|
||||
or partner.commercial_partner_id == project.partner_id.commercial_partner_id
|
||||
):
|
||||
return True
|
||||
follower = request.env['mail.followers'].sudo().search_count([
|
||||
('res_model', '=', 'project.project'),
|
||||
('res_id', '=', project.id),
|
||||
('partner_id', '=', partner.id),
|
||||
], limit=1)
|
||||
return bool(follower)
|
||||
|
||||
@http.route(
|
||||
['/my/projects/<int:project_id>/task/<int:task_id>/state'],
|
||||
type='http', auth='user', website=True, methods=['POST'],
|
||||
csrf=True,
|
||||
)
|
||||
def fp_portal_task_set_state(self, project_id, task_id, **post):
|
||||
try:
|
||||
project_sudo = self._document_check_access('project.project', project_id)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
if not self._fp_can_create_task(project_sudo):
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{task_id}')
|
||||
|
||||
task = request.env['project.task'].sudo().search([
|
||||
('id', '=', task_id),
|
||||
('project_id', '=', project_id),
|
||||
], limit=1)
|
||||
if not task:
|
||||
return request.redirect(f'/my/projects/{project_id}')
|
||||
|
||||
new_state = post.get('state')
|
||||
allowed = ['01_in_progress', '02_changes_requested', '03_approved', '1_done']
|
||||
if new_state not in allowed:
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{task_id}')
|
||||
|
||||
comment = (post.get('comment') or '').strip()
|
||||
partner = request.env.user.partner_id
|
||||
labels = dict(task._fields['state']._description_selection(request.env))
|
||||
label = labels.get(new_state, new_state)
|
||||
|
||||
body = f'Status set to <b>{label}</b> by {partner.name} from the customer portal.'
|
||||
if comment:
|
||||
body += f'<br/><br/><b>Comment:</b><br/>{comment}'
|
||||
|
||||
task.write({'state': new_state})
|
||||
task.message_post(
|
||||
body=body,
|
||||
author_id=partner.id,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{task_id}')
|
||||
|
||||
@http.route(
|
||||
[
|
||||
'/my/projects/<int:project_id>/task/new',
|
||||
'/my/projects/<int:project_id>/task/<int:parent_task_id>/subtask/new',
|
||||
],
|
||||
type='http', auth='user', website=True, methods=['GET', 'POST'],
|
||||
csrf=True,
|
||||
)
|
||||
def fp_portal_task_new(self, project_id, parent_task_id=None, **post):
|
||||
try:
|
||||
project_sudo = self._document_check_access('project.project', project_id)
|
||||
except (AccessError, MissingError):
|
||||
return request.redirect('/my')
|
||||
|
||||
if not self._fp_can_create_task(project_sudo):
|
||||
return request.redirect(f'/my/projects/{project_id}')
|
||||
|
||||
parent_task = None
|
||||
if parent_task_id:
|
||||
parent_task = request.env['project.task'].sudo().search([
|
||||
('id', '=', parent_task_id),
|
||||
('project_id', '=', project_id),
|
||||
], limit=1)
|
||||
if not parent_task:
|
||||
return request.redirect(f'/my/projects/{project_id}')
|
||||
|
||||
priority_selection = request.env['project.task']._fields['priority']._description_selection(request.env)
|
||||
|
||||
render_values = {
|
||||
'project': project_sudo,
|
||||
'parent_task': parent_task,
|
||||
'priority_options': priority_selection,
|
||||
'errors': {},
|
||||
'values': {},
|
||||
'page_name': 'fp_new_task',
|
||||
}
|
||||
|
||||
if request.httprequest.method == 'POST':
|
||||
name = (post.get('name') or '').strip()
|
||||
description = (post.get('description') or '').strip()
|
||||
priority = post.get('priority') or '0'
|
||||
if priority not in dict(priority_selection):
|
||||
priority = '0'
|
||||
|
||||
errors = {}
|
||||
if not name:
|
||||
errors['name'] = 'Please enter a title.'
|
||||
|
||||
if errors:
|
||||
if parent_task:
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{parent_task.id}')
|
||||
render_values.update({'errors': errors, 'values': post})
|
||||
return request.render('fusion_project_portal.portal_new_task_form', render_values)
|
||||
|
||||
partner = request.env.user.partner_id
|
||||
vals = {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'priority': priority,
|
||||
'project_id': project_sudo.id,
|
||||
'partner_id': project_sudo.partner_id.id or partner.id,
|
||||
'message_partner_ids': [(4, partner.id)],
|
||||
}
|
||||
if parent_task:
|
||||
vals['parent_id'] = parent_task.id
|
||||
vals['display_in_project'] = False
|
||||
|
||||
task = request.env['project.task'].sudo().create(vals)
|
||||
task.sudo().message_post(
|
||||
body=f'Submitted from customer portal by {partner.name}.',
|
||||
author_id=partner.id,
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
if parent_task:
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{parent_task.id}')
|
||||
return request.redirect(f'/my/projects/{project_id}/task/{task.id}')
|
||||
|
||||
return request.render('fusion_project_portal.portal_new_task_form', render_values)
|
||||
335
fusion_projects/fusion_project_portal/views/portal_templates.xml
Normal file
335
fusion_projects/fusion_project_portal/views/portal_templates.xml
Normal file
@@ -0,0 +1,335 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Status actions: Request Changes / Approve / Mark Done from the portal task page -->
|
||||
<template id="portal_my_task_inherit_state_actions"
|
||||
inherit_id="project.portal_my_task"
|
||||
name="Fusion: Status actions on portal task page"
|
||||
priority="55">
|
||||
<xpath expr="//div[@id='task_chat']" position="before">
|
||||
<div t-if="fp_can_create_task" class="card mt-4 mb-4 fp_state_card">
|
||||
<div class="card-header bg-light d-flex align-items-center">
|
||||
<h5 class="mb-0"><i class="fa fa-flag me-2"/> Status</h5>
|
||||
<span class="ms-auto badge"
|
||||
t-attf-class="badge ms-auto #{
|
||||
'bg-warning' if task.state == '02_changes_requested'
|
||||
else 'bg-success' if task.state == '03_approved'
|
||||
else 'bg-primary' if task.state == '1_done'
|
||||
else 'bg-danger' if task.state == '1_canceled'
|
||||
else 'bg-secondary'
|
||||
}">
|
||||
<t t-esc="fp_state_label"/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST"
|
||||
t-attf-action="/my/projects/{{ task.project_id.id }}/task/{{ task.id }}/state"
|
||||
class="row g-2">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="col-12">
|
||||
<textarea name="comment" rows="3"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Optional — what changes do you need, or any approval notes?"/>
|
||||
</div>
|
||||
<div class="col-12 d-flex flex-wrap gap-2 mt-2">
|
||||
<button type="submit" name="state" value="02_changes_requested"
|
||||
class="btn btn-warning btn-sm">
|
||||
<i class="fa fa-exclamation-circle me-1"/> Request Changes
|
||||
</button>
|
||||
<button type="submit" name="state" value="03_approved"
|
||||
class="btn btn-success btn-sm">
|
||||
<i class="fa fa-check-circle me-1"/> Approve
|
||||
</button>
|
||||
<button type="submit" name="state" value="1_done"
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="fa fa-flag-checkered me-1"/> Mark Done
|
||||
</button>
|
||||
<button type="submit" name="state" value="01_in_progress"
|
||||
class="btn btn-secondary btn-sm ms-auto"
|
||||
t-if="task.state != '01_in_progress'">
|
||||
<i class="fa fa-play me-1"/> Reopen / In Progress
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Timesheets / Time logged panel on the parent task page -->
|
||||
<template id="portal_my_task_inherit_timesheets"
|
||||
inherit_id="project.portal_my_task"
|
||||
name="Fusion: Time logged on portal task page"
|
||||
priority="60">
|
||||
<xpath expr="//div[@id='task_chat']" position="before">
|
||||
<div t-if="fp_timesheets" class="card mt-4 mb-4 fp_timesheets_card">
|
||||
<div class="card-header bg-light d-flex align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-clock-o me-2"/> Time Logged
|
||||
<span class="badge bg-success ms-2">
|
||||
<t t-esc="'%.2f h' % (fp_timesheets_total_hours or 0)"/>
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li t-foreach="fp_timesheets" t-as="ts" class="list-group-item">
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<strong t-esc="ts.name"/>
|
||||
<div class="small text-muted">
|
||||
<span t-esc="ts.date"/>
|
||||
<t t-if="ts.employee_id">
|
||||
— <span t-esc="ts.employee_id.name"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-self-center ms-2">
|
||||
<span class="badge bg-primary">
|
||||
<t t-esc="'%.2f h' % ts.unit_amount"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- + New Task button on the project's portal page -->
|
||||
<template id="portal_my_project_inherit_new_task_btn"
|
||||
inherit_id="project.portal_my_project"
|
||||
name="Fusion: New Task button on portal project">
|
||||
<xpath expr="//t[@t-call='portal.portal_searchbar']" position="after">
|
||||
<div class="d-flex justify-content-end mb-3" t-if="can_create_task">
|
||||
<a class="btn btn-primary"
|
||||
t-attf-href="/my/projects/{{ project.id }}/task/new">
|
||||
<i class="fa fa-plus me-1"/> New Task
|
||||
</a>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Indent task names by depth in the project task list -->
|
||||
<template id="portal_tasks_list_inherit_hierarchy"
|
||||
inherit_id="project.portal_tasks_list"
|
||||
name="Fusion: Indent sub-tasks in portal task list">
|
||||
<xpath expr="//t[@t-foreach='tasks']/tr/td/a[@t-attf-href]/span" position="before">
|
||||
<t t-set="_fp_depth" t-value="(fp_task_depth or {}).get(task.id, 0)"/>
|
||||
<t t-if="_fp_depth">
|
||||
<span t-attf-style="display:inline-block;width:{{ _fp_depth * 1.4 }}rem;"/>
|
||||
<i class="fa fa-level-up fa-rotate-90 me-2 text-muted small"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Reusable inline sub-task form (Bootstrap collapse target) -->
|
||||
<template id="fp_inline_subtask_form" name="Fusion: inline sub-task form">
|
||||
<div class="card-body bg-light border-top border-bottom py-3">
|
||||
<form method="POST"
|
||||
t-attf-action="/my/projects/{{ project_id }}/task/{{ parent_id }}/subtask/new"
|
||||
class="row g-2">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="col-md-7">
|
||||
<input type="text" name="name" class="form-control form-control-sm"
|
||||
placeholder="Sub-task title" required="required" maxlength="200"/>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="priority" class="form-select form-select-sm">
|
||||
<t t-foreach="fp_priority_options or []" t-as="opt">
|
||||
<option t-att-value="opt[0]"
|
||||
t-att-selected="opt[0] == '0' or None">
|
||||
<t t-esc="opt[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2 d-flex">
|
||||
<button type="submit" class="btn btn-sm btn-primary w-100">
|
||||
<i class="fa fa-paper-plane me-1"/> Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<textarea name="description" rows="2"
|
||||
class="form-control form-control-sm"
|
||||
placeholder="Description (optional)"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Sub-tasks panel + inline "New Sub-task" forms on parent task page -->
|
||||
<template id="portal_my_task_inherit_subtasks"
|
||||
inherit_id="project.portal_my_task"
|
||||
name="Fusion: Sub-tasks list inside parent task page">
|
||||
<xpath expr="//div[@id='task_chat']" position="before">
|
||||
<div class="card mt-4 mb-4 fp_subtasks_card">
|
||||
<div class="card-header bg-light d-flex align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fa fa-sitemap me-2"/>
|
||||
Sub-tasks
|
||||
<span class="badge bg-secondary ms-2">
|
||||
<t t-esc="len(fp_task_descendants or [])"/>
|
||||
</span>
|
||||
</h5>
|
||||
<button t-if="fp_can_create_task"
|
||||
class="btn btn-sm btn-primary ms-auto"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
t-attf-data-bs-target="#fp_form_root_{{ task.id }}"
|
||||
aria-expanded="false">
|
||||
<i class="fa fa-plus me-1"/> New Sub-task
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div t-if="fp_can_create_task" class="collapse"
|
||||
t-attf-id="fp_form_root_{{ task.id }}">
|
||||
<t t-call="fusion_project_portal.fp_inline_subtask_form">
|
||||
<t t-set="project_id" t-value="task.project_id.id"/>
|
||||
<t t-set="parent_id" t-value="task.id"/>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<ul t-if="fp_task_descendants" class="list-group list-group-flush">
|
||||
<t t-foreach="fp_task_descendants" t-as="sub">
|
||||
<li class="list-group-item d-flex align-items-center">
|
||||
<t t-set="_d" t-value="(fp_task_depth_inside or {}).get(sub.id, 0)"/>
|
||||
<span t-attf-style="display:inline-block;width:{{ _d * 1.4 }}rem;"/>
|
||||
<i t-if="_d" class="fa fa-level-up fa-rotate-90 me-2 text-muted small"/>
|
||||
<a t-attf-href="/my/projects/{{ task.project_id.id }}/task/{{ sub.id }}"
|
||||
class="flex-grow-1">
|
||||
<span t-esc="sub.name"/>
|
||||
</a>
|
||||
<button t-if="fp_can_create_task"
|
||||
class="btn btn-sm btn-link text-muted py-0 px-1"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
t-attf-data-bs-target="#fp_form_sub_{{ sub.id }}"
|
||||
aria-expanded="false"
|
||||
title="Add sub-task here">
|
||||
<i class="fa fa-plus"/>
|
||||
</button>
|
||||
<span t-if="sub.stage_id" class="badge bg-light text-dark border ms-2">
|
||||
<t t-esc="sub.stage_id.name"/>
|
||||
</span>
|
||||
</li>
|
||||
<li t-if="fp_can_create_task"
|
||||
class="list-group-item p-0 collapse"
|
||||
t-attf-id="fp_form_sub_{{ sub.id }}">
|
||||
<t t-call="fusion_project_portal.fp_inline_subtask_form">
|
||||
<t t-set="project_id" t-value="task.project_id.id"/>
|
||||
<t t-set="parent_id" t-value="sub.id"/>
|
||||
</t>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<div t-if="not fp_task_descendants" class="card-body text-muted small">
|
||||
No sub-tasks yet.
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Breadcrumbs for the standalone new-task form (top-level tasks only) -->
|
||||
<template id="portal_breadcrumbs_inherit_fp_new_task"
|
||||
inherit_id="portal.portal_breadcrumbs"
|
||||
name="Fusion: Breadcrumbs for new-task form"
|
||||
priority="50">
|
||||
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
|
||||
<li t-if="page_name == 'fp_new_task' and project" class="breadcrumb-item">
|
||||
<a t-attf-href="/my/projects">Projects</a>
|
||||
</li>
|
||||
<li t-if="page_name == 'fp_new_task' and project" class="breadcrumb-item">
|
||||
<a t-attf-href="/my/projects/{{ project.id }}">
|
||||
<t t-esc="project.name"/>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="page_name == 'fp_new_task' and parent_task" class="breadcrumb-item text-truncate">
|
||||
<a t-attf-href="/my/projects/{{ project.id }}/task/{{ parent_task.id }}">
|
||||
<t t-esc="parent_task.name"/>
|
||||
</a>
|
||||
</li>
|
||||
<li t-if="page_name == 'fp_new_task'" class="breadcrumb-item active">
|
||||
<t t-if="parent_task">New Sub-task</t>
|
||||
<t t-else="">New Task</t>
|
||||
</li>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- Standalone New Task form (still used for top-level "+ New Task" on project page) -->
|
||||
<template id="portal_new_task_form" name="Submit a new task">
|
||||
<t t-call="portal.portal_layout">
|
||||
<t t-set="title" t-value="('New Sub-task in ' + parent_task.name) if parent_task else ('New Task in ' + project.name)"/>
|
||||
|
||||
<div class="container my-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h3 class="mb-0">
|
||||
<t t-if="parent_task">
|
||||
New Sub-task in
|
||||
<span t-esc="parent_task.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
New Task in
|
||||
<span t-field="project.name"/>
|
||||
</t>
|
||||
</h3>
|
||||
<a t-attf-href="/my/projects/{{ project.id }}{{ '/task/' + str(parent_task.id) if parent_task else '' }}"
|
||||
class="btn btn-link ms-auto">
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="POST"
|
||||
t-attf-action="/my/projects/{{ project.id }}{{ '/task/' + str(parent_task.id) + '/subtask' if parent_task else '/task' }}/new"
|
||||
class="o_portal_form needs-validation">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fp_task_name" class="form-label">
|
||||
Title <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" id="fp_task_name" name="name"
|
||||
t-att-class="'form-control is-invalid' if errors.get('name') else 'form-control'"
|
||||
t-att-value="values.get('name')"
|
||||
required="required" maxlength="200"/>
|
||||
<div t-if="errors.get('name')" class="invalid-feedback d-block">
|
||||
<t t-esc="errors.get('name')"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fp_task_priority" class="form-label">Priority</label>
|
||||
<select id="fp_task_priority" name="priority" class="form-select">
|
||||
<t t-foreach="priority_options" t-as="opt">
|
||||
<option t-att-value="opt[0]"
|
||||
t-att-selected="opt[0] == values.get('priority', '0') or None">
|
||||
<t t-esc="opt[1]"/>
|
||||
</option>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="fp_task_description" class="form-label">Description</label>
|
||||
<textarea id="fp_task_description" name="description"
|
||||
class="form-control" rows="6"
|
||||
t-esc="values.get('description', '')"/>
|
||||
<div class="form-text">
|
||||
Add details, context, or steps to reproduce — your team will see this on the task.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fa fa-paper-plane me-1"/>
|
||||
<t t-if="parent_task">Submit Sub-task</t>
|
||||
<t t-else="">Submit Task</t>
|
||||
</button>
|
||||
<a t-attf-href="/my/projects/{{ project.id }}{{ '/task/' + str(parent_task.id) if parent_task else '' }}"
|
||||
class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
22
fusion_projects/fusion_project_portal/views/wizard_views.xml
Normal file
22
fusion_projects/fusion_project_portal/views/wizard_views.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- Light UX polish on the standard stop-timer confirmation wizard:
|
||||
- require a non-empty session summary
|
||||
- show a bigger textarea with a more directive placeholder
|
||||
- keep the built-in time-edit + Save/Resume/Delete buttons unchanged -->
|
||||
<record id="view_hr_timesheet_stop_timer_confirmation_wizard_inherit_fc"
|
||||
model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.stop.timer.confirmation.wizard.form.fc</field>
|
||||
<field name="model">hr.timesheet.stop.timer.confirmation.wizard</field>
|
||||
<field name="inherit_id"
|
||||
ref="timesheet_grid.hr_timesheet_stop_timer_confirmation_wizard_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="timesheet_name" position="attributes">
|
||||
<attribute name="placeholder">What did you complete in this session? — e.g. Implemented quote PDF rendering and validated edge cases</attribute>
|
||||
<attribute name="required">1</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user