diff --git a/.travis.yml b/.travis.yml index d512d34..2c2e784 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ env: install: - git clone https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} + - pip install -U pip - travis_install_nightly script: diff --git a/project_task_actual_report/README.rst b/project_task_actual_report/README.rst new file mode 100644 index 0000000..b173232 --- /dev/null +++ b/project_task_actual_report/README.rst @@ -0,0 +1,73 @@ +========================== +Project task actual report +========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproject--reporting-lightgray.png?logo=github + :target: https://github.com/OCA/project-reporting/tree/10.0/project_task_actual_report + :alt: OCA/project-reporting +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/project-reporting-10-0/project-reporting-10-0-project_task_actual_report + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/139/10.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a report showing actual time spent by tasks. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Agile Business Group + +Contributors +~~~~~~~~~~~~ + +* Simone Rubino + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/project-reporting `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/project_task_actual_report/__init__.py b/project_task_actual_report/__init__.py new file mode 100644 index 0000000..72d40fc --- /dev/null +++ b/project_task_actual_report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import reports diff --git a/project_task_actual_report/__manifest__.py b/project_task_actual_report/__manifest__.py new file mode 100644 index 0000000..dd161c1 --- /dev/null +++ b/project_task_actual_report/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Project task actual report", + "summary": "Create a report with actual time spent by tasks.", + "version": "10.0.1.0.0", + "category": "Project Management", + "website": "https://github.com/OCA/project-reporting/tree/" + "10.0/project_task_actual_report", + "author": "Agile Business Group, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": [ + "project", + ], + "data": [ + "security/ir.model.access.csv", + "reports/project_task_actual_report_views.xml", + ], +} diff --git a/project_task_actual_report/i18n/it.po b/project_task_actual_report/i18n/it.po new file mode 100644 index 0000000..e91c9b8 --- /dev/null +++ b/project_task_actual_report/i18n/it.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * project_task_actual_report +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-03-08 14:36+0000\n" +"PO-Revision-Date: 2022-03-08 14:36+0000\n" +"Last-Translator: Simone Rubino \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: project_task_actual_report +#: model:ir.model.fields,help:project_task_actual_report.field_project_task_actual_report_present_kanban_state +msgid "A task's kanban state indicates special situations affecting it:\n" +" * Normal is the default situation\n" +" * Blocked indicates something is preventing the progress of this task\n" +" * Ready for next stage indicates the task is ready to be pulled to the next stage" +msgstr "Lo stato kanban di un'attività indica la situazione speciale in cui si trova:\n" +"* Normale è la situazione standard\n" +"* Bloccato indica che qualcosa previene la progressione di questa attività\n" +"* Pronto per il prossimo stadio indica che l'attività può passare allo stadio successivo" + +#. module: project_task_actual_report +#: model:ir.actions.act_window,name:project_task_actual_report.project_task_actual_report_action +#: model:ir.ui.menu,name:project_task_actual_report.project_task_actual_report_menu +msgid "Actual task time" +msgstr "Tempo reale attività" + +#. module: project_task_actual_report +#: model:ir.model,name:project_task_actual_report.model_project_task_actual_report +msgid "Actual time spent by tasks" +msgstr "Tempo reale impiegato dalle attività" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_date +msgid "Date" +msgstr "Data" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_display_name +msgid "Display Name" +msgstr "Nome Visualizzato" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_duration +msgid "Duration" +msgstr "Durata" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_search +msgid "Group by" +msgstr "Raggruppa per" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_id +msgid "ID" +msgstr "ID" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_kanban_state +msgid "Kanban state" +msgstr "Stato Kanban" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report___last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_message_id +msgid "Message id" +msgstr "Messaggio" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_search +msgid "Month" +msgstr "Mese" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_name +msgid "Name" +msgstr "Nome" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_present_kanban_state +msgid "Present kanban state" +msgstr "Stato Kanban attuale" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_present_stage_id +msgid "Present stage" +msgstr "Stadio attuale" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_present_user_id +msgid "Present user" +msgstr "Utente attuale" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_prev_update +msgid "Previous update" +msgstr "Aggiornamento precedente" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_project_id +msgid "Project" +msgstr "Progetto" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_search +msgid "Stage" +msgstr "Stadio" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_stage_id +msgid "Stage id" +msgstr "Stadio" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_present_name +msgid "Task Title" +msgstr "Titolo Attività" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_form +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_graph +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_pivot +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_tree +msgid "Task changes" +msgstr "Aggiornamenti attività" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_task_id +msgid "Task id" +msgstr "Attività" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_search +msgid "User" +msgstr "Utente" + +#. module: project_task_actual_report +#: model:ir.model.fields,field_description:project_task_actual_report.field_project_task_actual_report_user_id +msgid "User id" +msgstr "Utente" + +#. module: project_task_actual_report +#: model:ir.ui.view,arch_db:project_task_actual_report.project_task_actual_report_view_search +msgid "Year" +msgstr "Anno" diff --git a/project_task_actual_report/readme/CONTRIBUTORS.rst b/project_task_actual_report/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..a200b87 --- /dev/null +++ b/project_task_actual_report/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Simone Rubino diff --git a/project_task_actual_report/readme/DESCRIPTION.rst b/project_task_actual_report/readme/DESCRIPTION.rst new file mode 100644 index 0000000..b717eb2 --- /dev/null +++ b/project_task_actual_report/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module adds a report showing actual time spent by tasks. diff --git a/project_task_actual_report/reports/__init__.py b/project_task_actual_report/reports/__init__.py new file mode 100644 index 0000000..eda530c --- /dev/null +++ b/project_task_actual_report/reports/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import project_task_actual_report diff --git a/project_task_actual_report/reports/project_task_actual_report.py b/project_task_actual_report/reports/project_task_actual_report.py new file mode 100644 index 0000000..302d975 --- /dev/null +++ b/project_task_actual_report/reports/project_task_actual_report.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +# Copyright 2020 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models, tools + + +TRACKED_TASK_FIELDS = [ + 'name', + 'user_id', + 'stage_id', + 'kanban_state', +] +""" +These fields are defined as tracked (see track_visibility attribute) +in the project.task model. +Editing this list automatically +includes new data in the view underlying the below report. +In order to show them to the user, you should create new fields and views. +""" + + +class ProjectTaskActualReport(models.AbstractModel): + _name = 'project.task.actual.report' + _description = "Actual time spent by tasks" + _order = 'date' + _rec_name = 'task_id' + + _depends = {} + task_id = fields.Many2one( + comodel_name='project.task', + readonly=True, + ) + project_id = fields.Many2one( + related='task_id.project_id', + comodel_name='project.project', + readonly=True, + ) + message_id = fields.Many2one( + comodel_name='mail.message', + readonly=True, + ) + date = fields.Datetime( + readonly=True, + ) + prev_update = fields.Datetime( + readonly=True, + string="Previous update", + ) + name = fields.Char( + readonly=True, + ) + present_name = fields.Char( + related='task_id.name', + readonly=True, + ) + user_id = fields.Many2one( + comodel_name='res.users', + readonly=True, + ) + present_user_id = fields.Many2one( + comodel_name='res.users', + related='task_id.user_id', + string="Present user", + readonly=True, + ) + stage_id = fields.Many2one( + comodel_name='project.task.type', + readonly=True, + ) + present_stage_id = fields.Many2one( + comodel_name='project.task.type', + related='task_id.stage_id', + string="Present stage", + readonly=True, + ) + kanban_state = fields.Char( + readonly=True, + ) + present_kanban_state = fields.Selection( + related='task_id.kanban_state', + string="Present kanban state", + readonly=True, + ) + duration = fields.Float() + + @api.model + def _get_real_data_query(self): + """Data gathered from DB.""" + tracked_fields = self._get_tracked_fields() + + select_clause = self._get_real_data_select(tracked_fields) + from_clause = self._get_real_data_from(tracked_fields) + where_clause = self._get_real_data_where(tracked_fields) + return ' '.join([ + select_clause, + from_clause, + where_clause, + ]) + + @api.model + def _get_tracked_fields(self): + """Get the field objects from field names in TRACKED_TASK_FIELDS.""" + task_model = self.env['project.task'] + tracked_fields = {ttf: task_model._fields.get(ttf) + for ttf in TRACKED_TASK_FIELDS} + return tracked_fields + + @api.model + def _get_real_data_where(self, _tracked_fields): + """Where clause for gathering data from DB.""" + where_clause = """where mm.model = 'project.task'""" + return where_clause + + @api.model + def _get_real_data_from(self, tracked_fields): + """From clause for gathering data from DB.""" + field_join_mapping = { + 'always': 'join', + 'onchange': 'left join', + } + from_clause = ["""from + mail_message mm +"""] + for field_name in TRACKED_TASK_FIELDS: + tracked_field = tracked_fields.get(field_name) + track_type = tracked_field.track_visibility + join_type = field_join_mapping.get(track_type) + from_clause.append(""" + {join_type} mail_tracking_value mtv_{field_name} on + mtv_{field_name}.mail_message_id = mm.id + and mtv_{field_name}.field = '{field_name}'""".format( + field_name=field_name, + join_type=join_type, + )) + from_clause = ' '.join(from_clause) + return from_clause + + @api.model + def _get_real_data_select(self, tracked_fields): + """Select clause for gathering data from DB.""" + # See mail.tracking.value.create_tracking_values + field_type_mapping = { + 'many2one': 'integer', + 'selection': 'char', + 'char': 'char', + } + select_clause = ["""select + mm.res_id task_id, + mm.id message_id, + mm."date" date +"""] + for field_name in TRACKED_TASK_FIELDS: + tracked_field = tracked_fields.get(field_name) + field_type = field_type_mapping.get(tracked_field.type) + select_clause.append(""" + mtv_{field_name}.old_value_{field_type} old_{field_name}, + mtv_{field_name}.new_value_{field_type} new_{field_name}""".format( + field_type=field_type, + field_name=field_name, + )) + select_clause = ', '.join(select_clause) + return select_clause + + @api.model + def _get_present_data_query(self): + """Create a record representing present state of every task.""" + select_clause = self._get_present_data_select() + from_clause = self._get_present_data_from() + where_clause = self._get_present_data_where() + return ' '.join([ + select_clause, + from_clause, + where_clause, + ]) + + @api.model + def _get_present_data_select(self): + """Select clause for record representing present state.""" + select_clause = ["""select + pt.id task_id, + ( + select + max(mail_message.id) as max_message_id + from + mail_message) + pt.id message_id, + now() at time zone 'utc' +"""] + for field_name in TRACKED_TASK_FIELDS: + select_clause.append(""" + pt.{field_name}, + null +""".format(field_name=field_name)) + select_clause = ', '.join(select_clause) + return select_clause + + @api.model + def _get_present_data_from(self): + """From clause for record representing present state.""" + from_clause = """from + project_task pt +join mail_message mm on + pt.id = mm.res_id +""" + return from_clause + + @api.model + def _get_present_data_where(self): + """Where clause for record representing present state.""" + where_clause = """where +mm.model = 'project.task' +""" + return where_clause + + @api.model + def _get_data_query(self): + """Query gathering all the data. + + This has a record representing every change occurred in every task, + plus a record representing its present state.""" + return '{real_data} union {present_data}'.format( + real_data=self._get_real_data_query(), + present_data=self._get_present_data_query(), + ) + + @api.model + def _get_partitioned_data_query(self): + """Create a partition for each change. + + This will be used to fill NULL values in the rows.""" + # The underlying idea comes from + # https://stackoverflow.com/a/19012333/11534960 + select_clause = self._get_partitioned_data_select() + from_clause = self._get_partitioned_data_from() + return ' '.join([ + select_clause, + from_clause, + ]) + + @api.model + def _get_partitioned_data_select(self): + """Select clause for partitioned data.""" + select_clause = ["""select + * +"""] + for field_name in TRACKED_TASK_FIELDS: + select_clause.append(""" + count(new_{field_name}) + over (partition by task_id order by message_id) count_new_{field_name} +""".format(field_name=field_name)) + select_clause = ', '.join(select_clause) + return select_clause + + @api.model + def _get_partitioned_data_from(self): + """From clause for partitioned data.""" + from_clause = """from ({data}) data +""".format( + data=self._get_data_query(), + ) + return from_clause + + @api.model + def _get_filled_data_query(self): + """Fill empty values in rows with previous value.""" + return """ +{select_clause} +from ({partitioned_data}) partitioned_data +""".format( + select_clause=self._get_filled_data_select(), + partitioned_data=self._get_partitioned_data_query(), + ) + + def _get_filled_data_select(self): + """ + Select duration of changes using partitions. + + For each tracked field, use a partition over the task id + to record the time passed + from the last change of any other tracked field. + Ordering of partitions is the date of the change. + Some other fields useful for the context + like message_id and task_id are selected. + `message_id` is used as this table's id. + """ + # Second part ot the idea coming from + # https://stackoverflow.com/a/19012333/11534960 + select_clause = ["""select +message_id as id, +task_id, +message_id, +date, +lag(date) over (partition by task_id order by date) as prev_update, +extract(epoch + from (date - lag(date) + over (partition by task_id order by date))) +/ 3600 +as duration + """] + for field_name in TRACKED_TASK_FIELDS: + field_select = """ +case when new_{field_name} <> old_{field_name} -- Change counts as old value +then old_{field_name} +else first_value(new_{field_name}) + over (partition by task_id, count_new_{field_name} order by message_id) +end +"{field_name}" + """.format(field_name=field_name) + select_clause.append(field_select) + select_clause = ', '.join(select_clause) + return select_clause + + @api.model + def _get_cleaned_data_query(self): + """Clan data that are not interesting. + + For instance, the first record always has duration 0 + because there has been no previous event (prev_update is NULL).""" + return """select + * +from ({filled_data}) filled_data +where + prev_update is not null +""".format( + filled_data=self._get_filled_data_query(), + ) + + @api.model_cr + def init(self): + super(ProjectTaskActualReport, self).init() + table_name = self._table + tools.drop_view_if_exists(self.env.cr, table_name) + self.env.cr.execute("""CREATE or REPLACE VIEW + {table_name} as ({cleaned_data})""".format( + table_name=table_name, + cleaned_data=self._get_cleaned_data_query(), + )) diff --git a/project_task_actual_report/reports/project_task_actual_report_views.xml b/project_task_actual_report/reports/project_task_actual_report_views.xml new file mode 100644 index 0000000..31c2f0c --- /dev/null +++ b/project_task_actual_report/reports/project_task_actual_report_views.xml @@ -0,0 +1,107 @@ + + + + + + Pivot view for project.task.actual.report + project.task.actual.report + + + + + + + + + Graph view for project.task.actual.report + project.task.actual.report + + + + + + + + + Tree view for project.task.actual.report + project.task.actual.report + + + + + + + + + + + + + + Search view for project.task.actual.report + project.task.actual.report + + + + + + + + + + + + + + + + + + + + + + + Form view for project.task.actual.report + project.task.actual.report + +
+ + + + + + + + + + + + + + + +
+
+
+ + + + +
diff --git a/project_task_actual_report/security/ir.model.access.csv b/project_task_actual_report/security/ir.model.access.csv new file mode 100644 index 0000000..ae49d4c --- /dev/null +++ b/project_task_actual_report/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_project_task_actual_report,account.invoice.report,model_project_task_actual_report,project.group_project_manager,1,1,1,1 diff --git a/project_task_actual_report/static/description/index.html b/project_task_actual_report/static/description/index.html new file mode 100644 index 0000000..0afce34 --- /dev/null +++ b/project_task_actual_report/static/description/index.html @@ -0,0 +1,419 @@ + + + + + + +Project task actual report + + + +
+

