[IMP] project: improve various mark as done features

New features developed in this PR:

- Write Javascript tests for the project.task.state widget
  - Tests are in the project_task_state_selection.js
- rewrite Research & Development demo data
- Created a digest tip for task state selection (in Settings > technical > digest tips)
  - Tip: Use task state to keep track of the task progression
- Fix Sharingview (Readonly) icons for the project.task.state widget, previously only the color bubble were displayed in readonly sharing views, now all of the icons are displayed
- Display state in the Calendar view
  - Display the same widget as in the kanban/list views
- Remove is_closed References
- Adapt Kanban stages Exemples
  - Kanban exemples are available when creating a new stage in kanban view
- improve Progressbar colors (different color for approved/done, in_progress/waiting)

Task-3213526

closes odoo/odoo#117968

Related: odoo/enterprise#39714
Related: odoo/upgrade#4548
Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com>
This commit is contained in:
Bastien (bvdn)
2023-06-13 18:20:44 +02:00
committed by Xavier Bol (xbo)
parent fc61ea4acb
commit 1a44b84997
23 changed files with 208 additions and 95 deletions
@@ -102,7 +102,7 @@ class Project(models.Model):
WHERE Project.allocated_hours > 0
AND Project.allow_timesheets = TRUE
AND Task.parent_id IS NULL
AND Task.is_closed IS FALSE
AND Task.state NOT IN ('1_done', '1_canceled')
GROUP BY Project.id
HAVING Project.allocated_hours - SUM(Task.effective_hours) < 0
"""
+15
View File
@@ -7,6 +7,21 @@
</data>
<data>
<record id="digest_tip_project_0" model="digest.tip">
<field name="name">Tip: Use task states to keep track of your tasks' progression</field>
<field name="sequence">1200</field>
<field name="group_id" ref="project.group_project_user"/>
<field name="tip_description" type="html">
<div>
<p class="tip_title">Tip: Use task states to keep track of your tasks' progression</p>
<p class="tip_content">
Quickly check the status of tasks for approvals or change requests and identify those on hold until dependencies are resolved with the hourglass icon.
</p>
<img src="https://download.odoocdn.com/digests/project/static/src/img/task-state-img.png" width="720" class="illustration_border" />
</div>
</field>
</record>
<record id="digest_tip_project_1" model="digest.tip">
<field name="name">Tip: Create tasks from incoming emails</field>
<field name="sequence">1300</field>
+8 -6
View File
@@ -725,6 +725,7 @@
<field name="priority">0</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Customer analysis + Architecture</field>
<field name="state">1_done</field>
<field name="color">7</field>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
</record>
@@ -771,9 +772,10 @@
<field name="user_ids" eval="[Command.link(ref('base.user_admin'))]"/>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Basic outline</field>
<field name="state">1_done</field>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_1'))]"/>
<field name="tag_ids" eval="[Command.set([ref('project_tags_02')])]"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_1'))]"/>
</record>
<record id="project_2_task_2_mail_message_1" model="mail.message">
<field name="model">project.task</field>
@@ -818,10 +820,10 @@
<field name="priority">1</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Planning and budget</field>
<field name="stage_id" ref="project_stage_1"/>
<field name="state">1_done</field>
<field name="stage_id" ref="project_stage_2"/>
<field name="color">6</field>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_2'))]"/>
<field name="date_deadline" eval="DateTime.now() - relativedelta(days=4)"/>
</record>
<record id="project_2_task_3_mail_message_1" model="mail.message">
@@ -916,7 +918,6 @@ Send it ASAP, its urgent.</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Social network integration</field>
<field name="description">Facebook and Twitter integration</field>
<field name="state">1_canceled</field>
<field name="stage_id" ref="project_stage_1"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_3'))]"/>
@@ -979,10 +980,11 @@ Send it ASAP, its urgent.</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">New portal system</field>
<field name="priority">0</field>
<field name="state">1_done</field>
<field name="stage_id" ref="project_stage_2"/>
<field name="tag_ids" eval="[Command.set([ref('project.project_tags_02')])]"/>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_3'))]"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_3')), Command.link(ref('project_2_task_2'))]"/>
</record>
<record id="project_2_task_7_mail_message_1" model="mail.message">
<field name="model">project.task</field>
@@ -1060,7 +1062,7 @@ Send it ASAP, its urgent.</field>
<field name="project_id" ref="project.project_project_2"/>
<field name="name">Document management</field>
<field name="stage_id" ref="project_stage_0"/>
<field name="state">1_done</field>
<field name="state">1_canceled</field>
<field name="create_date" eval="DateTime.now() - relativedelta(months=5)"/>
<field name="depend_on_ids" eval="[Command.link(ref('project.project_2_task_8'))]"/>
<field name="date_deadline" eval="DateTime.now() + relativedelta(days=15)"/>
+2 -2
View File
@@ -3,11 +3,11 @@
from . import account_analytic_account
from . import mail_message
from . import project_milestone
from . import project_project_stage
from . import project_task_recurrence
# `project_task_stage_personal` has to be loaded before `project`
# `project_task_stage_personal` has to be loaded before `project_project` and `project_milestone`
from . import project_task_stage_personal
from . import project_milestone
from . import project_project
from . import project_task
from . import project_task_type
+10 -7
View File
@@ -5,6 +5,8 @@ from collections import defaultdict
from odoo import api, fields, models
from .project_task import CLOSED_STATES
class ProjectMilestone(models.Model):
_name = 'project.milestone'
_description = "Project Milestone"
@@ -47,10 +49,10 @@ class ProjectMilestone(models.Model):
@api.depends('task_ids.milestone_id')
def _compute_task_count(self):
all_and_done_task_count_per_milestone = {
milestone.id: (count, is_closed_list.count(True))
for milestone, count, is_closed_list in self.env['project.task']._read_group(
milestone.id: (count, sum(state in CLOSED_STATES for state in state_list))
for milestone, count, state_list in self.env['project.task']._read_group(
[('milestone_id', 'in', self.ids), ('allow_milestones', '=', True)],
['milestone_id'], ['__count', 'is_closed:array_agg'],
['milestone_id'], ['__count', 'state:array_agg'],
)
}
for milestone in self:
@@ -59,19 +61,20 @@ class ProjectMilestone(models.Model):
def _compute_can_be_marked_as_done(self):
if not any(self._ids):
for milestone in self:
milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.is_closed)
milestone.can_be_marked_as_done = not milestone.is_reached and all(milestone.task_ids.mapped(lambda t: t.state in CLOSED_STATES))
return
unreached_milestones = self.filtered(lambda milestone: not milestone.is_reached)
(self - unreached_milestones).can_be_marked_as_done = False
task_read_group = self.env['project.task']._read_group(
[('milestone_id', 'in', unreached_milestones.ids)],
['milestone_id', 'is_closed'],
['milestone_id', 'state'],
['__count'],
)
task_count_per_milestones = defaultdict(lambda: (0, 0))
for milestone, is_closed, count in task_read_group:
for milestone, state, count in task_read_group:
opened_task_count, closed_task_count = task_count_per_milestones[milestone.id]
if is_closed:
if state in CLOSED_STATES:
closed_task_count += count
else:
opened_task_count += count
+3 -3
View File
@@ -11,7 +11,7 @@ from odoo.addons.rating.models import rating_data
from odoo.tools.misc import get_lang
from .project_update import STATUS_COLOR
from .project_task import CLOSED_STATES
class Project(models.Model):
_name = "project.project"
@@ -55,7 +55,7 @@ class Project(models.Model):
for project, count in self.env['project.task'].with_context(
active_test=any(project.active for project in self)
)._read_group(
[('is_closed', '=', False), ('project_id', 'in', self.ids)],
[('state', 'not in', list(CLOSED_STATES)), ('project_id', 'in', self.ids)],
['project_id'],
['__count'],
)
@@ -128,7 +128,7 @@ class Project(models.Model):
type_ids = fields.Many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', string='Tasks Stages')
task_count = fields.Integer(compute='_compute_task_count', string="Task Count")
task_ids = fields.One2many('project.task', 'project_id', string='Tasks',
domain=[('is_closed', '=', False)])
domain=[('state', 'not in', list(CLOSED_STATES))])
color = fields.Integer(string='Color Index')
user_id = fields.Many2one('res.users', string='Project Manager', default=lambda self: self.env.user, tracking=True)
alias_enabled = fields.Boolean(string='Use Email Alias', compute='_compute_alias_enabled', readonly=False)
+1 -3
View File
@@ -50,7 +50,6 @@ PROJECT_TASK_WRITABLE_FIELDS = {
'date_last_stage_update',
'tag_ids',
'sequence',
'is_closed',
'stage_id',
'child_ids',
'parent_id',
@@ -133,7 +132,7 @@ class Task(models.Model):
('03_approved', 'Approved'),
*CLOSED_STATES.items(),
('04_waiting_normal', 'Waiting'),
], string='Status', copy=False, default='01_in_progress', required=True, compute='_compute_state', inverse='_inverse_state', readonly=False, store=True, recursive=True, tracking=True)
], string='State', copy=False, default='01_in_progress', required=True, compute='_compute_state', inverse='_inverse_state', readonly=False, store=True, recursive=True, tracking=True)
create_date = fields.Datetime("Created On", readonly=True)
write_date = fields.Datetime("Last Updated On", readonly=True)
@@ -190,7 +189,6 @@ class Task(models.Model):
# In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
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)]")
subtask_count = fields.Integer("Sub-task Count", compute='_compute_subtask_count')
+1 -4
View File
@@ -42,11 +42,10 @@ class ReportProjectTaskUser(models.Model):
('03_approved', 'Approved'),
('1_canceled', 'Canceled'),
('02_changes_requested', 'Changes Requested'),
], string='Status', readonly=True)
], string='State', readonly=True)
company_id = fields.Many2one('res.company', string='Company', readonly=True)
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
stage_id = fields.Many2one('project.task.type', string='Stage', readonly=True)
is_closed = fields.Boolean("Closing Stage", readonly=True, help="Folded in Kanban stages are closing stages.")
task_id = fields.Many2one('project.task', string='Tasks', readonly=True)
active = fields.Boolean(readonly=True)
tag_ids = fields.Many2many('project.tags', relation='project_tags_project_task_rel',
@@ -81,7 +80,6 @@ class ReportProjectTaskUser(models.Model):
t.partner_id,
t.parent_id,
t.stage_id,
t.is_closed,
t.state,
t.milestone_id,
CASE WHEN pm.id IS NOT NULL THEN true ELSE false END as has_late_and_unreached_milestone,
@@ -113,7 +111,6 @@ class ReportProjectTaskUser(models.Model):
t.partner_id,
t.parent_id,
t.stage_id,
t.is_closed,
t.state,
t.rating_last_value,
t.working_days_close,
@@ -17,7 +17,14 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
date_assign = fields.Datetime(string='Assignment Date', readonly=True)
date_deadline = fields.Date(string='Deadline', readonly=True)
date_last_stage_update = fields.Date(string='Last Stage Update', readonly=True)
is_closed = fields.Boolean("Closing Stage", readonly=True)
state = fields.Selection([
('01_in_progress', 'In Progress'),
('1_done', 'Done'),
('04_waiting_normal', 'Waiting'),
('03_approved', 'Approved'),
('1_canceled', 'Canceled'),
('02_changes_requested', 'Changes Requested'),
], string='State', readonly=True)
milestone_id = fields.Many2one('project.milestone', readonly=True)
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
project_id = fields.Many2one('project.project', readonly=True)
@@ -38,7 +45,7 @@ class ReportProjectTaskBurndownChart(models.AbstractModel):
'date_assign',
'date_deadline',
'date_last_stage_update',
'is_closed',
'state',
'milestone_id',
'partner_id',
'project_id',
@@ -21,8 +21,8 @@
<filter name="filter_date_deadline" date="date_deadline"/>
<filter string="Last Month" invisible="1" name="last_month" domain="[('date','&gt;=', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
<separator/>
<filter string="Open Tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
<filter string="Closed Tasks" name="closed_tasks" domain="[('is_closed', '=', True)]"/>
<filter string="Open Tasks" name="open_tasks" domain="[('state', 'not in', ['1_done', '1_canceled'])]"/>
<filter string="Closed Tasks" name="closed_tasks" domain="[('state', 'in', ['1_done', '1_canceled'])]"/>
<group expand="0" string="Group By">
<filter string="Date" name="date" context="{'group_by': 'date'}" />
<filter string="Stage" name="stage" context="{'group_by': 'stage_id'}" invisible="1"/>
@@ -6,6 +6,7 @@ import {
} from "@web/views/fields/state_selection/state_selection_field";
import { sprintf } from "@web/core/utils/strings";
import { useCommand } from "@web/core/commands/command_hook";
import { formatSelection } from "@web/views/fields/formatters";
import { registry } from "@web/core/registry";
@@ -92,6 +93,14 @@ export class ProjectTaskStateSelection extends StateSelectionField {
return this.options;
}
get label() {
const fullSelection = [...this.options];
fullSelection.push(["04_waiting_normal", "Waiting"]);
return formatSelection(this.currentValue, {
selection: fullSelection,
});
}
stateIcon(value) {
return this.icons[value] || "";
}
@@ -122,14 +131,14 @@ export class ProjectTaskStateSelection extends StateSelectionField {
}
getDropdownPosition() {
if (this.isView(['kanban', 'list']) || this.env.isSmall) {
if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) {
return '';
}
return 'bottom-end';
}
getTogglerClass(currentValue) {
if (this.isView(['kanban', 'list']) || this.env.isSmall) {
if (this.isView(['kanban', 'list', 'calendar']) || this.env.isSmall) {
return 'btn btn-link d-flex p-0';
}
return 'o_state_button btn rounded-pill ' + this.colorButton[currentValue];
@@ -7,9 +7,13 @@
<xpath expr="//t[@t-if='props.readonly']/button/span[1]" position="attributes">
<attribute name="t-attf-class">{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }}</attribute>
</xpath>
<xpath expr="//t[@t-if='props.readonly']/button" position="attributes">
<attribute name="style">cursor: default;</attribute>
<attribute name="t-att-title">label</attribute>
</xpath>
<!-- Waiting state button -->
<xpath expr="//t[@t-if='props.readonly']" position="after">
<t t-elif="currentValue == '04_waiting_normal' and isView(['kanban', 'list'])">
<t t-elif="currentValue == '04_waiting_normal' and isView(['kanban', 'list', 'calendar'])">
<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>
@@ -18,7 +22,7 @@
<!-- 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="isView(['kanban', 'list']) or this.env.isSmall"
<button t-if="isView(['kanban', 'list', 'calendar']) or this.env.isSmall"
class="d-flex align-items-center btn fw-normal p-0"
tabindex="-1"
t-att-class="{'o_task_state_list_view': isView(['list'])}"
@@ -50,7 +54,7 @@
<!-- 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="isView(['kanban', 'list']) or this.env.isSmall" class="d-flex align-items-center" t-att-class="{'o_task_state_list_view': isView(['list'])}" t-att-title="label">
<div t-if="isView(['kanban', 'list', 'calendar']) or this.env.isSmall" class="d-flex align-items-center" t-att-class="{'o_task_state_list_view': isView(['list'])}" t-att-title="label">
<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) }} {{ ['1_done', '1_canceled'].includes(currentValue) and isView(['kanban']) ? 'opacity-50' : '' }}"/>
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -6,7 +6,7 @@ import { renderToMarkup } from '@web/core/utils/render';
const { markup } = owl;
const greenBullet = markup(`<span class="o_status d-inline-block o_status_green"></span>`);
const redBullet = markup(`<span class="o_status d-inline-block o_status_red"></span>`);
const orangeBullet = markup(`<span class="o_status d-inline-block text-warning"></span>`);
const star = markup(`<a style="color: gold;" class="fa fa-star"></a>`);
const clock = markup(`<a class="fa fa-clock-o"></a>`);
@@ -22,7 +22,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.generic");
},
bullets: [greenBullet, redBullet, star],
bullets: [greenBullet, orangeBullet, star],
}, {
name: _lt('Agile Scrum'),
columns: [_lt('Backlog'), _lt('Sprint Backlog'), _lt('Sprint in Progress')],
@@ -30,7 +30,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.agilescrum");
},
bullets: [greenBullet, redBullet],
bullets: [greenBullet, orangeBullet],
}, {
name: _lt('Digital Marketing'),
columns: [_lt('Ideas'), _lt('Researching'), _lt('Writing'), _lt('Editing')],
@@ -38,7 +38,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.digitalmarketing");
},
bullets: [greenBullet, redBullet],
bullets: [greenBullet, orangeBullet],
}, {
name: _lt('Customer Feedback'),
columns: [_lt('New'), _lt('In development')],
@@ -46,7 +46,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.customerfeedback");
},
bullets: [greenBullet, redBullet],
bullets: [greenBullet, orangeBullet],
}, {
name: _lt('Consulting'),
columns: [_lt('New Projects'), _lt('Resources Allocation'), _lt('In Progress')],
@@ -54,7 +54,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.consulting");
},
bullets: [greenBullet, redBullet],
bullets: [greenBullet, orangeBullet],
}, {
name: _lt('Research Project'),
columns: [_lt('Brainstorm'), _lt('Research'), _lt('Draft')],
@@ -62,7 +62,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.researchproject");
},
bullets: [greenBullet, redBullet],
bullets: [greenBullet, orangeBullet],
}, {
name: _lt('Website Redesign'),
columns: [_lt('Page Ideas'), _lt('Copywriting'), _lt('Design')],
@@ -85,7 +85,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.generic");
},
bullets: [greenBullet, redBullet, star, clock],
bullets: [greenBullet, orangeBullet, star, clock],
}, {
name: _lt('Publishing'),
columns: [_lt('Ideas'), _lt('Writing'), _lt('Editing')],
@@ -93,7 +93,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.generic");
},
bullets: [greenBullet, redBullet, star, clock],
bullets: [greenBullet, orangeBullet, star, clock],
}, {
name: _lt('Manufacturing'),
columns: [_lt('New Orders'), _lt('Material Sourcing'), _lt('Manufacturing'), _lt('Assembling')],
@@ -101,7 +101,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.generic");
},
bullets: [greenBullet, redBullet, star, clock],
bullets: [greenBullet, orangeBullet, star, clock],
}, {
name: _lt('Podcast and Video Production'),
columns: [_lt('Research'), _lt('Script'), _lt('Recording'), _lt('Mixing')],
@@ -109,7 +109,7 @@ const exampleData = {
get description() {
return renderToMarkup("project.example.generic");
},
bullets: [greenBullet, redBullet, star, clock],
bullets: [greenBullet, orangeBullet, star, clock],
}],
};
@@ -9,4 +9,9 @@
padding-right: 3px;
font-size: 16px;
}
.o_kanban_header .o_column_progress .bg-success-done {
--bs-bg-opacity: 1;
background-color: rgba(25, 135, 84, var(--bs-bg-opacity)) !important;
}
}
@@ -2,23 +2,31 @@
<templates xml:space="preserve">
<t t-name="project.example.generic" owl="1">
Prioritize Tasks by using the <a style="color: gold;" class="fa fa-star"></a> icon.
Prioritize your tasks by marking important ones using the <a style="color: gold;" class="fa fa-star"></a> button.
<br/><br/>
Use the <span class="o_status d-inline-block o_status_green"></span> state to inform your colleagues that a task is approved for the next stage.
<br/>
Use the <span class="o_status d-inline-block o_status_green"></span> button to inform your colleagues that a task is ready for the next stage.
<br/>
Use the <span class="o_status d-inline-block o_status_red"></span> to indicate a problem or a need for discussion on a task.
Use the <span class="o_status d-inline-block bg-warning"></span> state to indicate a request for changes or a need for discussion on a task.
<br/><br/>
Use the <span class="fa fa-check-circle text-success d-inline-block"></span> state to mark the task as complete.
<br/>
Use the <span class="fa fa-times-circle text-danger d-inline-block"></span> state to mark the task as canceled.
<br/><br/>
Look for the <span class="fa fa-hourglass-o d-inline-block"></span> icon to see tasks waiting on other ones. Once a task is marked as complete or canceled, all of its dependencies will be unblocked.
<br/><br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
</t>
<t t-name="project.example.agilescrum" owl="1">
Use <span class="o_status d-inline-block o_status_green"></span> and <span class="o_status d-inline-block o_status_red"></span> bullets to indicate the status of a task.
Use the <span class="o_status d-inline-block o_status_green"></span> state to inform your colleagues that a task is approved for the next stage.
<br/>
Use the <span class="o_status d-inline-block bg-warning"></span> state to indicate a request for changes or a need for discussion on a task.
<br/><br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
</t>
<t t-name="project.example.digitalmarketing" owl="1">
Everyone can propose ideas, and the Editor marks the best ones as <span class="o_status d-inline-block o_status_green"></span>.
Everyone can propose ideas, and the Editor marks the best ones as <span class="o_status d-inline-block o_status_green"></span>.<br/>
Attach all documents or links to the task directly, to have all research information centralized.
<br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
@@ -26,7 +34,7 @@
<t t-name="project.example.customerfeedback" owl="1">
Customers propose feedbacks by email; Odoo creates tasks automatically, and you can
communicate on the task directly. Your managers decide which feedback is accepted
communicate on the task directly. <br/>Your managers decide which feedback is accepted
<span class="o_status d-inline-block o_status_green"></span> and which feedback is
moved to the "Refused" column.
<br/>
@@ -36,15 +44,15 @@
<t t-name="project.example.consulting" owl="1">
Manage the lifecycle of your project using the kanban view. Add newly acquired projects,
assign them and use the <span class="o_status d-inline-block o_status_green"></span> and
<span class="o_status d-inline-block o_status_red"></span> to define if the project is
<span class="o_status d-inline-block bg-warning"></span> to define if the project is
ready for the next step.
<br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
</t>
<t t-name="project.example.researchproject" owl="1">
Handle your idea gathering within Tasks of your new Project and discuss them in the chatter of the tasks. Use the
<span class="o_status d-inline-block o_status_green"></span> and <span class="o_status d-inline-block o_status_red"></span>
Handle your idea gathering within Tasks of your new Project and discuss them in the chatter of the tasks. <br/>Use the
<span class="o_status d-inline-block o_status_green"></span> and <span class="o_status d-inline-block bg-warning"></span>
to signalize what is the current status of your Idea.
<br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
@@ -52,7 +60,7 @@
<t t-name="project.example.tshirtprinting" owl="1">
Communicate with customers on the task using the email gateway. Attach logo designs to the task, so that information flows from
designers to the workers who print the t-shirt. Organize priorities amongst orders using the
designers to the workers who print the t-shirt. <br/>Organize priorities amongst orders using the
<a style="color: gold;" class="fa fa-star"></a> icon.
<br/>
Use the <a class="fa fa-clock-o"></a> icon to organize your daily activities.
@@ -0,0 +1,67 @@
/** @odoo-module */
import {
click,
getFixture,
} from '@web/../tests/helpers/utils';
import { setupViewRegistries } from "@web/../tests/views/helpers";
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import { addModelNamesToFetch } from '@bus/../tests/helpers/model_definitions_helpers';
addModelNamesToFetch([
'project.project',
'project.task',
]);
let target;
QUnit.module('Task State Tests', {
beforeEach: async function () {
const pyEnv = await startServer();
const projectId = pyEnv['project.project'].create([
{ 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, state: '01_in_progress', user_ids: [userId] },
{ name: 'task two', state: '03_approved' },
{ name: 'task three', state: '04_waiting_normal' },
]);
this.views = {
"project.task,false,kanban":
`<kanban js_class="project_task_kanban">
<templates>
<t t-name="kanban-box">
<div>
<field name="state" widget="project_task_state_selection" class="project_task_state_test"/>
</div>
</t>
</templates>
</kanban>`,
};
target = getFixture();
setupViewRegistries();
}
}, function () {
QUnit.test("Check whether task state widget works as intended", async function (assert) {
const views = this.views;
const { openView } = await start({ serverData: { views } });
await openView({
res_model: "project.task",
views: [[false, "kanban"]],
});
assert.containsNone(target, '.o_field_project_task_state_selection .dropdown-menu', "If the state button has not been pressed yet, no dropdown should be displayed");
await click(target.querySelector('div[name="state"]:first-child button.dropdown-toggle'));
assert.containsOnce(target, '.o_field_project_task_state_selection .dropdown-menu', "Once the button has been pressed the dropdown should appear");
await click(target.querySelector('div[name="state"] .dropdown-menu span.text-danger'));
const times_button = target.querySelector('div[name="state"]:first-child button.dropdown-toggle i.fa-times-circle');
assert.ok(times_button, "If the canceled state as been selected, the fa-times-circle icon should be displayed");
await click(target.querySelector('div[name="state"] i.fa-hourglass-o'));
const waiting_dropdown_menu = target.querySelector('div[name="state"]:nth-of-type(3) button.dropdown-toggle dropdown-menu');
assert.notOk(waiting_dropdown_menu , "When trying to click on the waiting icon, no dropdown menu should display");
});
});
@@ -7,7 +7,7 @@
<template id="portal_my_tasks_state_widget_template" name="Status Widget Template">
<span
t-att-title="dict(task.fields_get(allfields=['state'])['state']['selection'])[task.state]"
t-attf-class="o_status rounded-circle #{'bg-success' if task.state == '1_done' else 'bg-danger' if task.state == '1_canceled' else 'bg-warning' if task.state == '02_changes_requested' else 'bg-success' if task.state == '03_approved' else ''}"
t-attf-class="#{'fa' if task.state in ['1_done','1_canceled','04_waiting_normal'] else 'o_status rounded-circle' } #{'fa-check-circle text-success' if task.state == '1_done' else 'fa-times-circle text-danger' if task.state == '1_canceled' else 'bg-warning' if task.state == '02_changes_requested' else 'bg-success' if task.state == '03_approved' else 'fa-hourglass-o' if task.state == '04_waiting_normal' else ''}"
/>
</template>
@@ -35,10 +35,9 @@
<field name="portal_user_names"/>
<field name="partner_id"/>
<field name="sequence"/>
<field name="is_closed"/>
<field name="state"/>
<field name="displayed_image_id"/>
<field name="active"/>
<field name="is_closed"/>
<field name="closed_subtask_count"/>
<field name="subtask_count"/>
<field name="allow_milestones" />
@@ -72,7 +71,7 @@
</div>
<div class="o_kanban_record_body">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" context="{'project_id': project_id}"/>
<div t-if="record.date_deadline.raw_value" name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}">
<div t-if="record.date_deadline.raw_value" name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}">
<field name="date_deadline" widget="remaining_days"/>
</div>
<div t-if="record.displayed_image_id.value" groups="base.group_user">
@@ -175,9 +174,9 @@
<field name="active" invisible="1"/>
<field name="parent_id" invisible="1" />
<field name="company_id" invisible="1" />
<field name="is_closed" invisible="1" />
<field name="state" 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)]}"/>
<field name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}"/>
</group>
</group>
<notebook>
@@ -188,7 +187,7 @@
<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"/>
<field name="is_closed" invisible="1"/>
<field name="state" invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="priority" widget="priority" optional="show" nolabel="1"/>
<field name="name"/>
@@ -202,7 +201,7 @@
<field name="partner_id" options="{'no_open': True, 'no_create': True, 'no_edit': True}" optional="hide" attrs="{'invisible': [('project_id', '=', False)]}"/>
<field name="user_ids" invisible="1" />
<field name="portal_user_names" string="Assignees" optional="show"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="show"/>
<field name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}" optional="show"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" optional="hide"/>
<field name="stage_id" optional="show"/>
<button name="action_open_task" type="object" title="View Task" string="View Task" class="btn btn-link float-end"
+18 -21
View File
@@ -21,13 +21,13 @@
<filter string="Starred Tasks" name="starred_tasks" domain="[('priority', '=', '1')]"/>
<separator groups="project.group_project_task_dependencies"/>
<filter string="Blocked" name="blocked" domain="[('state', '=', '04_waiting_normal')]" groups="project.group_project_task_dependencies"/>
<filter string="Blocking" name="blocking" domain="[('is_closed', '=', False), ('dependent_ids', '!=', False)]" groups="project.group_project_task_dependencies"/>
<filter string="Blocking" name="blocking" domain="[('state', 'not in', ['1_done','1_canceled']), ('dependent_ids', '!=', False)]" groups="project.group_project_task_dependencies"/>
<separator/>
<filter string="Last Stage Update" name="date_last_stage_update" date="date_last_stage_update"/>
<separator/>
<filter string="Open Tasks" name="open_tasks" domain="[('is_closed', '=', False)]"/>
<filter string="Closed Tasks" name="closed_tasks" domain="[('is_closed', '=', True)]"/>
<filter string="Closed On" name="closed_on" domain="[('is_closed', '=', True)]" date="date_last_stage_update"/>
<filter string="Open Tasks" name="open_tasks" domain="[('state', 'not in', ['1_done','1_canceled'])]"/>
<filter string="Closed Tasks" name="closed_tasks" domain="[('state', 'in', ['1_done','1_canceled'])]"/>
<filter string="Closed On" name="closed_on" domain="[('state', 'in', ['1_done','1_canceled'])]" date="date_last_stage_update"/>
<separator/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
<group expand="0" string="Group By">
@@ -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="is_closed" invisible="1" />
<field name="allow_recurring_tasks" invisible="1" />
<field name="recurrence_id" invisible="1" />
<field name="allow_task_dependencies" invisible="1" />
@@ -374,8 +373,7 @@
context="{'default_user_ids': user_ids, 'default_parent_id': id,
'default_partner_id': partner_id, 'default_milestone_id': allow_milestones and milestone_id}"
widget="subtasks_one2many">
<tree editable="bottom" decoration-muted="is_closed == True">
<field name="is_closed" invisible="1"/>
<tree editable="bottom" decoration-muted="state in ['1_done','1_canceled']">
<field name="allow_milestones" invisible="1"/>
<field name="sequence" widget="handle"/>
<field name="id" optional="hide"/>
@@ -394,7 +392,7 @@
<field name="user_ids" widget="many2many_avatar_user" optional="show" domain="[('share', '=', False), ('active', '=', True)]"/>
<field name="company_id" groups="base.group_multi_company" optional="hide"/>
<field name="company_id" invisible="1"/>
<field name="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="hide"/>
<field name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}" optional="hide"/>
<field name="activity_ids" string="Next Activity" widget="list_activity" optional="hide"/>
<field name="my_activity_date_deadline" string="My Deadline" widget="remaining_days" options="{'allow_order': '1'}" optional="hide"/>
<field name="rating_last_text" string="Rating" decoration-danger="rating_last_text == 'ko'"
@@ -410,10 +408,9 @@
<page name="task_dependencies" string="Blocked By" attrs="{'invisible': [('allow_task_dependencies', '=', False)]}" groups="project.group_project_task_dependencies">
<field name="depend_on_ids" nolabel="1"
context="{'default_project_id': project_id, 'search_view_ref' : 'project.view_task_search_form', 'search_default_project_id': project_id, 'tree_view_ref': 'project.open_view_all_tasks_list_view', 'search_default_open_tasks': 1}">
<tree editable="bottom" decoration-muted="is_closed == True">
<tree editable="bottom" decoration-muted="state in ['1_done','1_canceled']">
<field name="allow_milestones" invisible="1"/>
<field name="parent_id" invisible="1" />
<field name="is_closed" invisible="1" />
<field name="subtask_count" invisible="1"/>
<field name="closed_subtask_count" invisible="1"/>
<field name="id" optional="hide"/>
@@ -431,7 +428,7 @@
<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="date_deadline" attrs="{'invisible': [('is_closed', '=', True)]}" optional="hide" />
<field name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}" optional="hide" />
<field name="activity_ids" string="Next Activity" widget="list_activity" optional="hide"/>
<field name="my_activity_date_deadline" string="My Deadline" widget="remaining_days" options="{'allow_order': '1'}" optional="hide"/>
<field name="rating_last_text" string="Rating" decoration-danger="rating_last_text == 'ko'"
@@ -534,7 +531,6 @@
<field name="user_ids"/>
<field name="partner_id"/>
<field name="sequence"/>
<field name="is_closed"/>
<field name="displayed_image_id"/>
<field name="active"/>
<field name="activity_ids"/>
@@ -551,7 +547,7 @@
<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" }'/>
<progressbar field="state" colors='{"1_done": "success-done", "1_canceled": "danger", "03_approved": "success", "02_changes_requested": "warning", "04_waiting_normal": "info", "01_in_progress": "200" }'/>
<templates>
<t t-name="kanban-menu" t-if="!selection_mode" groups="base.group_user">
<a t-if="widget.editable" role="menuitem" type="set_cover" class="dropdown-item" data-field="displayed_image_id">Set Cover Image</a>
@@ -563,7 +559,7 @@
<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" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div class="o_kanban_record_top" t-att-class="['1_done', '1_canceled'].includes(record.state.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">
@@ -587,9 +583,9 @@
</span>
</div>
</div>
<div class="o_kanban_record_body" t-att-class="record.is_closed.raw_value ? 'opacity-50' : 'text-muted'">
<div class="o_kanban_record_body" t-att-class="['1_done', '1_canceled'].includes(record.state.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)]}">
<div t-if="record.date_deadline.raw_value" name="date_deadline" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}">
<field name="date_deadline" widget="remaining_days"/>
</div>
<div t-if="record.displayed_image_id.value">
@@ -597,7 +593,7 @@
</div>
</div>
<div class="o_kanban_record_bottom" t-if="!selection_mode">
<div class="oe_kanban_bottom_left" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div class="oe_kanban_bottom_left" t-att-class="['1_done', '1_canceled'].includes(record.state.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 &gt; 0" groups="project.group_project_rating">
@@ -622,7 +618,7 @@
</t>
</div>
<div class="oe_kanban_bottom_right" t-if="!selection_mode">
<div t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div t-att-class="['1_done', '1_canceled'].includes(record.state.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}"/>
@@ -646,7 +642,7 @@
<field name="model">project.task</field>
<field name="arch" type="xml">
<tree string="Tasks" sample="1" default_order="priority desc, state, date_deadline asc, sequence, id desc">
<field name="is_closed" invisible="1" />
<field name="company_id" invisible="1"/>
<field name="is_private" invisible="1" />
<field name="sequence" invisible="1" readonly="1"/>
<field name="allow_milestones" invisible="1"/>
@@ -662,7 +658,7 @@
<field name="user_ids" optional="show" widget="many2many_avatar_user" domain="[('share', '=', False), ('active', '=', True)]"/>
<field name="company_id" groups="base.group_multi_company" optional="show" invisible="context.get('default_project_id')"/>
<field name="company_id" invisible="1"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('is_closed', '=', True)]}"/>
<field name="date_deadline" optional="hide" widget="remaining_days" attrs="{'invisible': [('state', 'in', ['1_done','1_canceled'])]}"/>
<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"/>
</tree>
@@ -732,6 +728,7 @@
<field name="date_deadline"/>
<field name="tag_ids" widget="many2many_tags" attrs="{'invisible': [('tag_ids', '=', [])]}"/>
<field name="stage_id"/>
<field name="state" widget="project_task_state_selection" readonly="1"/>
</calendar>
</field>
</record>
@@ -1000,7 +997,7 @@
<field name="name">Overpassed Tasks</field>
<field name="res_model">project.task</field>
<field name="view_mode">tree,form,calendar,graph,kanban</field>
<field name="domain">[('is_closed', '=', False), ('date_deadline','&lt;',time.strftime('%Y-%m-%d')), ('project_id', '!=', False)]</field>
<field name="domain">[('state', 'not in', ['1_done', '1_canceled']), ('date_deadline','&lt;',time.strftime('%Y-%m-%d')), ('project_id', '!=', False)]</field>
<field name="filter" eval="True"/>
<field name="search_view_id" ref="view_task_search_form"/>
</record>
@@ -22,7 +22,7 @@
<field name="message_partner_ids"/>
<field name="activity_ids" />
<field name="activity_state" />
<field name="is_closed"/>
<field name="state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
<templates>
<t t-name="kanban-menu">
@@ -39,7 +39,7 @@
<div>
<field name="state" widget="todo_done_checkmark" options="{'hide_label': 1}"/>
</div>
<div t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div t-att-class="['1_done', '1_canceled'].includes(record.state.raw_value) ? 'opacity-50' : ''">
<strong class="o_kanban_record_title align-middle">
<field name="name" widget="name_with_subtask_count"/>
</strong>
@@ -47,12 +47,12 @@
</div>
</div>
</div>
<div class="o_kanban_record_body o_todo_kanban_card_body" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div class="o_kanban_record_body o_todo_kanban_card_body" t-att-class="['1_done', '1_canceled'].includes(record.state.raw_value) ? 'opacity-50' : ''">
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left"/>
<div class="oe_kanban_bottom_right" t-att-class="record.is_closed.raw_value ? 'opacity-50' : ''">
<div class="oe_kanban_bottom_right" t-att-class="['1_done', '1_canceled'].includes(record.state.raw_value) ? 'opacity-50' : ''">
<div t-attf-class="d-flex #{todoHasAssignees ? 'w-100 align-items-center justify-content-end' : 'align-items-end'}">
<div class="d-flex align-items-center me-2 mt-2">
<div t-if="todoHasAssignees">
@@ -215,9 +215,9 @@
<field name="tag_ids"/>
<field name="user_ids"/>
<field name="personal_stage_type_ids" string="Stage"/>
<filter name="open_tasks" string="Open" domain="[('is_closed', '=', False)]"/>
<filter name="closed_tasks" string="Closed" domain="[('is_closed', '=', True)]"/>
<filter string="Closed On" name="closed_on" domain="[('is_closed', '=', True)]" date="date_last_stage_update"/>
<filter name="open_tasks" string="Open" domain="[('state', 'not in', ['1_done','1_canceled'])]"/>
<filter name="closed_tasks" string="Closed" domain="[('state', 'in', ['1_done','1_canceled'])]"/>
<filter string="Closed On" name="closed_on" domain="[('state', 'in', ['1_done','1_canceled'])]" date="date_last_stage_update"/>
<separator/>
<filter name="active_false" string="Archived" domain="[('active', '=', False)]"/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
+4 -2
View File
@@ -11,6 +11,8 @@ from odoo.tools import Query
from datetime import date
from functools import reduce
from odoo.addons.project.models.project_task import CLOSED_STATES
class Project(models.Model):
_inherit = 'project.project'
@@ -86,7 +88,7 @@ class Project(models.Model):
@api.depends('sale_order_id', 'task_ids.sale_order_id')
def _compute_sale_order_count(self):
sale_order_items_per_project_id = self._fetch_sale_order_items_per_project_id({'project.task': [('is_closed', '=', False)]})
sale_order_items_per_project_id = self._fetch_sale_order_items_per_project_id({'project.task': [('state', 'not in', list(CLOSED_STATES))]})
for project in self:
project.sale_order_count = len(sale_order_items_per_project_id.get(project.id, self.env['sale.order.line']).order_id)
@@ -107,7 +109,7 @@ class Project(models.Model):
def action_view_sos(self):
self.ensure_one()
all_sale_orders = self._fetch_sale_order_items({'project.task': [('is_closed', '=', False)]}).order_id
all_sale_orders = self._fetch_sale_order_items({'project.task': [('state', 'not in', list(CLOSED_STATES))]}).order_id
action_window = {
"type": "ir.actions.act_window",
"res_model": "sale.order",