Skip to content

Commit

Permalink
[REF] project_timeline: Use dedicated fields for timeline planning
Browse files Browse the repository at this point in the history
The fields `date_assign` and `date_end` can't be used, as they are
automatically rewritten on certain flow events (user assignation and
stage changed to finished one), so we need dedicated fields for the
planning. A previous change switches `date_assign` to `date_start`, but
it didn't change demo data, and better to use a consistent naming,
prefixing both fields with `planned_`.

This includes the migration scripts for preserving previous data, and
automations to fill planned data from the previous fields. It also
pre-fills planning information from assignation date/close date as a
best effort pre-planning for existing tasks.

TT50618

Co-Authored-By: Pedro M. Baeza <[email protected]>
  • Loading branch information
juanjosesegui-tecnativa and pedrobaeza committed Sep 10, 2024
1 parent 4ae0a16 commit 31524d3
Show file tree
Hide file tree
Showing 13 changed files with 177 additions and 70 deletions.
7 changes: 6 additions & 1 deletion project_timeline/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ To view the timeline:
* Click on the timeline view icon.
* You will see the tasks or projects in the new view.

The Task timeline uses the Start Date and End Date fields, in the Extra Info tab.
The Task timeline uses the "Planned Start Date" and "Planned End Date" fields, in the
"Extra Info" tab (only visible in debug mode).

When a user is assigned, and there's no planned start date, current datetime is filled
there, and the same happens for the end one when the task is put in a done stage.

Bug Tracker
===========
Expand Down Expand Up @@ -79,6 +83,7 @@ Contributors
* Pedro M. Baeza
* Carlos Dauden
* Alexandre Díaz
* Juan José Seguí

* `Open Source Integrators <https://www.opensourceintegrators.com>`_:

Expand Down
2 changes: 1 addition & 1 deletion project_timeline/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from .hooks import populate_date_start
from .hooks import post_init_hook
from . import models
4 changes: 2 additions & 2 deletions project_timeline/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"name": "Project timeline",
"summary": "Timeline view for projects",
"version": "14.0.1.5.0",
"version": "14.0.2.0.0",
"category": "Project Management",
"website": "https://github.com/OCA/project",
"author": "Tecnativa, Onestein, Odoo Community Association (OCA)",
Expand All @@ -18,5 +18,5 @@
"views/project_task_view.xml",
],
"demo": ["demo/project_project_demo.xml", "demo/project_task_demo.xml"],
"post_init_hook": "populate_date_start",
"post_init_hook": "post_init_hook",
}
76 changes: 38 additions & 38 deletions project_timeline/demo/project_task_demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,191 +4,191 @@
<odoo>
<record id="project.project_task_1" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=13)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() - timedelta(days=6)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_2" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_3" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_4" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=10)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() - timedelta(days=6)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_5" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=9)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_6" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=7)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() - timedelta(days=2)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_7" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=6)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() - timedelta(days=3)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_8" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_9" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() - timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=6)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_10" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=13)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=23)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_11" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=13)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=23)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_12" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_19" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=10)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_20" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=11)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=15)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_21" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=12)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_22" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=22)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_24" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=30)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=32)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_25" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=30)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=35)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
<record id="project.project_task_26" model="project.task">
<field
name="date_assign"
name="planned_date_start"
eval="(DateTime.today() + timedelta(days=20)).strftime('%Y-%m-%d 00:00:00')"
/>
<field
name="date_end"
name="planned_date_end"
eval="(DateTime.today() + timedelta(days=50)).strftime('%Y-%m-%d 00:00:00')"
/>
</record>
Expand Down
19 changes: 11 additions & 8 deletions project_timeline/hooks.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
# Copyright 2021 Open Source Integrators - Daniel Reis
# Copyright 2024 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).


def populate_date_start(cr, registry):
"""
The date_start was introduced to be used instead of date_assign.
To keep same behaviour on upgrade, initialize it
to have the same data as before.
"""
def post_init_hook(cr, registry):
"""Pre-fill planned dates with existing data for having a "best effort" planning."""
cr.execute(
"UPDATE project_task "
"SET date_start = date_assign "
"WHERE date_start IS NULL "
"SET planned_date_start = date_assign "
"WHERE planned_date_start IS NULL "
"AND date_assign IS NOT NULL"
)
cr.execute(
"UPDATE project_task "
"SET planned_date_end = date_end "
"WHERE planned_date_end IS NULL "
"AND date_end IS NOT NULL"
)
16 changes: 16 additions & 0 deletions project_timeline/migrations/14.0.2.0.0/post-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 Tecnativa - Juan José Seguí
# Copyright 2024 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from openupgradelib import openupgrade


@openupgrade.migrate()
def migrate(env, version):
openupgrade.logged_query(
env.cr,
"""UPDATE project_task
SET planned_date_end = date_end
WHERE planned_date_end IS NULL AND date_end IS NOT NULL;
""",
)
11 changes: 11 additions & 0 deletions project_timeline/migrations/14.0.2.0.0/pre-migration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Copyright 2024 Tecnativa - Pedro M. Baeza
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from openupgradelib import openupgrade


@openupgrade.migrate()
def migrate(env, version):
openupgrade.rename_fields(
env, [("project.task", "project_task", "date_start", "planned_date_start")]
)
42 changes: 36 additions & 6 deletions project_timeline/models/project_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,45 @@
# Copyright 2021 Open Source Integrators - Daniel Reis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from odoo import fields, models
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError


class ProjectTask(models.Model):
_inherit = "project.task"

date_start = fields.Datetime("Start Date")
planned_date_start = fields.Datetime(
compute="_compute_planned_date_start",
store=True,
readonly=False,
)
planned_date_end = fields.Datetime(
compute="_compute_planned_date_end",
store=True,
readonly=False,
)

def update_date_end(self, stage_id):
res = super().update_date_end(stage_id)
res.pop("date_end", None)
return res
@api.depends("date_assign")
def _compute_planned_date_start(self):
"""Put the assignation date as the planned start if not other value is
previously set.
"""
for record in self.filtered(
lambda x: not x.planned_date_start and x.date_assign
):
record.planned_date_start = record.date_assign

@api.depends("date_end")
def _compute_planned_date_end(self):
"""Put the done date as the planned end if not other value is previously set."""
for record in self.filtered(lambda x: not x.planned_date_end and x.date_end):
record.planned_date_end = record.date_end

@api.constrains("planned_date_start", "planned_date_end")
def _check_planned_dates(self):
for task in self:
if task.planned_date_start and task.planned_date_end:
if task.planned_date_end < task.planned_date_start:
raise ValidationError(

Check warning on line 45 in project_timeline/models/project_task.py

View check run for this annotation

Codecov / codecov/patch

project_timeline/models/project_task.py#L45

Added line #L45 was not covered by tests
_("The end date must be after the start date.")
)
1 change: 1 addition & 0 deletions project_timeline/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Pedro M. Baeza
* Carlos Dauden
* Alexandre Díaz
* Juan José Seguí

* `Open Source Integrators <https://www.opensourceintegrators.com>`_:

Expand Down
6 changes: 5 additions & 1 deletion project_timeline/readme/USAGE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ To view the timeline:
* Click on the timeline view icon.
* You will see the tasks or projects in the new view.

The Task timeline uses the Start Date and End Date fields, in the Extra Info tab.
The Task timeline uses the "Planned Start Date" and "Planned End Date" fields, in the
"Extra Info" tab (only visible in debug mode).

When a user is assigned, and there's no planned start date, current datetime is filled
there, and the same happens for the end one when the task is put in a done stage.
Loading

0 comments on commit 31524d3

Please sign in to comment.