Project task actual report

+ + +

Beta License: AGPL-3 OCA/project-reporting Translate me on Weblate Try me on Runbot

+

This module adds a report showing actual time spent by tasks.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Agile Business Group
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/project-reporting project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/project_task_actual_report/tests/__init__.py b/project_task_actual_report/tests/__init__.py new file mode 100644 index 0000000..01b2b2a --- /dev/null +++ b/project_task_actual_report/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import test_actual_report diff --git a/project_task_actual_report/tests/test_actual_report.py b/project_task_actual_report/tests/test_actual_report.py new file mode 100644 index 0000000..e984b99 --- /dev/null +++ b/project_task_actual_report/tests/test_actual_report.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Simone Rubino - Agile Business Group +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo.fields import Datetime +from odoo.tests import SavepointCase + + +class TestActualReport(SavepointCase): + + def setUp(self): + super(TestActualReport, self).setUp() + self.report_model = self.env['project.task.actual.report'] + task_model = self.env['project.task'] + task_id = task_model.name_create("Test actual report")[0] + self.task = task_model.browse(task_id) + + def test_progress_time(self): + """ + Let a task be in progress for one hour. + + Check that the report shows + that such task has been in progress for one hour. + """ + now = Datetime.from_string(Datetime.now()) + in_progress_time = timedelta(hours=1) + creation_date = now + blocked_date = now + in_progress_time + + # Simulate that task has been created 1 hour ago + creation_message = self.task.message_ids + self.assertTrue(len(creation_message), 1) + self.assertTrue(self.task.kanban_state, 'normal') + creation_message.date = creation_date + + # Simulate that task is `now` blocked + self.task.kanban_state = 'blocked' + blocked_message = self.task.message_ids.filtered( + lambda m: m != creation_message + ) + self.assertEqual(len(blocked_message), 1) + blocked_message.date = blocked_date + + # Check that the task has been blocked for 1 hour + self.assertEqual( + Datetime.from_string(blocked_message.date) + - Datetime.from_string(creation_message.date), + in_progress_time) + + # Find the lines for blocked task + actual_report_lines = self.report_model.search([ + ('task_id', '=', self.task.id), + ]) + self.assertEqual(len(actual_report_lines), 2) + + # Check that there is one line representing the 1-hour progress + kanban_states = dict( + self.task._fields['kanban_state'] + ._description_selection(self.env) + ) + report_line = actual_report_lines.filtered( + lambda rl: rl.kanban_state == kanban_states['normal'] + ) + self.assertEqual(len(report_line), 1) + self.assertAlmostEqual(report_line.duration, 1, places=3)