[FIX] project: review UX and UI in Project app
This commit reviews the UX and UI in Project app and improves the usability of the
new features recently added (subtasks list in kanban card, state field replacing
kanban_state field, etc).
The following changes are made in this commit:
- swap of user_id icon in the kanban box and the removal of the allow_unassign field and setting
- change the ordering of the fields in the subtask list
- make small layout changes in the project.task and project.project kanban cards
- remove the "lock-icon private" sub-title of private tasks, replace it with a
little lock icon on the bottom right icons of the kanban card.
- remove the break tag in the project.task kanban card that was unecessary
given the new display and margin style settings
- lock icon is bigger
- kanban icons are better aligned
- state is as big as avatar
- remove the 'remaining hours on SO' field
- deadline field will be optional and hidden by default in project.task list view
- remove the rating field
- add stage field as optional in project.task list views
- priority and state fields will no longer be optional
- stage_id will be copied when duplicating a task, except when the task is
generated through the recurrence
- remove the tooltip of the tag_ids field
- When duplicating a task having sub-tasks, '(copy)' is no longer
added to the name of the sub-tasks of this task.
- project.task kanban view:
* (+ x tasks) mention next to the name is removed
* The caret is replaced with 'fa-check-square-o x/y'
which will represent the number of sub-tasks closed
compared to the total number of sub-tasks
* Only open subtasks are displayed
* When changing the state of a sub-task to a closing one,
the sub-task is muted and removed from the list on the view reload
* The name of the parent task on the kanban card of sub-tasks is
displayed except when viewing the sub-tasks of a particular task
through the sub-tasks stat button
* project.project kanban view: the fa-check-square-o icon of
milestones is replaced with fa-flag-o
* project.task kanban card: the fa-play and fa-pause icons
are moved on the right of the remaining hours widge.
* Allow users to edit the stage_id in batch from the list view of tasks
if all of the selected tasks are part of the same project.
* the state will have the same size as the avatar
* change the opacity of the tasks that are closed
* state is at the right of subtask list
* the striked should be replaced by the opacity on the kanban card
Enterprise PR: odoo/enterprise#38132
Task-3229873
closes odoo/odoo#116628
X-original-commit: 09b5d5843096d27b5a4ab0f603ad44bdb2e74723
Related: odoo/upgrade#4478
Related: odoo/enterprise#38771
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
Co-authored-by: Panagiotis Kyriakou <paky@odoo.com>
Co-authored-by: Bastien (bvdn) <bvdn@odoo.com>
This commit is contained in:
co-authored by
Panagiotis Kyriakou
Bastien
parent
cb12ab2f1c
commit
c5dd03ffee
@@ -7,7 +7,6 @@ from odoo.exceptions import UserError, RedirectWarning
|
||||
from odoo.addons.rating.models.rating_data import OPERATOR_MAPPING
|
||||
|
||||
PROJECT_TASK_READABLE_FIELDS = {
|
||||
'allow_subtasks',
|
||||
'allow_timesheets',
|
||||
'analytic_account_active',
|
||||
'effective_hours',
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
<field name="employee_id"/>
|
||||
<field name="project_id"/>
|
||||
<field name="task_id"/>
|
||||
<field name="parent_task_id" groups="project.group_subtask_project"/>
|
||||
<field name="parent_task_id"/>
|
||||
<field name="name"/>
|
||||
<field name="department_id"/>
|
||||
<field name="manager_id"/>
|
||||
@@ -176,7 +176,7 @@
|
||||
<filter name="month" string="Date" date="date"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Project" name="groupby_project" domain="[]" context="{'group_by': 'project_id'}"/>
|
||||
<filter string="Parent Task" name="groupby_parent_task" domain="[]" context="{'group_by': 'parent_task_id'}" groups="project.group_subtask_project"/>
|
||||
<filter string="Parent Task" name="groupby_parent_task" domain="[]" context="{'group_by': 'parent_task_id'}"/>
|
||||
<filter string="Task" name="groupby_task" domain="[]" context="{'group_by': 'task_id'}"/>
|
||||
<filter string="Date" name="groupby_date" domain="[]" context="{'group_by': 'date'}" help="Timesheet by Date"/>
|
||||
<filter string="Department" name="groupby_department" domain="[]" context="{'group_by': 'department_id'}"/>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<label for="planned_hours" attrs="{'invisible': [('allow_timesheets', '=', False)]}"/>
|
||||
<div class="text-nowrap" attrs="{'invisible': [('allow_timesheets', '=', False)]}">
|
||||
<field name="planned_hours" class="oe_inline" widget="float_time"/>
|
||||
<span attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}">
|
||||
(incl. <field name="subtask_planned_hours" nolabel="1" groups="project.group_subtask_project" widget="timesheet_uom_no_toggle" class="oe_inline"/> on
|
||||
<span attrs="{'invisible': [('subtask_count', '=', 0)]}">
|
||||
(incl. <field name="subtask_planned_hours" nolabel="1" widget="timesheet_uom_no_toggle" class="oe_inline"/> on
|
||||
<span class="fw-bold text-dark"> Sub-tasks</span>)
|
||||
</span>
|
||||
<span>(<field name="progress" class="oe_inline" nolabel="1" widget="integer"/> %)</span>
|
||||
@@ -78,20 +78,20 @@
|
||||
<label class="fw-bold" for="effective_hours" string="Days Spent" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
|
||||
</span>
|
||||
<field name="effective_hours" widget="timesheet_uom" nolabel="1"/>
|
||||
<button name="action_view_subtask_timesheet" type="object" class="ps-0 border-0 oe_inline oe_link mb-2 o_td_label float-start" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}">
|
||||
<button name="action_view_subtask_timesheet" type="object" class="ps-0 border-0 oe_inline oe_link mb-2 o_td_label float-start" attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}">
|
||||
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', True)]}">Hours Spent on Sub-tasks:</span>
|
||||
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', False)]}">Days Spent on Sub-tasks:</span>
|
||||
</button>
|
||||
<field name="subtask_effective_hours" class="mt-2" widget="timesheet_uom"
|
||||
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
|
||||
<span id="total_hours_spent_label" attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" class="o_td_label float-start">
|
||||
attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
|
||||
<span id="total_hours_spent_label" attrs="{'invisible': [('subtask_effective_hours', '=', 0.0)]}" class="o_td_label float-start">
|
||||
<label class="fw-bold" for="total_hours_spent" string="Total Hours"
|
||||
attrs="{'invisible': [('encode_uom_in_days', '=', True)]}"/>
|
||||
<label class="fw-bold" for="total_hours_spent" string="Total Days"
|
||||
attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
|
||||
</span>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" class="oe_subtotal_footer_separator" nolabel="1"
|
||||
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" />
|
||||
attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" />
|
||||
<span class="o_td_label float-start">
|
||||
<label class="fw-bold" for="remaining_hours" string="Remaining Hours"
|
||||
attrs="{'invisible': ['|', '|', ('planned_hours', '=', 0.0), ('encode_uom_in_days', '=', True), ('remaining_hours', '<', 0)]}"/>
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
<label for="planned_hours" attrs="{'invisible': [('allow_timesheets', '=', False)]}"/>
|
||||
<div class="text-nowrap" attrs="{'invisible': [('allow_timesheets', '=', False)]}">
|
||||
<field name="planned_hours" class="oe_inline" widget="float_time"/>
|
||||
<span attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_count', '=', 0)]}">
|
||||
(incl. <field name="subtask_planned_hours" nolabel="1" groups="project.group_subtask_project" widget="timesheet_uom_no_toggle" class="oe_inline"/> on
|
||||
<span attrs="{'invisible': [('subtask_count', '=', 0)]}">
|
||||
(incl. <field name="subtask_planned_hours" nolabel="1" widget="timesheet_uom_no_toggle" class="oe_inline"/> on
|
||||
<span class="fw-bold text-dark"> Sub-tasks</span>)
|
||||
</span>
|
||||
<span attrs="{'invisible': [('project_id', '=', False)]}">(<field name="progress" class="oe_inline" nolabel="1" widget="integer"/> %)</span>
|
||||
@@ -29,7 +29,6 @@
|
||||
</xpath>
|
||||
<xpath expr="//notebook/page[@name='description_page']" position="after">
|
||||
<field name="allow_timesheets" invisible="1"/>
|
||||
<field name="allow_subtasks" invisible="1"/>
|
||||
<t groups="hr_timesheet.group_hr_timesheet_user">
|
||||
<field name="analytic_account_active" invisible="1"/>
|
||||
</t>
|
||||
@@ -111,20 +110,20 @@
|
||||
<label class="fw-bold" for="effective_hours" string="Days Spent" attrs="{'invisible': [('encode_uom_in_days', '=', False)]}"/>
|
||||
</span>
|
||||
<field name="effective_hours" widget="timesheet_uom" nolabel="1"/>
|
||||
<button name="action_view_subtask_timesheet" type="object" class="ps-0 border-0 oe_inline oe_link mb-2 o_td_label float-start" attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}">
|
||||
<button name="action_view_subtask_timesheet" type="object" class="ps-0 border-0 oe_inline oe_link mb-2 o_td_label float-start" attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}">
|
||||
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', True)]}">Hours Spent on Sub-tasks:</span>
|
||||
<span class="text-nowrap" attrs="{'invisible' : [('encode_uom_in_days', '=', False)]}">Days Spent on Sub-tasks:</span>
|
||||
</button>
|
||||
<field name="subtask_effective_hours" class="mt-2" widget="timesheet_uom"
|
||||
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
|
||||
<span attrs="{'invisible': ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" class="o_td_label float-start">
|
||||
attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" nolabel="1"/>
|
||||
<span attrs="{'invisible': [('subtask_effective_hours', '=', 0.0)]}" class="o_td_label float-start">
|
||||
<label class="fw-bold" for="total_hours_spent" string="Total Hours"
|
||||
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', True)]}"/>
|
||||
attrs="{'invisible': ['|', ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', True)]}"/>
|
||||
<label class="fw-bold" for="total_hours_spent" string="Total Days"
|
||||
attrs="{'invisible': ['|', '|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', False)]}"/>
|
||||
attrs="{'invisible': ['|', ('subtask_effective_hours', '=', 0.0), ('encode_uom_in_days', '=', False)]}"/>
|
||||
</span>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" class="oe_subtotal_footer_separator" nolabel="1"
|
||||
attrs="{'invisible' : ['|', ('allow_subtasks', '=', False), ('subtask_effective_hours', '=', 0.0)]}" />
|
||||
attrs="{'invisible' : [('subtask_effective_hours', '=', 0.0)]}" />
|
||||
<span class="o_td_label float-start">
|
||||
<label class="fw-bold" for="remaining_hours" string="Remaining Hours"
|
||||
attrs="{'invisible': ['|', '|', ('planned_hours', '=', 0.0), ('encode_uom_in_days', '=', True), ('remaining_hours', '<', 0)]}"/>
|
||||
@@ -142,12 +141,11 @@
|
||||
</page>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='depend_on_ids']/tree//field[@name='company_id']" position="after">
|
||||
<field name="allow_subtasks" invisible="1" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="progress" invisible="1" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="planned_hours" widget="timesheet_uom_no_toggle" sum="Total Allocated Time" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="subtask_effective_hours" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="subtask_effective_hours" widget="timesheet_uom" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" optional="hide" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress >= 100" decoration-warning="progress >= 80 and progress < 100" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
<field name="progress" widget="progressbar" optional="hide" options="{'overflow_class': 'bg-danger'}" groups="hr_timesheet.group_hr_timesheet_user"/>
|
||||
</xpath>
|
||||
@@ -160,13 +158,12 @@
|
||||
<field name="inherit_id" ref="project.project_task_view_tree_main_base" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="date_deadline" position="before">
|
||||
<field name="allow_subtasks" invisible="1"/>
|
||||
<field name="progress" invisible="1"/>
|
||||
<field name="effective_hours" invisible="1"/>
|
||||
<field name="planned_hours" widget="timesheet_uom_no_toggle" sum="Total Allocated Time" attrs="{'invisible' : [('planned_hours', '=', 0)]}" optional="hide"/>
|
||||
<field name="effective_hours" widget="timesheet_uom" sum="Effective Hours" optional="show" attrs="{'invisible' : [('effective_hours', '=', 0)]}"/>
|
||||
<field name="subtask_effective_hours" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" attrs="{'invisible' : [('allow_subtasks', '=', False)]}" optional="hide"/>
|
||||
<field name="subtask_effective_hours" widget="timesheet_uom" optional="hide"/>
|
||||
<field name="total_hours_spent" widget="timesheet_uom" optional="hide"/>
|
||||
<field name="remaining_hours" widget="timesheet_uom" sum="Remaining Hours" optional="hide" decoration-danger="progress >= 100" decoration-warning="progress >= 80 and progress < 100" attrs="{'invisible' : [('planned_hours', '=', 0)]}"/>
|
||||
<field name="progress" widget="progressbar" optional="show" groups="hr_timesheet.group_hr_timesheet_user" attrs="{'invisible' : [('planned_hours', '=', 0)]}" options="{'overflow_class': 'bg-danger'}" />
|
||||
</field>
|
||||
|
||||
@@ -160,7 +160,6 @@ class Project(models.Model):
|
||||
date_start = fields.Date(string='Start Date')
|
||||
date = fields.Date(string='Expiration Date', index=True, tracking=True,
|
||||
help="Date on which this project ends. The timeframe defined on the project is taken into account when viewing its planning.")
|
||||
allow_subtasks = fields.Boolean('Sub-tasks', default=lambda self: self.env.user.has_group('project.group_subtask_project'))
|
||||
allow_recurring_tasks = fields.Boolean('Recurring Tasks', default=lambda self: self.env.user.has_group('project.group_project_recurring_tasks'))
|
||||
allow_task_dependencies = fields.Boolean('Task Dependencies', default=lambda self: self.env.user.has_group('project.group_project_task_dependencies'))
|
||||
allow_milestones = fields.Boolean('Milestones', default=lambda self: self.env.user.has_group('project.group_project_milestone'))
|
||||
|
||||
@@ -20,10 +20,8 @@ PROJECT_TASK_READABLE_FIELDS = {
|
||||
'priority',
|
||||
'project_id',
|
||||
'color',
|
||||
'allow_subtasks',
|
||||
'subtask_count',
|
||||
'is_private',
|
||||
'child_text',
|
||||
'email_from',
|
||||
'create_date',
|
||||
'write_date',
|
||||
@@ -41,6 +39,7 @@ PROJECT_TASK_READABLE_FIELDS = {
|
||||
'dependent_ids',
|
||||
'message_is_follower',
|
||||
'recurring_task',
|
||||
'closed_subtask_count',
|
||||
}
|
||||
|
||||
PROJECT_TASK_WRITABLE_FIELDS = {
|
||||
@@ -124,9 +123,8 @@ class Task(models.Model):
|
||||
stage_id = fields.Many2one('project.task.type', string='Stage', compute='_compute_stage_id',
|
||||
store=True, readonly=False, ondelete='restrict', tracking=True, index=True,
|
||||
default=_get_default_stage_id, group_expand='_read_group_stage_ids',
|
||||
domain="[('project_ids', '=', project_id)]", copy=False, task_dependency_tracking=True)
|
||||
tag_ids = fields.Many2many('project.tags', string='Tags',
|
||||
help="You can only see tags that are already present in your project. If you try creating a tag that is already existing in other projects, it won't generate any duplicates.")
|
||||
domain="[('project_ids', '=', project_id)]", task_dependency_tracking=True)
|
||||
tag_ids = fields.Many2many('project.tags', string='Tags')
|
||||
|
||||
state = fields.Selection([
|
||||
('01_in_progress', 'In Progress'),
|
||||
@@ -194,9 +192,8 @@ class Task(models.Model):
|
||||
is_closed = fields.Boolean(compute='_compute_is_closed', string="Closed State", store=True, index=True)
|
||||
parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
|
||||
child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", domain="[('recurring_task', '=', False)]")
|
||||
child_text = fields.Char(compute="_compute_child_text")
|
||||
allow_subtasks = fields.Boolean(string="Allow Sub-tasks", related="project_id.allow_subtasks", readonly=True)
|
||||
subtask_count = fields.Integer("Sub-task Count", compute='_compute_subtask_count')
|
||||
closed_subtask_count = fields.Integer("Closed Sub-tasks Count", compute='_compute_subtask_count')
|
||||
project_privacy_visibility = fields.Selection(related='project_id.privacy_visibility', string="Project Visibility")
|
||||
# Computed field about working time elapsed between record creation and assignation/closing.
|
||||
working_hours_open = fields.Float(compute='_compute_elapsed', string='Working Hours to Assign', digits=(16, 2), store=True, group_operator="avg")
|
||||
@@ -222,8 +219,6 @@ class Task(models.Model):
|
||||
compute='_compute_has_late_and_unreached_milestone',
|
||||
search='_search_has_late_and_unreached_milestone',
|
||||
)
|
||||
#used to display the stage of the task in the tree view (if the project is private will return the personnal_stage_id, else will return the stage_id)
|
||||
stage_display = fields.Char(compute='_compute_stage_display', string='Stage Display')
|
||||
# Task Dependencies fields
|
||||
allow_task_dependencies = fields.Boolean(related='project_id.allow_task_dependencies')
|
||||
# Tracking of this field is done in the write function
|
||||
@@ -525,20 +520,20 @@ class Task(models.Model):
|
||||
for task in self:
|
||||
task.subtask_planned_hours = sum(child_task.planned_hours + child_task.subtask_planned_hours for child_task in task.child_ids)
|
||||
|
||||
@api.depends('child_ids')
|
||||
def _compute_child_text(self):
|
||||
for task in self:
|
||||
if not task.subtask_count:
|
||||
task.child_text = False
|
||||
elif task.subtask_count == 1:
|
||||
task.child_text = _("(+ 1 task)")
|
||||
else:
|
||||
task.child_text = _("(+ %(child_count)s tasks)", child_count=task.subtask_count)
|
||||
|
||||
@api.depends('child_ids')
|
||||
def _compute_subtask_count(self):
|
||||
total_and_closed_subtask_count_per_parent_id = {
|
||||
group['parent_id'][0]: (
|
||||
group['parent_id_count'],
|
||||
sum(s in CLOSED_STATES for s in group['state']),
|
||||
) for group in self.env['project.task']._read_group(
|
||||
[('parent_id', 'in', self.ids)],
|
||||
['state:array_agg', 'parent_id'],
|
||||
['parent_id'],
|
||||
)
|
||||
}
|
||||
for task in self:
|
||||
task.subtask_count = len(task._get_all_subtasks())
|
||||
task.subtask_count, task.closed_subtask_count = total_and_closed_subtask_count_per_parent_id.get(task.id, (0, 0))
|
||||
|
||||
@api.onchange('company_id')
|
||||
def _onchange_task_company(self):
|
||||
@@ -561,11 +556,6 @@ class Task(models.Model):
|
||||
else:
|
||||
task.stage_id = False
|
||||
|
||||
@api.depends('project_id', 'stage_id', 'personal_stage_id')
|
||||
def _compute_stage_display(self):
|
||||
for task in self:
|
||||
task.stage_display = task.stage_id.name if task.project_id else task.personal_stage_id.stage_id.name
|
||||
|
||||
@api.depends('user_ids')
|
||||
def _compute_portal_user_names(self):
|
||||
""" This compute method allows to see all the names of assigned users to each task contained in `self`.
|
||||
@@ -676,8 +666,7 @@ class Task(models.Model):
|
||||
default['name'] = _("%s (copy)", self.name)
|
||||
if self.recurrence_id:
|
||||
default['recurrence_id'] = self.recurrence_id.copy().id
|
||||
if self.allow_subtasks:
|
||||
default['child_ids'] = [child.copy({'name': child.name} if has_default_name else None).id for child in self.child_ids]
|
||||
default['child_ids'] = [child.copy({'name': child.name}).id for child in self.child_ids]
|
||||
self_with_mail_context = self.with_context(mail_auto_subscribe_no_notify=True, mail_create_nosubscribe=True)
|
||||
task_copy = super(Task, self_with_mail_context).copy(default)
|
||||
if self.allow_task_dependencies:
|
||||
|
||||
@@ -102,8 +102,7 @@ class ProjectTaskRecurrence(models.Model):
|
||||
})
|
||||
|
||||
create_values['stage_id'] = occurrence_from.project_id.type_ids[0].id if occurrence_from.project_id.type_ids else occurrence_from.stage_id.id
|
||||
if occurrence_from.allow_subtasks:
|
||||
create_values['child_ids'] = [
|
||||
Command.create(self._create_next_occurrence_values(child)) for child in occurrence_from.with_context(active_test=False).child_ids
|
||||
]
|
||||
create_values['child_ids'] = [
|
||||
Command.create(self._create_next_occurrence_values(child)) for child in occurrence_from.with_context(active_test=False).child_ids
|
||||
]
|
||||
return create_values
|
||||
|
||||
@@ -9,7 +9,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
|
||||
module_project_forecast = fields.Boolean(string="Planning")
|
||||
module_hr_timesheet = fields.Boolean(string="Task Logs")
|
||||
group_subtask_project = fields.Boolean("Sub-tasks", implied_group="project.group_subtask_project")
|
||||
group_project_rating = fields.Boolean("Customer Ratings", implied_group='project.group_project_rating')
|
||||
group_project_stages = fields.Boolean("Project Stages", implied_group="project.group_project_stages")
|
||||
group_project_recurring_tasks = fields.Boolean("Recurring Tasks", implied_group="project.group_project_recurring_tasks")
|
||||
@@ -37,7 +36,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
# key: (config_flag, is_global), value: project_flag
|
||||
("group_project_rating", True): "rating_active",
|
||||
("group_project_recurring_tasks", True): "allow_recurring_tasks",
|
||||
("group_subtask_project", False): "allow_subtasks",
|
||||
("group_project_task_dependencies", False): "allow_task_dependencies",
|
||||
("group_project_milestone", False): "allow_milestones",
|
||||
}
|
||||
|
||||
@@ -19,11 +19,6 @@
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_subtask_project" model="res.groups">
|
||||
<field name="name">Use Subtasks</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
</record>
|
||||
|
||||
<record id="group_project_rating" model="res.groups">
|
||||
<field name="name">Use Rating on Project</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
|
||||
+3
-2
@@ -2,11 +2,12 @@
|
||||
|
||||
import { registry } from '@web/core/registry';
|
||||
import { CharField, charField } from '@web/views/fields/char/char_field';
|
||||
import { formatChar } from '@web/views/fields/formatters';
|
||||
|
||||
class ProjectTaskNameWithSubtaskCountCharField extends CharField {
|
||||
get formattedSubtaskCount() {
|
||||
return formatChar(this.props.record.data.allow_subtasks && this.props.record.data.child_text || '');
|
||||
return this.props.record.data.subtask_count
|
||||
? `(${this.props.record.data.closed_subtask_count}/${this.props.record.data.subtask_count} subtasks)`
|
||||
: "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-6
@@ -2,12 +2,6 @@
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="project.ProjectTaskNameWithSubtaskCountCharField" t-inherit="web.CharField" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//span[@t-esc='formattedValue']" position="before">
|
||||
<s t-if="['1_done', '1_canceled'].includes(this.props.record.data.state) and ['kanban', 'list'].includes(this.props.record.activeFields[this.props.name].viewType)" t-esc="formattedValue"/>
|
||||
</xpath>
|
||||
<xpath expr="//span[@t-esc='formattedValue']" position="attributes">
|
||||
<attribute name="t-else">""</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[@t-esc='formattedValue']" position="after">
|
||||
<span
|
||||
class="text-muted ms-2"
|
||||
|
||||
+6
-2
@@ -112,20 +112,24 @@ export class ProjectTaskStateSelection extends StateSelectionField {
|
||||
return this.props.isToggleMode || !this.props.record.data.project_id;
|
||||
}
|
||||
|
||||
isView(viewNames) {
|
||||
return viewNames.includes(this.props.viewType);
|
||||
}
|
||||
|
||||
async toggleState() {
|
||||
const toggleVal = this.currentValue == "1_done" ? "01_in_progress" : "1_done";
|
||||
await this.updateRecord(toggleVal);
|
||||
}
|
||||
|
||||
getDropdownPosition() {
|
||||
if (['kanban', 'list'].includes(this.props.viewType) || this.env.isSmall) {
|
||||
if (this.isView(['kanban', 'list']) || this.env.isSmall) {
|
||||
return '';
|
||||
}
|
||||
return 'bottom-end';
|
||||
}
|
||||
|
||||
getTogglerClass(currentValue) {
|
||||
if (['kanban', 'list'].includes(this.props.viewType) || this.env.isSmall) {
|
||||
if (this.isView(['kanban', 'list']) || this.env.isSmall) {
|
||||
return 'btn btn-link d-flex p-0';
|
||||
}
|
||||
return 'o_state_button btn rounded-pill ' + this.colorButton[currentValue];
|
||||
|
||||
+42
-11
@@ -1,25 +1,56 @@
|
||||
.o_field_project_task_state_selection{
|
||||
.o_status {
|
||||
width: 14.65px;
|
||||
height: 14.65px;
|
||||
width: 19.5px;
|
||||
height: 19.5px;
|
||||
text-align: center;
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
|
||||
.fa-lg {
|
||||
font-size: 1.75em;
|
||||
margin-top: -2.5px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.o_color_bubble_22 {
|
||||
background-color: #ffac00;
|
||||
}
|
||||
|
||||
.dropdown-item .fa {
|
||||
transform: translateX(-50%);
|
||||
.fa-hourglass-o {
|
||||
font-size: 1.4em !important;
|
||||
margin-top: 0.5px !important;
|
||||
}
|
||||
|
||||
.fa-hourglass-o{
|
||||
font-size: 1.15em;
|
||||
padding-left: 1px;
|
||||
.o_task_state_list_view {
|
||||
.fa-lg {
|
||||
font-size: 1.315em;
|
||||
vertical-align: -6%;
|
||||
}
|
||||
.o_status {
|
||||
width: 14.65px;
|
||||
height: 14.65px;
|
||||
text-align: center;
|
||||
}
|
||||
.fa-hourglass-o {
|
||||
font-size: 1.15em !important;
|
||||
padding-left: 1px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This negative margin is only useful in list_view
|
||||
.o_data_cell .o_field_project_task_state_selection .dropdown-toggle .fa{
|
||||
margin-top: -1.22px;
|
||||
.dropdown-menu {
|
||||
.fa {
|
||||
margin-top: -1.5px;
|
||||
font-size: 1.315em;
|
||||
vertical-align: -6%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.o_status {
|
||||
margin-top: 1px;
|
||||
width: 14.65px;
|
||||
height: 14.65px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -6,8 +6,8 @@
|
||||
</xpath>
|
||||
<!-- Waiting state button -->
|
||||
<xpath expr="//t[@t-if='props.readonly']" position="after">
|
||||
<t t-elif="currentValue == '04_waiting_normal' and ['kanban', 'list'].includes(this.props.viewType)">
|
||||
<button class="d-flex align-items-center btn fw-normal p-0 justify-content-center " title="This task is blocked by another unfinished task">
|
||||
<t t-elif="currentValue == '04_waiting_normal' and isView(['kanban', 'list'])">
|
||||
<button class="d-flex align-items-center btn fw-normal p-0 justify-content-center " title="This task is blocked by another unfinished task" t-att-class="{'o_task_state_list_view': isView(['list'])}">
|
||||
<i class="fa fa-lg fa-hourglass-o"></i>
|
||||
</button>
|
||||
</t>
|
||||
@@ -15,11 +15,11 @@
|
||||
<!-- The toggle mark as done button -->
|
||||
<xpath expr="//t[@t-if='props.readonly']" position="after">
|
||||
<t t-elif="isToggleMode and currentValue == '01_in_progress'">
|
||||
<button t-if="['kanban', 'list'].includes(this.props.viewType) or this.env.isSmall" class="d-flex align-items-center btn fw-normal p-0" t-on-click.stop="toggleState">
|
||||
<i t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}"></i>
|
||||
<button t-if="isView(['kanban', 'list']) or this.env.isSmall" class="d-flex align-items-center btn fw-normal p-0" t-on-click.stop="toggleState" t-att-class="{'o_task_state_list_view': isView(['list'])}">
|
||||
<i t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }} {{ ['1_done', '1_canceled'].includes(currentValue) and isView(['kanban']) ? 'opacity-50' : '' }}"></i>
|
||||
</button>
|
||||
<button t-else="" class="o_state_button btn oe_highlight rounded-pill" style="white-space: nowrap;"
|
||||
t-attf-class="{ #{currentValue == '1_done' ? 'btn-success' : 'btn-light border-2 border-dark'} }"
|
||||
t-att-class="currentValue == '1_done' ? 'btn-success' : 'btn-light border-2 border-dark'"
|
||||
t-on-click="toggleState" t-on-mouseenter="onMouseEnterStateButton"
|
||||
t-on-mouseleave="onMouseLeaveStateButton">
|
||||
<div class="d-flex align-items-center">
|
||||
@@ -41,9 +41,9 @@
|
||||
|
||||
<!-- Normal dropdown mode toggle button (displayed on the card/record by default) -->
|
||||
<xpath expr="//t[@t-set-slot='toggler']/div" position="replace">
|
||||
<div t-if="['kanban', 'list'].includes(this.props.viewType) or this.env.isSmall" class="d-flex align-items-center">
|
||||
<div t-if="isView(['kanban', 'list']) or this.env.isSmall" class="d-flex align-items-center" t-att-class="{'o_task_state_list_view': isView(['list'])}">
|
||||
<i t-if="currentValue == '04_waiting_normal'" t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}" style="color: #4A4F59;"/>
|
||||
<i t-else="" t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}"/>
|
||||
<i t-else="" t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }} {{ ['1_done', '1_canceled'].includes(currentValue) and isView(['kanban']) ? 'opacity-50' : '' }}"/>
|
||||
</div>
|
||||
<div t-else="" class="d-flex align-items-center">
|
||||
<t t-if="currentValue == '04_waiting_normal'">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { Component } from "@odoo/owl";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { Record } from "@web/views/record";
|
||||
import { KanbanMany2ManyTagsAvatarUserField } from "@mail/web/fields/many2many_avatar_user_field/many2many_avatar_user_field";
|
||||
import { Field } from "@web/views/fields/field";
|
||||
@@ -12,6 +12,17 @@ import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
|
||||
export class SubtaskKanbanList extends Component {
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.orm = useService("orm");
|
||||
this.subTaskIds = [];
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
async onWillStart() {
|
||||
this.subTaskIds = await this.orm.search(this.props.record.resModel, [
|
||||
["parent_id", "=", this.props.record.resId],
|
||||
["state", "not in", ["1_done", "1_canceled"]],
|
||||
]);
|
||||
}
|
||||
|
||||
async goToSubtask(subtask_id) {
|
||||
@@ -62,6 +73,10 @@ export class SubtaskKanbanList extends Component {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
get values() {
|
||||
return { child_ids: this.subTaskIds };
|
||||
}
|
||||
}
|
||||
|
||||
SubtaskKanbanList.components = {
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
.subtask_list_row {
|
||||
display: grid;
|
||||
grid-template-columns: 20px auto auto;
|
||||
column-gap: 4px;
|
||||
grid-template-columns: auto auto 22px;
|
||||
padding: 4px 0px;
|
||||
|
||||
.subtask_name_col {
|
||||
|
||||
@@ -3,27 +3,27 @@
|
||||
<templates>
|
||||
|
||||
<t t-name="project.SubtaskKanbanList" owl="1">
|
||||
<Record resModel="'project.task'" resId="props.record.resId" fields="fieldsInfo" activeFields="fieldsInfo" t-slot-scope="data">
|
||||
<Record resModel="'project.task'" resId="props.record.resId" fields="fieldsInfo" activeFields="fieldsInfo" values="values" t-slot-scope="data">
|
||||
<div class="subtask_list">
|
||||
<t t-foreach="data.record.data.child_ids.records" t-as="subTask" t-key="subTask.resId">
|
||||
<div class="subtask_list_row">
|
||||
<Field name="'state'"
|
||||
class="`subtask_state_widget_col d-flex justify-content-center align-items-center`"
|
||||
record="subTask"
|
||||
fieldInfo="subTask.fields.state"
|
||||
type="'project_task_state_selection'"/>
|
||||
<a class="subtask_name_col"
|
||||
<a t-attf-class="subtask_name_col {{['1_done', '1_canceled'].includes(subTask.data.state) ? 'text-muted opacity-50' : ''}}"
|
||||
t-att-title="subTask.data.display_name"
|
||||
style="color: inherit;"
|
||||
t-esc="subTask.data.display_name"
|
||||
t-on-click.prevent="() => this.goToSubtask(subTask.resId)"/>
|
||||
<Field
|
||||
class="`subtask_user_widget_col justify-content-end align-items-center`"
|
||||
class="`subtask_user_widget_col justify-content-end align-items-center ${['1_done', '1_canceled'].includes(subTask.data.state) ? 'opacity-50' : ''}`"
|
||||
name="'user_ids'"
|
||||
record="subTask"
|
||||
fieldInfo="subTask.fields.user_ids"
|
||||
readonly="true"
|
||||
type="'many2many_avatar_user'"/>
|
||||
<Field name="'state'"
|
||||
class="`subtask_state_widget_col d-flex justify-content-center align-items-center`"
|
||||
record="subTask"
|
||||
fieldInfo="subTask.fields.state"
|
||||
type="'project_task_state_selection'"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
@@ -62,3 +62,11 @@
|
||||
width: 5%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_field_project_state_selection {
|
||||
.dropdown-toggle .o_status {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ export class ProjectTaskKanbanCompiler extends KanbanCompiler {
|
||||
compileSubtaskListButton(el) {
|
||||
this.subtaskListComponentCompiled.button = true;
|
||||
el.setAttribute("t-on-click", `() => __comp__.state.folded = !__comp__.state.folded`);
|
||||
el.removeAttribute("class");
|
||||
el.setAttribute("t-attf-class", "subtask_list_button btn fa {{ state.folded ? 'fa-solid fa-angle-right' : 'fa-solid fa-angle-down' }}");
|
||||
const compiled = createElement(el.nodeName);
|
||||
for (const { name, value } of el.attributes) {
|
||||
compiled.setAttribute(name, value);
|
||||
|
||||
@@ -2,4 +2,11 @@
|
||||
div.o_field_many2many_avatar_user[name="user_ids"] {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.rating_face {
|
||||
font-weight: bold;
|
||||
padding-top: 2px;
|
||||
padding-right: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { getRawValue } from "@web/views/kanban/kanban_record";
|
||||
|
||||
export class ProjectTaskListRenderer extends ListRenderer {
|
||||
getCellReadonly(column, record) {
|
||||
let readonly = false;
|
||||
const selection = this.props.list.selection;
|
||||
if (column.name === "stage_id" && selection.length) {
|
||||
const projectId = getRawValue(selection[0], "project_id");
|
||||
readonly = selection.some(
|
||||
(task) => getRawValue(task, "project_id") !== projectId
|
||||
);
|
||||
}
|
||||
return readonly || super.getCellReadonly(column, record);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { listView } from '@web/views/list/list_view';
|
||||
import { ProjectControlPanel } from "../../components/project_control_panel/project_control_panel";
|
||||
import { ProjectTaskListRenderer } from "./project_task_list_renderer";
|
||||
|
||||
export const projectTaskListView = {
|
||||
...listView,
|
||||
ControlPanel: ProjectControlPanel,
|
||||
Renderer: ProjectTaskListRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("project_task_list", projectTaskListView);
|
||||
|
||||
@@ -18,28 +18,34 @@ QUnit.module('Subtask Kanban List tests', {
|
||||
beforeEach: async function () {
|
||||
const pyEnv = await startServer();
|
||||
const projectId = pyEnv['project.project'].create([
|
||||
{ name: "Project One", allow_subtasks: true },
|
||||
{ name: "Project One" },
|
||||
]);
|
||||
const userId = pyEnv['res.users'].create([
|
||||
{ name: "User One", login: 'one', password: 'one' },
|
||||
]);
|
||||
pyEnv['project.task'].create([
|
||||
{ name: 'task one', project_id: projectId, subtask_count: 2, child_ids: [2, 3], state: '01_in_progress', user_ids: [userId] },
|
||||
{ name: 'task two', subtask_count: 0, child_ids: [], state: '03_approved' },
|
||||
{ name: 'task three', subtask_count: 0, child_ids: [], state: '02_changes_requested' },
|
||||
{ name: 'task four', project_id: projectId, subtask_count: 0, child_ids: [], state: '01_in_progress' },
|
||||
{ name: 'task one', project_id: projectId, closed_subtask_count: 2, subtask_count: 3, child_ids: [2, 3, 4], state: '01_in_progress', user_ids: [userId] },
|
||||
{ name: 'task two', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '03_approved' },
|
||||
{ name: 'task three', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '02_changes_requested' },
|
||||
{ name: 'task four', parent_id: 1, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '1_done' },
|
||||
{ name: 'task five', closed_subtask_count: 0, subtask_count: 1, child_ids: [6], state: '03_approved' },
|
||||
{ name: 'task six', parent_id: 5, closed_subtask_count: 0, subtask_count: 0, child_ids: [], state: '1_canceled' },
|
||||
]);
|
||||
this.views = {
|
||||
"project.task,false,kanban":
|
||||
`<kanban js_class="project_task_kanban">
|
||||
<field name="subtask_count"/>
|
||||
<field name="closed_subtask_count"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="user_ids" invisible="1"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<a t-if="record.subtask_count.raw_value" class="subtask_list_button" title="See Subtasks" style="width: 29px;"/>
|
||||
<a t-if="record.closed_subtask_count.raw_value">
|
||||
<span title="See Subtasks" class="subtask_list_button fa fa-check-square-o me-1"/>
|
||||
<t t-out="record.closed_subtask_count.value"/>/<t t-out="record.subtask_count.value"/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
</t>
|
||||
@@ -60,13 +66,13 @@ QUnit.module('Subtask Kanban List tests', {
|
||||
views: [[false, "kanban"]],
|
||||
});
|
||||
|
||||
assert.containsOnce(target, '.subtask_list_button', "Only kanban boxes of parent tasks should have the drawdown button, in this case this is 1");
|
||||
assert.containsOnce(target, '.subtask_list_button', "Only kanban boxes of parent tasks having open subtasks should have the drawdown button, in this case this is 1");
|
||||
assert.containsNone(target, '.subtask_list', "If the drawdown button is not clicked, the subtasks list should be hidden");
|
||||
|
||||
await click(target, '.subtask_list_button');
|
||||
|
||||
assert.containsOnce(target, '.subtask_list', "Clicking on the button should make the subtask list render, in this case we are expectig 1 list");
|
||||
assert.containsN(target, '.subtask_list_row', 2, "The list rendered should show all the subtasks of the task, in this case 2");
|
||||
assert.containsN(target, '.subtask_list_row', 2, "The list rendered should show the open subtasks of the task, in this case 2");
|
||||
assert.containsN(target, '.subtask_state_widget_col', 2, "Each of the list's rows should have 1 state widget, thus we are looking for 2 in total");
|
||||
assert.containsN(target, '.subtask_user_widget_col', 2, "Each of the list's rows should have 1 user widgets, thus we are looking for 2 in total");
|
||||
assert.containsN(target, '.subtask_name_col', 2, "Each of the list's rows should display the subtask's name, thus we are looking for 2 in total");
|
||||
|
||||
@@ -260,12 +260,7 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
||||
self.task_2.write({'project_id': False})
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
# <field name="parent_id" groups="base.group_no_one"/>
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
@@ -281,17 +276,10 @@ class TestMultiCompanyProject(TestMultiCompanyCommon):
|
||||
self.assertEqual(task.company_id, self.task_1.company_id, "The company of the subtask should be the one from its parent when no project is set.")
|
||||
|
||||
def test_cross_subtask_project(self):
|
||||
# set up default subtask project
|
||||
self.project_company_a.write({'allow_subtasks': True})
|
||||
|
||||
# For `parent_id` to be visible in the view, you need
|
||||
# 1. The debug mode
|
||||
# 2. `allow_subtasks` to be true
|
||||
# <field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
# `allow_subtasks` is a related to `allow_subtasks` on the project
|
||||
# as the point of the test is to test the behavior of the task `_compute_project_id` when there is no project,
|
||||
# `allow_subtasks` is by default invisible, and you shouldn't therefore be able to change it.
|
||||
# So, to make it visible, temporary modify the view to make it visible even when `allow_subtasks` is `False`.
|
||||
# <field name="parent_id" groups="base.group_no_one"/>
|
||||
view = self.env.ref('project.view_task_form2').sudo()
|
||||
tree = etree.fromstring(view.arch)
|
||||
for node in tree.xpath('//field[@name="parent_id"][@attrs]'):
|
||||
|
||||
@@ -17,7 +17,6 @@ class TestProjectConfig(TestProjectCommon):
|
||||
cls.Settings = cls.env["res.config.settings"]
|
||||
cls.features = (
|
||||
# Pairs of associated (config_flag, project_flag)
|
||||
("group_subtask_project", "allow_subtasks"),
|
||||
("group_project_recurring_tasks", "allow_recurring_tasks"),
|
||||
("group_project_rating", "rating_active"),
|
||||
)
|
||||
|
||||
@@ -145,10 +145,6 @@ class TestProjectSharing(TestProjectSharingCommon):
|
||||
3.2) Create a sub-task
|
||||
3.3) Create a second sub-task
|
||||
"""
|
||||
# 0) Allow to create subtasks in the project tasks
|
||||
# Required for `child_ids` to be visible in the view
|
||||
# {'invisible': [('allow_subtasks', '=', False)]}
|
||||
self.project_cows.allow_subtasks = True
|
||||
# 1) Give the 'read' access mode to a portal user in a project and try to create task with this user.
|
||||
with self.assertRaises(AccessError, msg="Should not accept the portal user create a task in the project when he has not the edit access right."):
|
||||
with self.get_project_sharing_form_view(self.task_cow.with_context({'tracking_disable': True, 'default_project_id': self.project_cows.id}), self.user_portal) as form:
|
||||
|
||||
@@ -8,15 +8,6 @@ from odoo.tests.common import Form
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestProjectSubtasks(TestProjectCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Enable the company setting
|
||||
cls.env['res.config.settings'].create({
|
||||
'group_subtask_project': True
|
||||
}).execute()
|
||||
|
||||
def test_task_display_project_with_default_form(self):
|
||||
"""
|
||||
Create a task in the default task form should take the project set in the form or the default project in the context
|
||||
@@ -92,7 +83,8 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
6) Remove parent task:
|
||||
- The project id should remain unchanged
|
||||
7) Remove project id then parent id:
|
||||
- should not be possible since allow_subtasks is false when no project is set on the subtask
|
||||
- Project should be removed
|
||||
- Parent should be removed
|
||||
"""
|
||||
# 1)
|
||||
with Form(self.task_1.with_context({'tracking_disable': True})) as task_form:
|
||||
@@ -149,12 +141,11 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
child_task_form.project_id = self.project_goats
|
||||
with Form(self.task_1.child_ids.with_context({'tracking_disable': True})) as subtask_form:
|
||||
subtask_form.project_id = self.env['project.project']
|
||||
with self.assertRaises(AssertionError, msg="the parent_id field should not be visible since no project is set on the subtask and so allow_subtasks is false"):
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
subtask_form.parent_id = self.env['project.task']
|
||||
orphan_subtask = subtask_form.save()
|
||||
|
||||
self.assertFalse(orphan_subtask.project_id, "The project should be removed as expected.")
|
||||
self.assertEqual(orphan_subtask.parent_id, self.task_1, "The parent should not be removed since the field was not there in the form view after project had been removed.")
|
||||
self.assertFalse(orphan_subtask.project_id, "The Parent should be removed as expected.")
|
||||
|
||||
def test_subtask_stage(self):
|
||||
"""
|
||||
@@ -208,25 +199,31 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
],
|
||||
})
|
||||
|
||||
task_count_with_subtasks_including_archived_in_project_goats = 6
|
||||
task_count_with_subtasks_including_archived = 8
|
||||
task_count_in_project_pigs = self.project_pigs.task_count
|
||||
self.project_goats._compute_task_count() # recompute without archived tasks and subtasks
|
||||
task_count_in_project_goats = self.project_goats.task_count
|
||||
project_goats_duplicated = self.project_goats.copy()
|
||||
self.project_pigs._compute_task_count() # retrigger since a new task should be added in the project after the duplication of Project Goats
|
||||
|
||||
def subtask_in_project(tasks, project):
|
||||
task_count = 0
|
||||
for task in tasks:
|
||||
if not task.project_id or task.project_id == project:
|
||||
task_count += 1 + subtask_in_project(task.child_ids, project)
|
||||
return task_count
|
||||
def dfs(task):
|
||||
# ABGH: i used dfs to avoid visiting a task 2 times as it can be a direct task for the project and a subtask for another task like child 6
|
||||
visited[task.id] = True
|
||||
total_count = 1
|
||||
for child_id in task.child_ids:
|
||||
if child_id.id not in visited:
|
||||
total_count += dfs(child_id)
|
||||
return total_count
|
||||
|
||||
tasks_copied_count = subtask_in_project(project_goats_duplicated.tasks, project_goats_duplicated)
|
||||
visited = {}
|
||||
tasks_copied_count = 0
|
||||
for task in project_goats_duplicated.tasks:
|
||||
if not task.id in visited:
|
||||
tasks_copied_count += dfs(task)
|
||||
|
||||
self.assertEqual(
|
||||
tasks_copied_count,
|
||||
task_count_with_subtasks_including_archived_in_project_goats - 1,
|
||||
task_count_with_subtasks_including_archived - 1,
|
||||
'The number of duplicated tasks (subtasks included) should be equal to the number of all tasks (with active subtasks included) of both projects, '
|
||||
'that is only the active subtasks are duplicated.')
|
||||
self.assertEqual(self.project_goats.task_count, task_count_in_project_goats, 'The number of tasks should be the same before and after the duplication of this project.')
|
||||
@@ -254,6 +251,6 @@ class TestProjectSubtasks(TestProjectCommon):
|
||||
child_subtask_form.name = 'Test Subtask 2'
|
||||
self.assertFalse(child_subtask_form.project_id)
|
||||
|
||||
self.assertEqual(task.subtask_count, 2, "Parent task should have 2 children")
|
||||
self.assertEqual(task.subtask_count, 1, "Parent task should have 1 children")
|
||||
task_2 = task.copy()
|
||||
self.assertEqual(task_2.subtask_count, 2, "If the parent task is duplicated then the sub task should be copied")
|
||||
self.assertEqual(task_2.subtask_count, 1, "If the parent task is duplicated then the sub task should be copied")
|
||||
|
||||
@@ -189,7 +189,6 @@ class TestTaskDependencies(TestProjectCommon):
|
||||
|
||||
def test_duplicate_project_with_subtask_dependencies(self):
|
||||
self.project_goats.allow_task_dependencies = True
|
||||
self.project_goats.allow_subtasks = True
|
||||
parent_task = self.env['project.task'].with_context({'mail_create_nolog': True}).create({
|
||||
'name': 'Parent Task',
|
||||
'project_id': self.project_goats.id,
|
||||
|
||||
@@ -112,11 +112,8 @@
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group name="group_tasks_managment" string="Tasks Management" col="1" class="row mt16 o_settings_container" groups="project.group_subtask_project,project.group_project_task_dependencies,project.group_project_milestone,project.group_project_recurring_tasks">
|
||||
<group name="group_tasks_managment" string="Tasks Management" col="1" class="row mt16 o_settings_container" groups="project.group_project_task_dependencies,project.group_project_milestone,project.group_project_recurring_tasks">
|
||||
<div>
|
||||
<setting id="subtask_settings" help="Split your tasks to organize your work into sub-milestones" groups="project.group_subtask_project">
|
||||
<field name="allow_subtasks"/>
|
||||
</setting>
|
||||
<setting id="recurring_tasks_setting" help="Auto-generate tasks for regular activities" groups="project.group_project_recurring_tasks">
|
||||
<field name="allow_recurring_tasks"/>
|
||||
</setting>
|
||||
@@ -463,16 +460,16 @@
|
||||
type="object"
|
||||
t-attf-title="#{record.milestone_count_reached.value} Milestones reached out of #{record.milestone_count.value}"
|
||||
>
|
||||
<span class="fa fa-check-square-o me-1"/>
|
||||
<span class="fa fa-flag me-1"/>
|
||||
<t t-out="record.milestone_count_reached.value"/>/<t t-out="record.milestone_count.value"/>
|
||||
</a>
|
||||
</div>
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="user_id" widget="many2one_avatar_user" domain="[('share', '=', False)]" style="padding-right: 4px;"/>
|
||||
<field t-if="record.last_update_status.value && widget.editable" name="last_update_status" widget="project_state_selection"/>
|
||||
<span t-if="record.last_update_status.value && !widget.editable" t-att-class="'o_status_bubble mx-0 o_color_bubble_' + record.last_update_color.value" t-att-title="record.last_update_status.value"></span>
|
||||
<field name="user_id" widget="many2one_avatar_user" domain="[('share', '=', False)]"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<field name="is_closed"/>
|
||||
<field name="displayed_image_id"/>
|
||||
<field name="active"/>
|
||||
<field name="allow_subtasks"/>
|
||||
<field name="child_text"/>
|
||||
<field name="is_closed"/>
|
||||
<field name="closed_subtask_count"/>
|
||||
<field name="subtask_count"/>
|
||||
<field name="allow_milestones" />
|
||||
<field name="has_late_and_unreached_milestone"/>
|
||||
<progressbar field="state" colors='{"1_done": "success", "03_approved": "success", "02_changes_requested": "warning", "1_canceled": "danger", "04_waiting_normal": "200", "01_in_progress": "200"}'/>
|
||||
@@ -136,7 +136,7 @@
|
||||
<field name="display_parent_task_button" invisible="1"/>
|
||||
<button name="action_project_sharing_view_parent_task" type="object" class="oe_stat_button" icon="fa-tasks" string="Parent Task" attrs="{'invisible': [('display_parent_task_button', '=', False)]}"/>
|
||||
<button name="action_project_sharing_open_subtasks" type="object" class="oe_stat_button" icon="fa-tasks"
|
||||
attrs="{'invisible' : ['|', '|', ('allow_subtasks', '=', False), ('id', '=', False), ('subtask_count', '=', 0)]}" context="{'default_user_ids': [(6, 0, [uid])]}">
|
||||
attrs="{'invisible' : ['|', ('id', '=', False), ('subtask_count', '=', 0)]}" context="{'default_user_ids': [(6, 0, [uid])]}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="subtask_count" widget="statinfo" nolabel="1"/>
|
||||
@@ -174,7 +174,6 @@
|
||||
<field name="parent_id" invisible="1" />
|
||||
<field name="company_id" invisible="1" />
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="allow_subtasks" invisible="1" />
|
||||
<field name="partner_id" options="{'no_open': True, 'no_create': True, 'no_edit': True}" attrs="{'invisible': [('project_id', '=', False)]}"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
</group>
|
||||
@@ -183,7 +182,7 @@
|
||||
<page name="description_page" string="Description">
|
||||
<field name="description" type="html" options="{'collaborative': true}"/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
|
||||
<page name="sub_tasks_page" string="Sub-tasks">
|
||||
<field name="child_ids" context="{'default_project_id': project_id, 'default_parent_id': id, 'default_partner_id': partner_id, 'form_view_ref' : 'project.project_sharing_project_task_view_form'}">
|
||||
<tree editable="bottom">
|
||||
<field name="project_id" invisible="1"/>
|
||||
|
||||
@@ -240,7 +240,6 @@
|
||||
<field eval="2" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Task" class="o_form_project_tasks" js_class="project_task_form">
|
||||
<field name="allow_subtasks" invisible="1" />
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="allow_recurring_tasks" invisible="1" />
|
||||
<field name="recurrence_id" invisible="1" />
|
||||
@@ -278,7 +277,7 @@
|
||||
</button>
|
||||
<!-- Dummy tag used to organize buttons -->
|
||||
<span id="end_rating_buttons" invisible="1"/>
|
||||
<button name="action_open_parent_task" type="object" class="oe_stat_button" icon="fa-tasks" string="Parent Task" attrs="{'invisible': [('parent_id', '=', False)]}" groups="project.group_subtask_project"/>
|
||||
<button name="action_open_parent_task" type="object" class="oe_stat_button" icon="fa-tasks" string="Parent Task" attrs="{'invisible': [('parent_id', '=', False)]}"/>
|
||||
<button name="action_recurring_tasks" type="object" attrs="{'invisible': ['|', ('active', '=', False), ('recurrence_id', '=', False)]}" class="oe_stat_button" icon="fa-repeat" groups="project.group_project_recurring_tasks">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
@@ -289,7 +288,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<button name="%(project_task_action_sub_task)d" type="action" class="oe_stat_button" icon="fa-tasks"
|
||||
attrs="{'invisible' : ['|', '|', ('allow_subtasks', '=', False), ('id', '=', False), ('subtask_count', '=', 0)]}" context="{'default_user_ids': user_ids}">
|
||||
attrs="{'invisible' : ['|', ('id', '=', False), ('subtask_count', '=', 0)]}" context="{'default_user_ids': user_ids}">
|
||||
<div class="o_field_widget o_stat_info">
|
||||
<span class="o_stat_value">
|
||||
<field name="subtask_count" widget="statinfo" nolabel="1"/>
|
||||
@@ -370,7 +369,7 @@
|
||||
<page name="description_page" string="Description">
|
||||
<field name="description" type="html" options="{'collaborative': true, 'resizable': false}" placeholder="Add details about this task..."/>
|
||||
</page>
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('allow_subtasks', '=', False)]}">
|
||||
<page name="sub_tasks_page" string="Sub-tasks" attrs="{'invisible': [('is_private', '=', True)]}">
|
||||
<field name="child_ids"
|
||||
context="{'default_user_ids': user_ids, 'default_parent_id': id,
|
||||
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}"
|
||||
@@ -379,11 +378,11 @@
|
||||
<field name="is_closed" invisible="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="priority" widget="priority" optional="show" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" optional="show" nolabel="1" options="{'hide_label': 1}" />
|
||||
<field name="priority" widget="priority" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" nolabel="1" options="{'hide_label': 1}" />
|
||||
<field name="id" optional="hide"/>
|
||||
<field name="child_text" invisible="1"/>
|
||||
<field name="allow_subtasks" invisible="1"/>
|
||||
<field name="subtask_count" invisible="1"/>
|
||||
<field name="closed_subtask_count" invisible="1"/>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="project_id" string="Project" optional="hide" options="{'no_open': 1}"/>
|
||||
<field name="milestone_id"
|
||||
@@ -396,13 +395,13 @@
|
||||
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="activity_ids" string="Next Activity" widget="list_activity" optional="hide"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="hide"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
|
||||
<field name="rating_last_text" string="Rating" decoration-danger="rating_last_text == 'ko'"
|
||||
decoration-warning="rating_last_text == 'ok'" decoration-success="rating_last_text == 'top'"
|
||||
attrs="{'invisible': [('rating_last_text', '=', 'none')]}"
|
||||
class="fw-bold" widget="badge" optional="hide"/>
|
||||
<field name="stage_id" optional="show" context="{'default_project_id': project_id}" invisible="1"/>
|
||||
class="fw-bold" widget="badge" optional="hide" invisible="1"/>
|
||||
<field name="stage_id" optional="hide" context="{'default_project_id': project_id}"/>
|
||||
<button name="action_open_task" type="object" title="View Task" string="View Task" class="btn btn-link float-end"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -414,10 +413,10 @@
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="parent_id" invisible="1" />
|
||||
<field name="is_closed" invisible="1" />
|
||||
<field name="priority" widget="priority" optional="show" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" optional="show" nolabel="1"/>
|
||||
<field name="child_text" invisible="1"/>
|
||||
<field name="allow_subtasks" invisible="1"/>
|
||||
<field name="priority" widget="priority" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" nolabel="1"/>
|
||||
<field name="subtask_count" invisible="1"/>
|
||||
<field name="closed_subtask_count" invisible="1"/>
|
||||
<field name="name" widget="name_with_subtask_count"/>
|
||||
<field name="id" optional="hide"/>
|
||||
<field name="project_id" optional="hide" options="{'no_open': 1}" />
|
||||
@@ -427,18 +426,18 @@
|
||||
attrs="{'invisible': [('allow_milestones', '=', False)], 'column_invisible': [('parent.allow_milestones', '=', False)]}"
|
||||
/>
|
||||
<field name="partner_id" optional="hide" widget="res_partner_many2one" attrs="{'invisible': [('project_id', '=', False)]}"/>
|
||||
<field name="parent_id" optional="hide" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="parent_id" optional="hide" groups="base.group_no_one"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" optional="show" domain="[('share', '=', False), ('active', '=', True)]"/>
|
||||
<field name="company_id" optional="hide" groups="base.group_multi_company" />
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="activity_ids" string="Next Activity" widget="list_activity" optional="hide"/>
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show" />
|
||||
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="hide" />
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
|
||||
<field name="rating_last_text" string="Rating" decoration-danger="rating_last_text == 'ko'"
|
||||
decoration-warning="rating_last_text == 'ok'" decoration-success="rating_last_text == 'top'"
|
||||
attrs="{'invisible': [('rating_last_text', '=', 'none')]}"
|
||||
class="fw-bold" widget="badge" optional="hide"/>
|
||||
<field name="stage_id" optional="show" invisible="1"/>
|
||||
class="fw-bold" widget="badge" optional="hide" invisible="1"/>
|
||||
<field name="stage_id" optional="hide"/>
|
||||
<button class="oe_link float-end" string="View Task" name="action_open_task" type="object"/>
|
||||
</tree>
|
||||
</field>
|
||||
@@ -446,7 +445,7 @@
|
||||
<page name="extra_info" string="Extra Info" groups="base.group_no_one">
|
||||
<group>
|
||||
<group>
|
||||
<field name="parent_id" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one" context="{'search_view_ref' : 'project.view_task_search_form','search_default_project_id': project_id}"/>
|
||||
<field name="parent_id" groups="base.group_no_one" context="{'search_view_ref' : 'project.view_task_search_form','search_default_project_id': project_id}"/>
|
||||
<field name="analytic_account_id" groups="analytic.group_analytic_accounting" context="{'default_partner_id': partner_id}"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
<field name="sequence" groups="base.group_no_one"/>
|
||||
@@ -531,8 +530,6 @@
|
||||
<field name="activity_state"/>
|
||||
<field name="rating_count"/>
|
||||
<field name="rating_avg"/>
|
||||
<field name="allow_subtasks"/>
|
||||
<field name="child_text"/>
|
||||
<field name="is_private"/>
|
||||
<field name="rating_active"/>
|
||||
<field name="has_late_and_unreached_milestone" />
|
||||
@@ -541,6 +538,8 @@
|
||||
<field name="company_id"/>
|
||||
<field name="recurrence_id" />
|
||||
<field name="subtask_count"/>
|
||||
<field name="closed_subtask_count"/>
|
||||
<field name="subtask_count"/>
|
||||
<progressbar field="state" colors='{"1_done": "success", "03_approved": "success", "02_changes_requested": "warning", "1_canceled": "danger", "01_in_progress": "200", "04_waiting_normal": "200" }'/>
|
||||
<templates>
|
||||
<t t-name="kanban-menu" t-if="!selection_mode" groups="base.group_user">
|
||||
@@ -553,24 +552,31 @@
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
|
||||
<div class="oe_kanban_content">
|
||||
<div class="o_kanban_record_top">
|
||||
<div class="o_kanban_record_headings">
|
||||
<div class="o_kanban_record_top" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<div class="o_kanban_record_headings text-muted">
|
||||
<strong class="o_kanban_record_title">
|
||||
<s t-if="!record.active.raw_value"><field name="name" widget="name_with_subtask_count"/></s>
|
||||
<t t-else=""><field name="name" widget="name_with_subtask_count"/></t>
|
||||
<s t-if="!record.active.raw_value">
|
||||
<field name="name"/>
|
||||
</s>
|
||||
<t t-else="">
|
||||
<field name="name"/>
|
||||
</t>
|
||||
</strong>
|
||||
<span invisible="context.get('default_project_id', False)"><br/><field name="project_id" widget="project_private_task" options="{'no_open': True}"/></span>
|
||||
<span t-if="record.allow_milestones.raw_value and record.milestone_id.raw_value" t-attf-class="{{record.has_late_and_unreached_milestone.raw_value ? 'text-danger' : ''}}">
|
||||
<br/>
|
||||
<span t-if="record.parent_id.raw_value" invisible="context.get('default_parent_id', False)" style="display: block; margin-top: 4px;">
|
||||
<field name="parent_id"/>
|
||||
</span>
|
||||
<span invisible="context.get('default_project_id', False)" style="display: block; margin-top: 4px;">
|
||||
<field name="project_id" options="{'no_open': True}"/>
|
||||
</span>
|
||||
<t t-if="record.partner_id.value">
|
||||
<field name="partner_id" style="display: block; margin-top: 4px;"/>
|
||||
</t>
|
||||
<span t-if="record.allow_milestones.raw_value and record.milestone_id.raw_value" t-att-class="record.has_late_and_unreached_milestone.raw_value ? 'text-danger' : ''" style="display: block; margin-top: 4px;">
|
||||
<field name="milestone_id" options="{'no_open': True}" />
|
||||
</span>
|
||||
<br />
|
||||
<t t-if="record.partner_id.value">
|
||||
<field name="partner_id"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_body">
|
||||
<div class="o_kanban_record_body" t-att-class="record.is_closed.raw_value ? 'opacity-50' : 'text-muted'">
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<div t-if="record.date_deadline.raw_value" name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}">
|
||||
<field name="date_deadline" widget="remaining_days"/>
|
||||
@@ -580,22 +586,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom" t-if="!selection_mode">
|
||||
<div class="oe_kanban_bottom_left">
|
||||
<field name="priority" widget="priority"/>
|
||||
<field name="activity_ids" widget="kanban_activity"/>
|
||||
<div class="oe_kanban_bottom_left" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<field name="priority" widget="priority" style="margin-right: 5px;"/>
|
||||
<field name="activity_ids" widget="kanban_activity" style="padding-top: 1.5px;"/>
|
||||
<b t-if="record.rating_active.raw_value and record.rating_count.raw_value > 0" groups="project.group_project_rating">
|
||||
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-smile-o text-success" t-if="record.rating_avg.raw_value >= 3.66" title="Average Rating: Satisfied" role="img" aria-label="Happy face"/>
|
||||
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-meh-o text-warning" t-elif="record.rating_avg.raw_value >= 2.33" title="Average Rating: Okay" role="img" aria-label="Neutral face"/>
|
||||
<span style="font-weight:bold;" class="fa fa-fw mt4 fa-frown-o text-danger" t-else="" title="Average Rating: Dissatisfied" role="img" aria-label="Sad face"/>
|
||||
<span class="fa fa-fw fa-smile-o text-success rating_face" t-if="record.rating_avg.raw_value >= 3.66" title="Average Rating: Satisfied" role="img" aria-label="Happy face"/>
|
||||
<span class="fa fa-fw fa-meh-o text-warning rating_face" t-elif="record.rating_avg.raw_value >= 2.33" title="Average Rating: Okay" role="img" aria-label="Neutral face"/>
|
||||
<span class="fa fa-fw fa-frown-o text-danger rating_face" t-else="" title="Average Rating: Dissatisfied" role="img" aria-label="Sad face"/>
|
||||
</b>
|
||||
<a t-if="record.subtask_count.raw_value and record.allow_subtasks.raw_value" class="subtask_list_button" title="See Subtasks" style="padding: 0px 5px 0px 0px; box-sizing: border-box; font-size: 20px;"/>
|
||||
<a t-if="!record.project_id.raw_value" class="text-muted" style="font-size: 17px; padding-top: 1.5px;">
|
||||
<i title="Private Task" class="fa fa-lock"/>
|
||||
</a>
|
||||
<t t-if="!record.is_private.raw_value and record.subtask_count.raw_value">
|
||||
<div t-attf-class="{{record.subtask_count.value > record.closed_subtask_count.value}}"></div>
|
||||
<a t-if="!record.is_private.raw_value and record.subtask_count.raw_value and record.subtask_count.value > record.closed_subtask_count.value" class="text-muted">
|
||||
<span t-attf-title="{{ record.closed_subtask_count.value }} sub-tasks closed out of {{ record.subtask_count.value }}" class="subtask_list_button fa fa-check-square-o me-1"/>
|
||||
<t t-out="record.closed_subtask_count.value"/>/<t t-out="record.subtask_count.value"/>
|
||||
</a>
|
||||
<div t-else="" class="text-muted">
|
||||
<span t-attf-title="{{ record.closed_subtask_count.value }} sub-tasks closed out of {{ record.subtask_count.value }}" class="fa fa-check-square-o me-1"/>
|
||||
<t t-out="record.closed_subtask_count.value"/>/<t t-out="record.subtask_count.value"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
|
||||
<div t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
|
||||
<field name="user_ids" widget="many2many_avatar_user" domain="[('share', '=', False)]"/>
|
||||
</div>
|
||||
<field name="state" widget="project_task_state_selection" options="{'hide_label': 1, 'is_toggle_mode': false}"/>
|
||||
<field name="user_ids" widget="many2many_avatar_user" domain="[('share', '=', False)]"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
<t t-if="!record.is_private.raw_value and record.subtask_count.raw_value and record.subtask_count.value > record.closed_subtask_count.value">
|
||||
<div class="kanban_bottom_subtasks_section"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -616,11 +639,11 @@
|
||||
<field name="is_private" invisible="1" />
|
||||
<field name="sequence" invisible="1" readonly="1"/>
|
||||
<field name="allow_milestones" invisible="1"/>
|
||||
<field name="subtask_count" invisible="1"/>
|
||||
<field name="closed_subtask_count" invisible="1"/>
|
||||
<field name="id" optional="hide"/>
|
||||
<field name="priority" widget="priority" optional="show" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" optional="show" nolabel="1" options="{'hide_label': 1, 'is_toggle_mode': false}"/>
|
||||
<field name="child_text" invisible="1"/>
|
||||
<field name="allow_subtasks" invisible="1"/>
|
||||
<field name="priority" widget="priority" nolabel="1"/>
|
||||
<field name="state" widget="project_task_state_selection" nolabel="1" options="{'hide_label': 1, 'is_toggle_mode': false}"/>
|
||||
<field name="name" string="Title" widget="name_with_subtask_count"/>
|
||||
<field name="project_id" widget="project_private_task" optional="show" readonly="1" options="{'no_open': 1}" invisible="context.get('default_project_id')"/>
|
||||
<field name="milestone_id" attrs="{'invisible': [('allow_milestones', '=', False)]}" context="{'default_project_id': project_id}" groups="project.group_project_milestone" optional="hide"/>
|
||||
@@ -629,7 +652,7 @@
|
||||
<field name="company_id" groups="base.group_multi_company" optional="show" invisible="context.get('default_project_id')"/>
|
||||
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
|
||||
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="show" context="{'project_id': project_id}"/>
|
||||
<field name="stage_id" invisible="context.get('set_visible',False)" optional="show" readonly="not context.get('default_project_id')"/>
|
||||
<field name="stage_id" invisible="context.get('set_visible',False)" optional="show"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
@@ -658,7 +681,7 @@
|
||||
<field name="recurrence_id" invisible="1" />
|
||||
</tree>
|
||||
<xpath expr="//field[@name='partner_id']" position="after">
|
||||
<field name="parent_id" optional="hide" context="{'search_view_ref' : 'project.view_task_search_form', 'search_default_project_id': project_id}" attrs="{'invisible': [('allow_subtasks', '=', False)]}" groups="base.group_no_one"/>
|
||||
<field name="parent_id" optional="hide" context="{'search_view_ref': 'project.view_task_search_form', 'search_default_project_id': project_id}" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -809,9 +832,6 @@
|
||||
<tree position="attributes">
|
||||
<attribute name="default_group_by">personal_stage_type_ids</attribute>
|
||||
</tree>
|
||||
<field name="stage_id" position="replace">
|
||||
<field name="stage_display" optional="show" string="Stage"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -827,9 +847,6 @@
|
||||
<field name="project_id" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</field>
|
||||
<field name="stage_id" position="replace">
|
||||
<field name="stage_display" optional="show" string="Stage"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -9,9 +9,6 @@
|
||||
<xpath expr="//form" position="inside">
|
||||
<app data-string="Project" string="Project" name="project" groups="project.group_project_manager">
|
||||
<block title="Tasks Management" id="tasks_management">
|
||||
<setting help="Split your tasks to organize your work into sub-milestones">
|
||||
<field name="group_subtask_project"/>
|
||||
</setting>
|
||||
<setting id="recurring_tasks_setting" help="Auto-generate tasks for regular activities">
|
||||
<field name="group_project_recurring_tasks"/>
|
||||
</setting>
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
<a name="action_view_all_rating" type="object">
|
||||
Customer Ratings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -87,27 +87,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_service_inherit_form2" model="ir.ui.view">
|
||||
<field name="name">sale.service.form.view.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='child_ids']/tree//field[@name='remaining_hours']" position="after">
|
||||
<field name="sale_line_id" invisible="1"/>
|
||||
<field name="remaining_hours_available" invisible="1"/>
|
||||
<field name="remaining_hours_so" widget="timesheet_uom" optional="hide" groups="base.group_user"
|
||||
attrs="{'invisible': ['|', ('sale_line_id', '=', False), ('remaining_hours_available', '=', False)]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='depend_on_ids']/tree//field[@name='remaining_hours']" position="after">
|
||||
<field name="sale_line_id" invisible="1"/>
|
||||
<field name="remaining_hours_available" invisible="1"/>
|
||||
<field name="remaining_hours_so" widget="timesheet_uom" optional="hide" groups="base.group_user"
|
||||
attrs="{'invisible': ['|', ('sale_line_id', '=', False), ('remaining_hours_available', '=', False)]}"/>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_task_tree2_inherited" model="ir.ui.view">
|
||||
<field name="name">project.task.tree.inherited</field>
|
||||
<field name="model">project.task</field>
|
||||
|
||||
Reference in New Issue
Block a user