diff --git a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
index 5d4f30c94..bc365ef89 100644
--- a/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
+++ b/addons_extensions/hr_emp_dashboard/static/src/js/profile_component.js
@@ -373,8 +373,8 @@ this.state.attendance_lines = groupedAttendance;
line.color = 'red';
break;
default:
- line.state = 'Refused';
- line.color = 'red';
+ line.state = 'Draft';
+ line.color = '#6871f2';
break;
}
});
diff --git a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
index 3fcad56b0..adb3b4a34 100644
--- a/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
+++ b/addons_extensions/hr_emp_dashboard/static/src/xml/employee_profile_template.xml
@@ -186,11 +186,11 @@
Attendance
-
+
+
+
+
+
@@ -280,11 +280,11 @@
Expenses
-
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/__init__.py b/addons_extensions/web_gantt/__init__.py
new file mode 100644
index 000000000..dc5e6b693
--- /dev/null
+++ b/addons_extensions/web_gantt/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons_extensions/web_gantt/__manifest__.py b/addons_extensions/web_gantt/__manifest__.py
new file mode 100644
index 000000000..ffb3c5f9b
--- /dev/null
+++ b/addons_extensions/web_gantt/__manifest__.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+{
+ 'name': 'Web Gantt',
+ 'category': 'Hidden',
+ 'description': """
+Odoo Web Gantt chart view.
+=============================
+
+ """,
+ 'version': '2.0',
+ 'depends': ['web'],
+ 'assets': {
+ 'web._assets_primary_variables': [
+ 'web_gantt/static/src/gantt_view.variables.scss',
+ ],
+ 'web.assets_backend_lazy': [
+ 'web_gantt/static/src/**/*',
+
+ # Don't include dark mode files in light mode
+ ('remove', 'web_gantt/static/src/**/*.dark.scss'),
+ ],
+ 'web.assets_backend_lazy_dark': [
+ 'web_gantt/static/src/**/*.dark.scss',
+ ],
+ 'web.assets_unit_tests': [
+ 'web_gantt/static/tests/**/*',
+ ],
+
+ },
+ 'auto_install': True,
+}
diff --git a/addons_extensions/web_gantt/i18n/ar.po b/addons_extensions/web_gantt/i18n/ar.po
new file mode 100644
index 000000000..24853303f
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ar.po
@@ -0,0 +1,363 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Arabic (https://app.transifex.com/odoo/teams/41243/ar/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ar\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s ساعات و%(minute)s "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "لا يمكن جدولة %s في الماضي "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s ساعات "
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "عرض نافذة الإجراء"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "تطبيق"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "هل أنت متأكد من أنك ترغب في حذف هذا السجل؟ "
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "قاعدة "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "تصغير الصفوف "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "إنشاء"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "تحرير"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "تكبير الصفوف "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "ركّز اليوم "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "من"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "جانت"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "أداة عرض جانت"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "يمكن أن يكون تابع غانت إما حقل أو قالب فقط، به %s "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "يجب أن يكون لعرض غانت خاصية 'date_start' "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "يجب أن يكون لعرض غانت خاصية 'date_stop' "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"يجب أن يكون لنافذة عرض غانت خاصية 'dependency_inverted_field' بمجرد أن يتم "
+"تحديد 'dependency_field' "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "تاريخ بدء غانت "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "تاريخ انتهاء غانت "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "يمكن أن يحتوي عرض غانت على علامة تصنيف قوالب واحدة فقط "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "لا يمكن القيام بالجدولة في الماضي. "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "لا توجد حقول كافية لعرض غانت! "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"خواص غير صالحة (%(invalid_attributes)s) في عرض غانت. يجب أن تكون الخواص في "
+"(%(valid_attributes)s) "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' غير صالح في عرض غانت "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "default_scale '%s' غير صالح في عرض غانت "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode '%s' في نافذة عرض غانت غير صالح "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "الاسم"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "جديد"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "فتح"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "الخطة "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "بدء"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "إيقاف"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "التبعيات غير صالحة. لقد تسببت في إحداث دائرة مغلقة. "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "لا يوجد مرشحون صالحون لإعادة التخطيط "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "هذا الشهر"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "ربع السنة هذا "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "هذا الأسبوع "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "هذا العام"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "اليوم "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "قائمة شريط الأدوات "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "الإجمالي"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "غير محدد %s "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "أداة العرض"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "نوع واجهة العرض"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "لا يمكنك تحريك %(record)s نحو %(related_record)s. "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "لا يمكنك إعادة جدولة %(main_record)s نحو %(other_record)s. "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "ساعات"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "دقائق "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "شهور"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "إلى"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/bg.po b/addons_extensions/web_gantt/i18n/bg.po
new file mode 100644
index 000000000..ea290d022
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/bg.po
@@ -0,0 +1,367 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Petko Karamotchev, 2024
+# Martin Trigaux, 2024
+# Rosen Vladimirov , 2024
+# Boyan Rabchev , 2024
+# Rumena Georgieva , 2024
+# Vladimir Petrov , 2024
+# KeyVillage, 2024
+# Albena Mincheva , 2024
+# Maria Boyadjieva , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Maria Boyadjieva , 2024\n"
+"Language-Team: Bulgarian (https://app.transifex.com/odoo/teams/41243/bg/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: bg\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Приложи"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Основа"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Създай"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Редактирай"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "От"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Диаграма на Гант"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Изглед на диаграма на Гант"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Име"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Нов"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Отворен"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Планирайте"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Стартирайте"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Край"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Този месец"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Тази седмица"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Днес"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Обща стойност"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Преглед"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Вид изглед"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "часове"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "минути"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "месеци"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "до"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/ca.po b/addons_extensions/web_gantt/i18n/ca.po
new file mode 100644
index 000000000..3c70de6eb
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ca.po
@@ -0,0 +1,371 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Arnau Ros, 2024
+# Ivan Espinola, 2024
+# jabiri7, 2024
+# Sandra Franch , 2024
+# Josep Anton Belchi, 2024
+# marcescu, 2024
+# Martin Trigaux, 2024
+# Quim - eccit , 2024
+# RGB Consulting , 2024
+# Eric Antones , 2024
+# Manel Fernandez Ramirez , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Manel Fernandez Ramirez , 2024\n"
+"Language-Team: Catalan (https://app.transifex.com/odoo/teams/41243/ca/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ca\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Vista de la finestra d'acció"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Aplica"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "¿Està segur d'eliminar aquest registre?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Col·lapsar files"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Crear"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Modificar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Expandeix les files"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Des de"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gràfic de Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vista Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "El fill de Gantt només pot ser un camp o una plantilla, té %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt ha de tenir un atribut 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt ha de tenir un atribut \"date_stop\"."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt ha de tenir un atribut 'dependency_inverted_field' una vegada s'hagi "
+"especificat 'dependency_field' "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "La vista de Gantt només pot contenir una etiqueta de plantilles"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "¡Camps insuficients per a la vista de Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Escala_per defecte invàlida '%s'a gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nom"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nou"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Oberts"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Pla"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Inicia"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Atura"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Aquest mes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Aquesta setmana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Aquest any"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Avui"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "No definit %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vista"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tipus de vista"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "hores"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuts"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "mesos"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "fins"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/cs.po b/addons_extensions/web_gantt/i18n/cs.po
new file mode 100644
index 000000000..7d061f3af
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/cs.po
@@ -0,0 +1,361 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Czech (https://app.transifex.com/odoo/teams/41243/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: cs\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Akce okna zobrazení"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Použít"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Opravdu chcete tento záznam smazat?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Jádro"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Sbalit řádky"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Vytvořit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Upravit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Rozbalte řádky"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Od"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantův diagram"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Ganttův potomek může být pouze pole nebo šablona, dostal %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt musí mít atribut 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt musí mít atribut 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt musí mít atribut 'dependency_inverted_field', jakmile je specifikováno"
+" 'dependency_field'"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Zobrazení Gantt může obsahovat pouze jeden štítek šablony"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Nedostatečná pole pro zobrazení Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Neplatný default_scale '%s' v gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Název"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nové"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Volný"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plán"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Začít"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Zastavit"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Tento měsíc"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Tento týden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Tento rok"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Dnes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Celkem"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Nedefinováno %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Zobrazení"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Typ zobrazení"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "hodin"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minut"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "měsíce"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "k"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/da.po b/addons_extensions/web_gantt/i18n/da.po
new file mode 100644
index 000000000..ba88c8f8b
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/da.po
@@ -0,0 +1,264 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Sanne Kristensen , 2022
+# Mads Søndergaard, 2022
+# Mads Søndergaard, 2022
+# Martin Trigaux, 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2022-09-22 05:49+0000\n"
+"Last-Translator: Martin Trigaux, 2023\n"
+"Language-Team: Danish (https://app.transifex.com/odoo/teams/41243/da/)\n"
+"Language: da\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Er du sikker på du vil slette dette datasæt?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Basis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr "Sammenfald rækker"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Opret"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Dag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr "Udvid rækker"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt visning"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantt arving kan kun være et felt eller en skabelon, fik %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt skal have en 'date_start' egenskab"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt skal have en 'date_stop' egenskab"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Gantt visning kan kun indeholde én skabelons tag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr "Gruppering per dato er ikke understøttet"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Utilstrækkelig felter til Gantt visning!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Ugyldig default_scale '%s' i gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Måned"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Ny"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Næste"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Åben"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planlæg"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr "Plan eksisterer"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Forrige"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "I dag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Udefineret %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Uge"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "År"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/de.po b/addons_extensions/web_gantt/i18n/de.po
new file mode 100644
index 000000000..793a843f4
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/de.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Larissa Manderfeld, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Larissa Manderfeld, 2024\n"
+"Language-Team: German (https://app.transifex.com/odoo/teams/41243/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sSt.%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s kann nicht in der Vergangenheit liegen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sSt."
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Ansicht des Aktionsfensters"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Dense-Modus aktivieren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Sparse-Modus aktivieren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Anwenden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Sind Sie sicher, dass Sie diesen Datensatz löschen möchten?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Basis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Zeilen einklappen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Erstellen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Bearbeiten"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Zeilen erweitern"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Heute fokussieren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Von"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt-Ansicht"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Untergeordnetes Gantt kann nur Feld oder Vorlage sein, %s erhalten"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt muss ein „date_start“-Attribut haben"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt muss ein „date_stop“-Attribut haben"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt muss ein „dependency_inverted_field“-Attribut haben, sobald das "
+"„dependency_field“ bestimmt wurde"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Gantt-Startdatum"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Gantt-Enddatum"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Gantt-Ansicht kann nur ein Vorlagen-Stichwort beinhalten"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Kann nicht in der Vergangenheit liegen"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Unzureichende Felder für die Gantt-Ansicht!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Ungültige Attribute (%(invalid_attributes)s) in der Gantt-Ansicht. Attribute"
+" müssen in (%(valid_attributes)s) stehen."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "Ungültige default_range „%s“ in Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Ungültige default_scale „%s“ in Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "Ungültiger display_mode „%s“ in Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Namen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Neu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Öffnen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planen"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Verschiebung erfolgreich abgeschlossen."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Loslegen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Stopp"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Die Abhängigkeiten sind nicht gültig, es gibt einen Kreis."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Es sind keine gültigen Kandidaten neu zu planen."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Diesen Monat"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Dieses Quartal"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Diese Woche"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Dieses Jahr"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Heute"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Symbolleistenmenü"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Gesamt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s Undefiniert"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Ansicht"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Ansichtstyp"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Sie können %(record)s nicht nach %(related_record)s verschieben."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "Sie können %(main_record)s nicht nach %(other_record)s verschieben."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "Stunden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "Minuten"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "Monate"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "bis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/el.po b/addons_extensions/web_gantt/i18n/el.po
new file mode 100644
index 000000000..6a83b6cbb
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/el.po
@@ -0,0 +1,263 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Martin Trigaux, 2019
+# Kostas Goutoudis , 2019
+# Giota Dandidou , 2019
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server saas~12.2+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2016-08-05 13:32+0000\n"
+"Last-Translator: Giota Dandidou , 2019\n"
+"Language-Team: Greek (https://www.transifex.com/odoo/teams/41243/el/)\n"
+"Language: el\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Βάση"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Δημιουργία"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Ημέρα"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Προβολή Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Μήνας"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Επόμενο"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Ανοιχτό"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Σχεδίασε"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Προηγούμενο"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "Σήμερα"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Εβδομάδα"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "Έτος"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/es.po b/addons_extensions/web_gantt/i18n/es.po
new file mode 100644
index 000000000..04a755514
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/es.po
@@ -0,0 +1,363 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Spanish (https://app.transifex.com/odoo/teams/41243/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: es\n"
+"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s h %(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s no se puede programar en el pasado"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s h"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Vista de la ventana de acción"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Activar modo denso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Activar modo disperso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Aplicar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "¿Está seguro de que desea eliminar este registro?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Ocultar filas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Crear"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Editar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Expandir filas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Enfoque de hoy"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Desde"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vista Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantt hijo solo puede ser campo o plantilla, obtuvo %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt debe tener un atributo \"date_start\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt debe tener un atributo \"date_stop\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt debe tener un atributo 'dependency_inverted_field' cuando se "
+"especifique el campo 'dependency_field'."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Fecha de inicio de Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Fecha de finalización de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "La vista de Gantt solo puede contener una etiqueta de plantilla"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "No es posible programar en el pasado."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "¡Campos insuficientes para la vista de Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Atributos no válidos (%(invalid_attributes)s) en la vista gantt. Los "
+"atributos deben estar en (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' no válido en Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Default_scale '%s' no válida en vista de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode \"%s\" no válido en gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nombre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nuevo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Abrir"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planificar"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Iniciar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Detener"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Las dependencias no son válidas, hay un ciclo."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "No hay candidatos válidos para replanificar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Este mes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Este trimestre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Esta semana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Este año"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Hoy"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menú de barra de herramientas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s sin definir"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vista"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tipo de vista"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "No puede mover %(record)s a %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "No puede reprogramar %(main_record)s a %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "horas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minutos"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "meses"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "a"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/es_419.po b/addons_extensions/web_gantt/i18n/es_419.po
new file mode 100644
index 000000000..e3126e757
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/es_419.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Patricia Gutiérrez Capetillo , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Patricia Gutiérrez Capetillo , 2024\n"
+"Language-Team: Spanish (Latin America) (https://app.transifex.com/odoo/teams/41243/es_419/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: es_419\n"
+"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s no se puede programar en el pasado"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Vista de acción de ventana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Activar modo denso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Activar modo disperso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Aplicar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "¿Está seguro de que desea eliminar este registro?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Contraer filas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Crear"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Editar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Expandir filas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Importantes de hoy"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Desde"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vista de gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantt secundario solo puede ser un campo o plantilla, obtuvo %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt debe tener un atributo \"date_start\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt debe tener un atributo \"date_stopt\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt debe tener un atributo 'dependency_inverted_field' cuando se "
+"especifique el campo 'dependency_field'."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Fecha de inicio de Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Fecha de finalización de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "La vista de Gantt solo puede contener una etiqueta de plantilla"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "No es posible programar en el pasado."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Campos insuficientes para la vista de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Atributos inválidos (%(invalid_attributes)s) en la vista de Gantt. Los "
+"atributos deben estar en (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' no válido en Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Default_scale '%s' no válida en vista gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode \"%s\" inválido en la vista de Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nombre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nuevo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Abierto"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Reprogramación realizada con éxito."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Iniciar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Detener"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Las dependencias no son válidas, hay un ciclo."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "No hay candidatos válidos para replanificar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Este mes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Este trimestre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Esta semana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Este año"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Hoy"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menú de barra de herramientas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s sin definir"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vista"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Ver tipo"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "No puede mover %(record)s a %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "No puede mover %(main_record)s a %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "horas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minutos"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "meses"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "para"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/et.po b/addons_extensions/web_gantt/i18n/et.po
new file mode 100644
index 000000000..89a7bc3c5
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/et.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Siim Raasuke, 2024
+# Stevin Lilla, 2024
+# Martin Trigaux, 2024
+# Eneli Õigus , 2024
+# Anna, 2024
+# Triine Aavik , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Triine Aavik , 2024\n"
+"Language-Team: Estonian (https://app.transifex.com/odoo/teams/41243/et/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: et\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)stundi%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%stund"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Toimingu akna vaade"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Kinnita"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Kas oled kindel, et soovid antud kirjet kustutada?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Baas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "peida read"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Loo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Muuda"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Laienda ridu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Kellelt?"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantti vaade"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Vigane default_scale '%s' ganti vaates"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nimi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Uus"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Avatud"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plaan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Alusta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Peata"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "See kuu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "See nädal"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "See aasta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Täna"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Kokku"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Määramata %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vaade"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Vaate tüüp"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "tundi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minutit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "kuud"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "kuni"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/fi.po b/addons_extensions/web_gantt/i18n/fi.po
new file mode 100644
index 000000000..3ef0a5273
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/fi.po
@@ -0,0 +1,367 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Tiffany Chang, 2024
+# Miku Laitinen , 2024
+# Jarmo Kortetjärvi , 2024
+# Mikko Salmela , 2024
+# Ossi Mantylahti , 2024
+# Tuomo Aura , 2024
+# Martin Trigaux, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Martin Trigaux, 2024\n"
+"Language-Team: Finnish (https://app.transifex.com/odoo/teams/41243/fi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: fi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Toimintoikkunan näkymä"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Käytä"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Oletko varma, että haluat poistaa tämän tietueen?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Pohja"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Supista rivit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Luo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Muokkaa"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Laajenna rivit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Alkaa"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt-näkymä"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantt-kaavion alataso voi olla vain kenttä tai malli, saatiin %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Ganttissa on oltava attribuutti 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Ganttissa on oltava attribuutti 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Ganttissa on oltava attribuutti 'dependency_inverted_field', kun "
+"'dependency_field' on määritetty"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Gantt-näkymä voi sisältää vain yhden mallitunnisteen"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Kenttiä ei ole tarpeeksi Gantt-näkymään!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Ganttissa on virheellinen default_scale '%s'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nimi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Uusi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Avoin"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Suunnittele"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Aloitus"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Lopeta"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Tässä kuussa"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Tällä viikolla"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Tämä vuosi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Tänään"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Yhteensä"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Määrittelemätön %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Näytä"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Näkymän tyyppi"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "tunti(a)"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuutit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "kuukaudet"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "->"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/fr.po b/addons_extensions/web_gantt/i18n/fr.po
new file mode 100644
index 000000000..76f023e84
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/fr.po
@@ -0,0 +1,366 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: French (https://app.transifex.com/odoo/teams/41243/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: fr\n"
+"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s ne peut pas être planifié dans le passé"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Vue de la fenêtre d'action"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Activer le mode dense"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Activer le mode clairsemé"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Appliquer"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Êtes-vous sûr de vouloir supprimer cet enregistrement ?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Plier les lignes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Créer"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Modifier"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Déplier les lignes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Cibler la date d'aujourd'hui"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "À partir de"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vue de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+"Une ligne enfant Gantt peut seulement être un champ ou un modèle, vous avez "
+"%s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt doit avoir un attribut 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt doit avoir un attribut 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gant doit avoir un attribut 'dependency_inverted_field' dès que le "
+"'dependency_field' est défini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Date de début Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Date de fin Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "La vue Gantt peut uniquement contenir une étiquette de modèles"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Impossible de planifier dans le passé."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Champs insuffisants pour la vue Gantt !"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Des attributs invalides se trouvent (%(invalid_attributes)s) dans la vue "
+"Gantt. Les attributs doivent être en (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' invalide dans gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "default_scale '%s' invalide dans Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "Display_mode '%s' invalide dans Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nom"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nouveau"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Ouvert"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planifier"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Lancer"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Arrêter"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Les dépendances ne sont pas valides, il y a un cycle."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Il n'y a pas de candidats valides à replanifier."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Ce mois"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Ce trimestre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Cette semaine"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Cette année"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Aujourd'hui"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menu barre d'outils"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s indéfini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vue"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Type de vue"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Vous ne pouvez pas déplacer %(record)s vers %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+"Vous ne pouvez pas reprogrammer %(main_record)s vers %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "heures"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minutes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "mois"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "au"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/he.po b/addons_extensions/web_gantt/i18n/he.po
new file mode 100644
index 000000000..8053443b1
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/he.po
@@ -0,0 +1,365 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# שהאב חוסיין , 2024
+# MichaelHadar, 2024
+# דודי מלכה , 2024
+# Yihya Hugirat , 2024
+# Lilach Gilliam , 2024
+# ZVI BLONDER , 2024
+# Martin Trigaux, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Martin Trigaux, 2024\n"
+"Language-Team: Hebrew (https://app.transifex.com/odoo/teams/41243/he/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: he\n"
+"Plural-Forms: nplurals=3; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % 1 == 0) ? 1: 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "תצוגת חלון פעולה"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "החל"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "האם אתה בטוח שברצונך למחוק רשומה זו?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "בסיס"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "צמצם שורות"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "יצירה"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "ערוך"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "הרחב שורות"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "מ"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "גאנט"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "תצוגת גאנט"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "גאנט מחייב הגדרת \"תאריך התחלה\":"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "גאנט מחייב הגדרת \"תאריך סיום\":"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "אין מספיק שדות לתצוגת גאנט!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "שם"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "חדש"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "פתח"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "תוכנית"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "התחל"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "עצור"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "חודש נוכחי"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "שבוע נוכחי"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "שנה נוכחית"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "היום"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "סה\"כ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s לא מוגדר"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "תצוגה"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "סוג תצוגה"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "שעות"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "דקות"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "חודשים"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "ל"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/hi.po b/addons_extensions/web_gantt/i18n/hi.po
new file mode 100644
index 000000000..b7cc662c1
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/hi.po
@@ -0,0 +1,359 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Martin Trigaux, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Martin Trigaux, 2024\n"
+"Language-Team: Hindi (https://app.transifex.com/odoo/teams/41243/hi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: hi\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "बनाएँ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "संपादित"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "नया"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/hr.po b/addons_extensions/web_gantt/i18n/hr.po
new file mode 100644
index 000000000..faceab3e4
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/hr.po
@@ -0,0 +1,365 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Igor Krizanovic , 2024
+# Vladimir Olujić , 2024
+# Karolina Tonković , 2024
+# Bole , 2024
+# Kristina Palaš, 2024
+# Tina Milas, 2024
+# Martin Trigaux, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Martin Trigaux, 2024\n"
+"Language-Team: Croatian (https://app.transifex.com/odoo/teams/41243/hr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: hr\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Radni prozor"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Primjeni"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Osnovica"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Kreiraj"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Uredi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Od"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantogram"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantogram"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Naziv"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Novi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Otvori"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Start"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Kraj"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Ovaj mjesec"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Ovaj tjedan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Ove godine"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Danas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Ukupno"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Pogledaj"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tip pogleda"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "sati"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "mjeseci"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "do"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/hu.po b/addons_extensions/web_gantt/i18n/hu.po
new file mode 100644
index 000000000..06e0789d2
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/hu.po
@@ -0,0 +1,266 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# gezza , 2022
+# Tamás Németh , 2022
+# Istvan , 2022
+# Csaba Tóth , 2022
+# Martin Trigaux, 2022
+# krnkris, 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2022-09-22 05:49+0000\n"
+"Last-Translator: krnkris, 2023\n"
+"Language-Team: Hungarian (https://app.transifex.com/odoo/teams/41243/hu/)\n"
+"Language: hu\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Alap"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr "Sorok becsukása"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Létrehozás"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Nap"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr "Sorok kinyitása"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt nézet"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Hónap"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Új"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Következő"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Megnyitás"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Terv"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Előző"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "Ma"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Megtekintés"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Hét"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "Év"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/id.po b/addons_extensions/web_gantt/i18n/id.po
new file mode 100644
index 000000000..21eaa43a5
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/id.po
@@ -0,0 +1,365 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Abe Manyo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Abe Manyo, 2024\n"
+"Language-Team: Indonesian (https://app.transifex.com/odoo/teams/41243/id/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: id\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sj%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s tidak dapat dijadwalkan di masa lalu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sj"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Jendela Tindakan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Aktifkan mode dense"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Aktifkan mode sparse"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Terapkan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Apakah Anda yakin ingin menghapus record ini?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Tutup baris"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Buat"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Edit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Perluas baris"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Fokus Hari Ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Dari"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Tampilan Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Anak Gantt hanya dapat menjadi field atau templat, memiliki %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt harus memiliki attribute 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt harus memiliki attribute 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt harus memiliki attribute 'dependency_inverted_field' setelah "
+"'dependency_field' ditentukan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Tanggal mulai Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Tanggal berhenti Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Tampilan Gantt hanya dapat memiliki satu tag templat"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Tidak dapat menjadwalkan di masa lalu"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Field tidak mencukupi untuk Tampilan Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Atribut tidak valid (%(invalid_attributes)s) di tampilan gantt. Atribut "
+"harus dalam (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' tidak valid di gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "default_scale '%s' invalid di Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode '%s' tidak valid d gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nama"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Baru"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Terbuka"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Rencana"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Dijadwalkan ulang dengan sukses."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Mulai"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Stop"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Ketergantungan tidak valid, terdapat cycle."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Tidak ada kandidat yang valid untuk direncanakan ulang"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Bulan ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Triwulan ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Minggu ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Tahun ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Hari Ini"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menu toolbar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Undefined %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Tampilan"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tipe Tampilan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Anda tidak dapat menggerakkan %(record)s ke %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+"Anda tidak dapat menjadwalkan ulang %(main_record)s ke %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "jam"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "menit"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "bulan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "kepada"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/it.po b/addons_extensions/web_gantt/i18n/it.po
new file mode 100644
index 000000000..7094d07b1
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/it.po
@@ -0,0 +1,365 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Marianna Ciofani, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Marianna Ciofani, 2024\n"
+"Language-Team: Italian (https://app.transifex.com/odoo/teams/41243/it/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: it\n"
+"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s:%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s non può essere programmato nel passato"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s "
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Azione visualizzazione finestra"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Attiva modalità dense"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Attiva modalità sparse"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Applica"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Eliminare veramente questo record?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Imponibile"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Contrai righe"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Crea"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Modifica"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Espandi righe"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Focus oggi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Da"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vista Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+"Una riga figlia Gantt può essere solo un campo o un modello, tu hai %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt deve possedere un attributo \"date_start\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt deve possedere un attributo \"date_stop\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"La vista Gantt deve avere un attributo 'dependency_inverted_field' una volta"
+" che il 'dependency_field' viene specificato"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Data di inizio gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Data di fine gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "La vista Gantt può contenere un solo tag templates"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Impossibile programmare in passato."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Campi non sufficienti per la vista Gantt."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Attributi non validi (%(invalid_attributes)s) nella vista gantt, devono "
+"essere tra (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range non valido '%s' in gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "default_scale \"%s\" non valido in gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode \"%s\" non valida in gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nome"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nuovo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Apri"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Pianificazione"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Riprogrammazione avvenuta con successo."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Avvia"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Ferma"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Le dipendenze non sono valide, c'è un ciclo."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Non sono presenti candidati validi per la ripianificazione"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Questo mese"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Questo trimestre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Questa settimana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Questo anno"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Oggi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menu barra degli strumenti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Totale"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "%s non definiti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vista"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tipo vista"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Impossibile spostare %(record)s in %(related_record)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "Impossibile riprogrammare %(main_record)s in %(other_record)s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "ore"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "mesi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "a"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/ja.po b/addons_extensions/web_gantt/i18n/ja.po
new file mode 100644
index 000000000..705bb503f
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ja.po
@@ -0,0 +1,362 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Junko Augias, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Junko Augias, 2024\n"
+"Language-Team: Japanese (https://app.transifex.com/odoo/teams/41243/ja/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ja\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%sは過去に予定することはできません"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "アクションウィンドウビュー"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "デンスモードを有効化する"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "スパースモードを有効化する"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "適用"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "このレコードを削除してもよろしいですか?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "ベース"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "行を折りたたむ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "作成"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "編集"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "行を展開"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "今日のフォーカス"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "from"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "ガント"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "ガントビュー"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "ガントのchildはフィールドまたはテンプレートのみにできますが、%sを取得しました"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "ガントには'date_start'属性が必要です"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "ガントには'date_stop'属性が必要です"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"'dependency_field' が指定されたら、ガントは 'dependency_inverted_field' を持つ必要があります。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "ガント開始日"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "ガント停止日"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "ガントビューには、テンプレートタグを1つだけ含めることができます"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "過去に予定することはできません。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "ガントビューのフィールドが不十分です!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"ガントビューの属性 (%(invalid_attributes)s)が無効です。属性は(%(valid_attributes)s)である必要があります。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "ガントでの無効な default_range '%s' "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "ganttのdefault_scale '%s'が無効です"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "ガントでの無効な display_mode '%s' "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "名称"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "新規"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "開く"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "計画"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "正常に再スケジュールされました。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "開始"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "停止"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "依存関係が有効でなく、サイクルがあります。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "再計画に有効な候補者はいません"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "今月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "今四半期"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "今週"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "今年"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "今日"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "ツールバーメニュー"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "合計"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "未定義の%s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "ビュー"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "ビュータイプ"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr " %(record)sを %(related_record)sへ移動することはできません。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr " %(record)sを %(related_record)sへリスケジュールすることはできません。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "時間"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "分"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "か月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "から"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/ko.po b/addons_extensions/web_gantt/i18n/ko.po
new file mode 100644
index 000000000..b04d76fb7
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ko.po
@@ -0,0 +1,363 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Daye Jeong, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Daye Jeong, 2024\n"
+"Language-Team: Korean (https://app.transifex.com/odoo/teams/41243/ko/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ko\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s시간 %(minute)s분"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s는 과거로 예약할 수 없습니다."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "작업 윈도우 보기"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "dense mode 활성화"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "sparse mode 활성화"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "적용"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "이 레코드를 삭제 하시겠습니까?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "기준액"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "행 축소"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "작성"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "편집"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "행 확장"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "오늘의 포커스"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "시작 시간"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "간트 차트"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "간트 화면"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "간트 하위 메뉴는 필드나 템플릿만 선택 가능하며, 다음 내용입니다 %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "간트에는 'date_start' 속성을 지정해야 합니다"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "간트에는 'date_stop' 속성을 지정해야 합니다"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"'dependency_field'가 지정되는 경우, 간트에서 'dependency_inverted_field' 속성을 지정해야 합니다."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "간트 시작일"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "간트 종료일"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "간트 뷰에는 서식 태그가 하나만 포함될 수 있습니다"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "지난 시점으로는 예약할 수 없습니다."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "간트 차트 화면에 대한 필드가 충분하지 않습니다!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"간트 보기에 잘못된 속성 (%(invalid_attributes)s)이 있습니다. 속성은 (%(valid_attributes)s)에 "
+"위치해야 합니다."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "간트에 잘못된 default_range '%s'가 있습니다."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "간트에서 default_scale '%s' 가 유효하지 않습니다"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "간트에 잘못된 display_mode '%s'가 있습니다."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "이름"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "신규"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "열기"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "계획"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "일정 변경이 성공적으로 완료되었습니다."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "시작"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "중지"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "종속성이 유효하지 않으며 주기가 있습니다."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "다시 계획할 수 있는 적격 후보자가 없습니다."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "이번 달"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "이번 분기"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "이번 주"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "올해"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "오늘"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "도구 모음 메뉴"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "총계"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "정의되지 않은 %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "화면"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "화면 유형"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "%(record)s를 %(related_record)s로 이동할 수 없습니다."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "%(main_record)s을 %(other_record)s으로 일정을 변경할 수 없습니다. "
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "시간"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "분"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "월"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "종료"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/lb.po b/addons_extensions/web_gantt/i18n/lb.po
new file mode 100644
index 000000000..e484c05e4
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/lb.po
@@ -0,0 +1,257 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server saas~12.5+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2019-08-26 09:38+0000\n"
+"Language-Team: Luxembourgish (https://www.transifex.com/odoo/teams/41243/lb/)\n"
+"Language: lb\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/lt.po b/addons_extensions/web_gantt/i18n/lt.po
new file mode 100644
index 000000000..81e9d1894
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/lt.po
@@ -0,0 +1,267 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Audrius Palenskis , 2022
+# Rolandas , 2022
+# Edgaras Kriukonis , 2022
+# Ramunė ViaLaurea , 2022
+# Naglis Jonaitis, 2022
+# Martin Trigaux, 2022
+# Linas Versada , 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2022-09-22 05:49+0000\n"
+"Last-Translator: Linas Versada , 2023\n"
+"Language-Team: Lithuanian (https://app.transifex.com/odoo/teams/41243/lt/)\n"
+"Language: lt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < 11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? 1 : n % 1 != 0 ? 2: 3);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Ar tikrai norite ištrinti šį įrašą?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Bazė"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr "Sutraukti eilutes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Sukurti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Diena"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr "Išskleisti eilutes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Ganto vaizdas"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Ganto diagrama privalo turėti pradžios ir pabaigos požymius"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr "Grupavimas pagal datas nepalaikomas"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Mėnuo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Naujas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Kitas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Atidaryti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Suplanuoti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Ankstesnis"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "Šiandien"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Nežinoma %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Peržiūrėti"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Savaitė"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "Metai"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/mn.po b/addons_extensions/web_gantt/i18n/mn.po
new file mode 100644
index 000000000..4fe6c100c
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/mn.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Bayarkhuu Bataa, 2024
+# Batmunkh Ganbat , 2024
+# Uuganbayar Batbaatar , 2024
+# Baskhuu Lodoikhuu , 2024
+# Martin Trigaux, 2024
+# hish, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: hish, 2024\n"
+"Language-Team: Mongolian (https://app.transifex.com/odoo/teams/41243/mn/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: mn\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Үйлдлийн цонх харагдац"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Хэрэгжүүлэх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Суурь"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Мөрүүдийг хумих"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Үүсгэх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Засах"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Мөрүүдийг дэлгэх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Эхлэх"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Хүснэгтлэх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Хүснэгтлэн харах"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Нэр"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Шинэ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Нээх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Төлөвлөх"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Эхлэх"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Зогс"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Энэ сар"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Энэ долоо хоног"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Өнөөдөр"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Нийт дүн"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Харах"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Дэлгэцийн төрөл"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "цаг"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "минут"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "сарууд"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "дуусах"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/nb.po b/addons_extensions/web_gantt/i18n/nb.po
new file mode 100644
index 000000000..4aeb0c691
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/nb.po
@@ -0,0 +1,262 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Marius Stedjan , 2022
+# Martin Trigaux, 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2022-09-22 05:49+0000\n"
+"Last-Translator: Martin Trigaux, 2023\n"
+"Language-Team: Norwegian Bokmål (https://app.transifex.com/odoo/teams/41243/nb/)\n"
+"Language: nb\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Opprett"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Dag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt-visning"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Måned"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Ny"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Neste"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Åpen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planlegg"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Tilbake"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "I dag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Vis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Uke"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "År"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/nl.po b/addons_extensions/web_gantt/i18n/nl.po
new file mode 100644
index 000000000..932edc519
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/nl.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Erwin van der Ploeg , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Erwin van der Ploeg , 2024\n"
+"Language-Team: Dutch (https://app.transifex.com/odoo/teams/41243/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)su%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s kan niet in het verlden plaatsvinden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%su"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Actie venster weergave"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Dense mode activeren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Sparse mode activeren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Toepassen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Ben je zeker dat je dit record wilt verwijderen?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Basis"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Rijen inklappen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Aanmaken"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Bewerken"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Rijen uitvouwen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Focus Vandaag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Van"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt weergave"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Onderliggende Gantt kan alleen een veld of sjabloon zijn, krijg %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt moet een 'date_start' kenmerk hebben"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt moet een 'date_stop' kenmerk hebben"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gant moet een 'dependency_inverted_field' kenmerk hebben zodra het "
+"'dependency_field' is gespecifieerd"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Gantt-startdatum"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Gantt-einddatum"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "De Gantt-weergave kan slechts één sjabloon-label bevatten"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Je kan niet in het verleden plannen."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Niet genoeg velden voor Gantt weergave!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Ongeldige kenmerken (%(invalid_attributes)s) in Gantt-weergave. Kenmerken "
+"moeten in (%(valid_attributes)s) zijn"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "Ongeldige default_range '%s' in gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Ongeldige default_scale '%s' in gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "Ongeldige display_mode '%s' in gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Naam"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nieuw"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Openen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Met succes één opnieuw gepland."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Start"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Stop"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "De afhankelijkheden zijn niet geldig, er is een cyclus."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Er zijn geen geldige kandidaten om te herplannen"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Deze maand"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Dit kwartaal"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Deze week"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Dit jaar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Vandaag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Werkbalkmenu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Totaal"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Niet gedefinieerd %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Bekijk"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Soort weergave"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Je kunt %(record)s niet naar %(related_record)s verplaatsen."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "Je kunt %(main_record)s niet op %(other_record)s herplannen."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "uren"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuten"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "maanden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "aan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/pl.po b/addons_extensions/web_gantt/i18n/pl.po
new file mode 100644
index 000000000..a44efd921
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/pl.po
@@ -0,0 +1,361 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Polish (https://app.transifex.com/odoo/teams/41243/pl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: pl\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Widok okna akcji"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Zastosuj"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Czy na pewno chcesz usunąć ten rekord?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Baza"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Zwiń wiersze"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Utwórz"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Edytuj"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Rozwiń wiersze"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Od"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Wykres Gantta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Widok Gantta"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Dziecko Gantt może być tylko polem lub szablonem, mam %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt musi mieć atrybut \"date_start\"."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt musi mieć atrybut \"date_stop\"."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt musi mieć atrybut \"dependency_inverted_field\" po określeniu "
+"\"dependency_field\"."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Widok Gantta może zawierać tylko jeden tag szablonu"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Niewystarczająca liczba pól dla widoku Gantta!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Nieprawidłowy default_scale '%s' w gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nazwa"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nowe"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Otwarta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Uruchom"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Zatrzymaj"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Ten miesiąc"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Ten tydzień"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Ten rok"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Na dzisiaj"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Suma"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Niezdefiniowany %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Widok"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Typ widoku"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "godziny"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minut"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "miesiące"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "do"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/pt.po b/addons_extensions/web_gantt/i18n/pt.po
new file mode 100644
index 000000000..2ca5d3a15
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/pt.po
@@ -0,0 +1,263 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Martin Trigaux, 2022
+# Nuno Silva , 2022
+# Manuela Silva , 2023
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 16.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2023-05-16 16:03+0000\n"
+"PO-Revision-Date: 2022-09-22 05:49+0000\n"
+"Last-Translator: Manuela Silva , 2023\n"
+"Language-Team: Portuguese (https://app.transifex.com/odoo/teams/41243/pt/)\n"
+"Language: pt\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr "Colapsar linhas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Criar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "Dia"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr "Expandir linhas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vista de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Grouping by date is not supported"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Mês"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Novo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Seguinte"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Aberto"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plano"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_cell_buttons.xml:0
+msgid "Plan existing"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Anterior"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "Hoje"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Ver"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Semana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "Ano"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/pt_BR.po b/addons_extensions/web_gantt/i18n/pt_BR.po
new file mode 100644
index 000000000..b4d8861e5
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/pt_BR.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Maitê Dietze, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Maitê Dietze, 2024\n"
+"Language-Team: Portuguese (Brazil) (https://app.transifex.com/odoo/teams/41243/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s não pode ser agendado no passado"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Exibição da janela de ação"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Ativar o modo denso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Ativar o modo esparso"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Aplicar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Você tem certeza de que deseja excluir este registro?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Base"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Recolher linhas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Criar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Editar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Expandir linhas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Foco em Hoje"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "De"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Visualização de Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantts secundários só podem ser campos ou modelos, há %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt deve ter um atributo \"date_start\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt deve ter um atributo \"date_stop\""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt deve ter um atributo \"'dependency_inverted_field\" uma vez que "
+"\"dependency_field\" está especificado"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Data de início do Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Data de término do Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "A visualização de Gantt deve conter somente um marcador do modelo"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "É impossível fazer agendamentos no passado."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Campos insuficientes para a visualização de Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Atributos inválidos (%(invalid_attributes)s) na visualização de Gantt. Os "
+"atributos devem estar em (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "default_range '%s' inválido no Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Default_scale \"%s\" inválida em Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "Display_mode '%s' inválido em gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nome"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Novo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Aberto"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plano"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Reagendamento feito com sucesso."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Iniciar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Parar"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "As dependências não são válidas; há um ciclo."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "Não há candidatos válidos para replanejar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Este mês"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Este trimestre"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Esta semana"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Este ano"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Hoje"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menu da barra de ferramentas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Indefinido %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Visualização"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tipo de visualização"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Você não pode se mover %(record)s para %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "Não é possível reagendar %(main_record)s para %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "horas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minutos"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "meses"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "até"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/ro.po b/addons_extensions/web_gantt/i18n/ro.po
new file mode 100644
index 000000000..9deaa1698
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ro.po
@@ -0,0 +1,361 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Lyall Kindmurr, 2024
+# Foldi Robert , 2024
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Romanian (https://app.transifex.com/odoo/teams/41243/ro/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: ro\n"
+"Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Acțiune Vizualizare Fereastră"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Aplică"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Sigur doriți să ștergeți această înregistrare?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Baza"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Reduceți rândurile"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Creează"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Editare"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Extindere Rânduri"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "De la"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Vizualizare Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Nume"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Nou"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Afișare"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Start"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Stop"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Luna aceasta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Săpt. curentă"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Anul Acesta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Astăzi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Total"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Nedefinit %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Afișare"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Tip vizualizare"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "ore"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minute"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "luni"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "la"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/ru.po b/addons_extensions/web_gantt/i18n/ru.po
new file mode 100644
index 000000000..a23228073
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/ru.po
@@ -0,0 +1,258 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 17.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-01-08 07:03+0000\n"
+"PO-Revision-Date: 2024-01-30 15:14+0400\n"
+"Last-Translator: \n"
+"Language-Team: Russian (https://app.transifex.com/odoo/teams/41243/ru/)\n"
+"Language: ru\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Вы уверены, что хотите удалить эту запись?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "База"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Collapse rows"
+msgstr "Свернуть ряды"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Создать"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Day"
+msgstr "День"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Редактировать"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Expand rows"
+msgstr "Расширить ряды"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_view.js:0
+msgid "Gantt"
+msgstr "Гантт"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Вид Гантт"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Дочерний элемент Ганта может быть только полем или шаблоном, получено %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Гант должен иметь атрибут 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Гант должен иметь атрибут 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified"
+msgstr "Gantt должен иметь атрибут 'dependency_inverted_field' после указания 'dependency_field'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Представление Ганта может содержать только один тег шаблона"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Недостаточно полей для представления Ганта!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid attributes (%s) in gantt view. Attributes must be in (%s)"
+msgstr "Недопустимые атрибуты (%s) в представлении Ганта. Атрибуты должны быть в (%s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Недопустимая шкала по умолчанию '%s' в гигантской схеме"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Month"
+msgstr "Месяц"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Имя"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Новый"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Next"
+msgstr "Следующий"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Открыть"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "План"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Previous"
+msgstr "Предыдущий"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Records that are in the past cannot be automatically rescheduled. They should be manually rescheduled instead."
+msgstr "Записи, которые находятся в прошлом, не могут быть автоматически перенесены. Их следует переносить вручную."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Начало"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Стоп"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.xml:0
+msgid "Today"
+msgstr "Сегодня"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Всего"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Неопределено %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Просмотр"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Week"
+msgstr "Неделя"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Year"
+msgstr "Год"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %s towards %s."
+msgstr "Вы не можете перенести %s на %s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule tasks that do not follow a direct dependency path. Only the first task has been automatically rescheduled."
+msgstr "Вы не можете перенести в график задачи, которые не следуют прямому пути зависимости. Автоматически переносится только первая задача."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "часов"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "минут"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "месяцев"
diff --git a/addons_extensions/web_gantt/i18n/sl.po b/addons_extensions/web_gantt/i18n/sl.po
new file mode 100644
index 000000000..5c6355e87
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/sl.po
@@ -0,0 +1,359 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Slovenian (https://app.transifex.com/odoo/teams/41243/sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: sl\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Pogled okna za ukrep"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Uporabi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Ste prepričani, da želite izbrisati ta zapis?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Osnova"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Strni vrstice"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Ustvari"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Uredi"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Razširi vrstice"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Od"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt diagram"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Naziv"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Novo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Odprto"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Prični"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Ustavi"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Ta mesec"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Ta teden"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Danes"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Skupaj"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Prikaz"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Vrsta prikaza"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "ure"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minute"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "meseci"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "do"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/sv.po b/addons_extensions/web_gantt/i18n/sv.po
new file mode 100644
index 000000000..26971e51e
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/sv.po
@@ -0,0 +1,367 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Mikael Åkerberg , 2024
+# 03992e16f8df6e39b9d1cc0ff635887e, 2024
+# Martin Wilderoth , 2024
+# Simon S, 2024
+# Anders Wallenquist , 2024
+# Chrille Hedberg , 2024
+# Martin Trigaux, 2024
+# Kristoffer Grundström , 2024
+# Jakob Krabbe , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Jakob Krabbe , 2024\n"
+"Language-Team: Swedish (https://app.transifex.com/odoo/teams/41243/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Vy för åtgärdsfönster"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Verkställ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Bas"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Skapa"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Redigera"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Från"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Ganttvy"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Namn"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Ny"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Öppna"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Plan"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Start"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Stoppa"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Denna månad"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Denna vecka"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Detta år"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Idag"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Totalt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Visa"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Typ av vy"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "timmar"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "minuter"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "månader"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "till"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/th.po b/addons_extensions/web_gantt/i18n/th.po
new file mode 100644
index 000000000..14bade5c6
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/th.po
@@ -0,0 +1,363 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Wil Odoo, 2024\n"
+"Language-Team: Thai (https://app.transifex.com/odoo/teams/41243/th/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: th\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s ชั่วโมง %(minute)s นาที"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s ไม่สามารถกำหนดเวลาในอดีตได้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s ชั่วโมง"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "มุมมองหน้าต่างการดำเนินการ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "นำไปใช้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "คุณแน่ใจหรือว่าต้องการลบบันทึกนี้?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "ฐาน"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "ย่อแถว"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "สร้าง"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "แก้ไข"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "ขยายแถว"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "จาก"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "แกนต์"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "มุมมองแบบแกนต์"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "แกนต์ย่อยได้เฉพาะฟิลด์หรือเทมเพลตเท่านั้น มี %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "แกนต์ต้องมี 'date_start' แอตทริบิวต์ "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "แกนต์ต้องมี \"date_stop\" แอตทริบิวต์ "
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt ต้องมีแอตทริบิวต์ 'dependency_inverted_field' เมื่อระบุ "
+"'dependency_field' แล้ว"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "มุมมองแกนต์สามารถมีแท็กเทมเพลตได้เพียงแท็กเดียว"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "ไม่สามารถกำหนดเวลาในอดีตได้"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "ฟิลด์ไม่เพียงพอสำหรับมุมมองแกนต์!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"แอตทริบิวต์ไม่ถูกต้อง (%(invalid_attributes)s) ในมุมมองแกนต์ "
+"แอตทริบิวต์ต้องอยู่ใน (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Invalid default_scale '%s' ในแกนต์"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode '%s' ไม่ถูกต้องในแกนต์"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "ชื่อ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "ใหม่"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "เปิด"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "แผน"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "เริ่ม"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "หยุด"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "การขึ้นต่อกันไม่ถูกต้อง มีวงจรอยู่"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "ไม่มีผู้สมัครที่ถูกต้องที่จะวางแผนใหม่"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "เดือนนี้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "สัปดาห์นี้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "ปีนี้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "วันนี้"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "รวม"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "ไม่กำหนด %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "ดู"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "ประเภทมุมมอง"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "คุณไม่สามารถย้าย %(record)s ไปยัง %( related_record)s ได้"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "คุณไม่สามารถจัดกำหนดการใหม่ %(main_record)s ไปยัง %(other_record)s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "ชั่วโมง"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "นาที"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "เดือน"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "ถึง"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/tr.po b/addons_extensions/web_gantt/i18n/tr.po
new file mode 100644
index 000000000..277289563
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/tr.po
@@ -0,0 +1,372 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Ahmet Altinisik , 2024
+# Ozlem Cikrikci , 2024
+# Tugay Hatıl , 2024
+# Gökhan Erdoğdu , 2024
+# Levent Karakaş , 2024
+# abc Def , 2024
+# Murat Kaplan , 2024
+# Ediz Duman , 2024
+# Ertuğrul Güreş , 2024
+# Halil, 2024
+# Murat Durmuş , 2024
+# Martin Trigaux, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Martin Trigaux, 2024\n"
+"Language-Team: Turkish (https://app.transifex.com/odoo/teams/41243/tr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: tr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Pencere Aksiyon Görünümü"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Uygula"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Bu kaydı silmek istediğinizden emin misiniz?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Temel"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Satırları daralt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Oluştur"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Düzenle"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Satırları genişlet"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Başlama"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Gantt Görünümü"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Gantt alt öğesi yalnızca alan veya şablon olabilir, %svar"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt'ın bir 'date_start' özelliği olmalıdır"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt'ın bir 'date_stop' özelliği olmalıdır"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"\"dependency_field\" belirtildikten sonra Gantt'ın "
+"\"dependency_inverted_field\" özniteliğine sahip olması gerekir"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Gantt görünümü yalnızca bir şablon etiketi içerebilir"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Gantt görünümü için yetersiz alanlar!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Gantt'ta geçersiz default_scale '%s'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Adı"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Yeni"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Açık"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Planla"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Başla"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Durdur"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Bu Ay"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Bu Hafta"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Bu Yıl"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Bugün"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Toplam"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Tanımsız %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Görüntüle"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Görünüm Türü"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "saat"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "dakika"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "ay"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "den"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/uk.po b/addons_extensions/web_gantt/i18n/uk.po
new file mode 100644
index 000000000..b97b36123
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/uk.po
@@ -0,0 +1,363 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Alina Lisnenko , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Alina Lisnenko , 2024\n"
+"Language-Team: Ukrainian (https://app.transifex.com/odoo/teams/41243/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: uk\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sгод%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sгод"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Перегляд вікна дії"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Застосувати"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Ви впевнені, що хочете видалити цей запис?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "База"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Згорнути рядки"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Створити"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Редагувати"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Розгорнути рядки"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Від"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Діаграма Ґанта"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Перегляд діаграми Ґанта"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+"Дочірня діаграма Ганта може бути лише полем або шаблоном, отримайте %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Діаграма Ганта повинна мати атрибут 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Діаграма Ганта повинна мати атрибут 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Гант повинен мати атрибут 'dependency_inverted_field', коли вказано "
+"'dependency_field'"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Діаграма Ганта може містити лише один тег шаблону"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Недостатньо полів для перегляду діаграми Ганта!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Недійсне default_scale '%s' у діаграмі Ганта"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Назва"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Новий"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Відкрито"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "План"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Початок"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Зупинити"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Цього місяця"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Цього тижня"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Цього року"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Сьогодні"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Разом"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Невизначений %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Перегляд"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Тип перегляду"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "годин"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "хвилин"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "місяців"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "до"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/vi.po b/addons_extensions/web_gantt/i18n/vi.po
new file mode 100644
index 000000000..86501add3
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/vi.po
@@ -0,0 +1,364 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Thi Huong Nguyen, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Thi Huong Nguyen, 2024\n"
+"Language-Team: Vietnamese (https://app.transifex.com/odoo/teams/41243/vi/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: vi\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)sh%(minute)s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s không thể được lên lịch trong quá khứ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%sh"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "Chế độ xem cửa sổ tác vụ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "Kích hoạt dense mode"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "Kích hoạt sparse mode"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "Áp dụng"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "Bạn có chắc muốn xóa bảng ghi này?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "Cơ sở"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "Thu gọn hàng"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "Tạo"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "Chỉnh sửa"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "Mở rộng hàng"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "Tiêu điểm hôm nay"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "Từ"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "Gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "Biểu đồ Gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "Phần phụ của Gantt chỉ có thể là trường hoặc mẫu, nhưng đây là %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "Gantt cần có một thuột tính 'date_start'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "Gantt cần có một thuột tính 'date_stop'"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+"Gantt phải có thuộc tính 'dependency_inverted_field' sau khi xác định "
+"'dependency_field'"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "Ngày bắt đầu gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "Ngày ngừng gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "Chế độ xem Gantt chỉ có thể chứa một thẻ mẫu"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "Không thể lên lịch trong quá khứ"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "Không đủ trường cho Chế độ xem Gantt!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+"Thuộc tính không hợp lệ (%(invalid_attributes)s) trong chế độ xem gantt. Các"
+" thuộc tính phải có dạng (%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "Default_range '%s' không hợp lệ trong gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "Default_scale không hợp hệ '%s' trong gantt"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "display_mode không hợp hệ '%s' trong gantt"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "Tên"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "Mới"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "Mở"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "Kế hoạch"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "Đã lên lịch lại thành công."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "Bắt đầu"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "Ngừng"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "Phần phụ thuộc không hợp lệ, có một chu kỳ."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "There are no valid candidates to re-plan"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "Tháng này"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "Quý này"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "Tuần này"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "Năm nay"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "Hôm nay"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "Menu thanh công cụ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "Tổng"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "Không xác định %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "Chế độ xem"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "Dạng hiển thị"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "Bạn không thể chuyển %(record)s đến %(related_record)s."
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "Bạn không thể lên lịch lại %(main_record)s sang %(other_record)s."
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "giờ"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "phút"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "tháng"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "đến"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/web_gantt.pot b/addons_extensions/web_gantt/i18n/web_gantt.pot
new file mode 100644
index 000000000..10bb1629c
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/web_gantt.pot
@@ -0,0 +1,355 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:29+0000\n"
+"Last-Translator: \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: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr ""
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr ""
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr ""
diff --git a/addons_extensions/web_gantt/i18n/zh_CN.po b/addons_extensions/web_gantt/i18n/zh_CN.po
new file mode 100644
index 000000000..7fc41f214
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/zh_CN.po
@@ -0,0 +1,360 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Odoo哥 , 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Odoo哥 , 2024\n"
+"Language-Team: Chinese (China) (https://app.transifex.com/odoo/teams/41243/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s小时%(minute)s分钟"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s 无法排期至过往日期"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s小时"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "动作窗口视图"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "激活密集模式"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "激活稀疏模式"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "应用"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "确定要删除此记录吗?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "基数"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "折叠行"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "创建"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "编辑"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "展开行"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "聚焦到今天"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "来自"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "甘特图"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "甘特视图"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "甘特儿只能是字段或模板,转到%s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "甘特图必须有一个'date_start'属性"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "甘特图必须有一个'date_stop'属性"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr "一旦指定了 \"依赖关系_字段\",甘特图就必须具有 \"依赖关系_反转字段 \"属性"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "甘特图开始日期"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "甘特图结束日期"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "甘特图视图只能包含一个模板标签"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "无法排期到已往的日期。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "甘特图视图的字段不足!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr "甘特图视图中的属性(%(invalid_attributes)s)无效,属性必须在(%(valid_attributes)s)之中。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "甘特图中的default_range'%s'无效"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "甘特图中的default_scale'%s无效"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "甘特图中的display_mode'%s'无效"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "名称"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "新建"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "打开"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "安排"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "重新安排成功完成。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "开始"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "停止"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "依赖项目无效:存在一个循环。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "没有有效的候选人来重新规划"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "本月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "本季度"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "本周"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "今年"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "今天"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "工具栏菜单"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "总计"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "未定义的 %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "视图"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "视图类型"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "您不可移动%(record)s至%(related_record)s。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "您不可重新排期%(main_record)s至%(other_record)s。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "小时"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "分钟"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "到"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/i18n/zh_TW.po b/addons_extensions/web_gantt/i18n/zh_TW.po
new file mode 100644
index 000000000..43f40a4cd
--- /dev/null
+++ b/addons_extensions/web_gantt/i18n/zh_TW.po
@@ -0,0 +1,360 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * web_gantt
+#
+# Translators:
+# Wil Odoo, 2024
+# Tony Ng, 2024
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Odoo Server 18.0+e\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-09-25 09:29+0000\n"
+"PO-Revision-Date: 2024-09-25 09:44+0000\n"
+"Last-Translator: Tony Ng, 2024\n"
+"Language-Team: Chinese (Taiwan) (https://app.transifex.com/odoo/teams/41243/zh_TW/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: \n"
+"Language: zh_TW\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%(hour)sh%(minute)s"
+msgstr "%(hour)s 小時 %(minute)s 分鐘"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "%s cannot be scheduled in the past"
+msgstr "%s 不可排期至過往日期"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "%sh"
+msgstr "%s 小時"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_ir_actions_act_window_view
+msgid "Action Window View"
+msgstr "動作窗檢視"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate dense mode"
+msgstr "啟動緊密模式"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Activate sparse mode"
+msgstr "啟動稀疏模式"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Apply"
+msgstr "套用"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Are you sure to delete this record?"
+msgstr "確定刪除此記錄?"
+
+#. module: web_gantt
+#: model:ir.model,name:web_gantt.model_base
+msgid "Base"
+msgstr "計稅基數"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Collapse rows"
+msgstr "摺疊列"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Create"
+msgstr "建立"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Edit"
+msgstr "編輯"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Expand rows"
+msgstr "展開列"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Focus Today"
+msgstr "聚焦至今天"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "From"
+msgstr "由"
+
+#. module: web_gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_actions_act_window_view__view_mode__gantt
+#: model:ir.model.fields.selection,name:web_gantt.selection__ir_ui_view__type__gantt
+msgid "Gantt"
+msgstr "甘特圖"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Gantt View"
+msgstr "甘特圖檢視"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt child can only be field or template, got %s"
+msgstr "甘特圖子項只可以是欄位或範本,但收到 %s"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_start' attribute"
+msgstr "甘特圖必須有一個 date_start 屬性"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt must have a 'date_stop' attribute"
+msgstr "甘特圖必須有一個 date_stop 屬性"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Gantt must have a 'dependency_inverted_field' attribute once the "
+"'dependency_field' is specified"
+msgstr "甘特圖若有指定 dependency_field,就必須有 dependency_inverted_field 屬性"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt start date"
+msgstr "甘特圖開始日期"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.js:0
+msgid "Gantt stop date"
+msgstr "甘特圖結束日期"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Gantt view can contain only one templates tag"
+msgstr "甘特圖只可包含一個範本標籤"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Impossible to schedule in the past."
+msgstr "無法排期至過往日期。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Insufficient fields for Gantt View!"
+msgstr "欄位數目不足以供甘特圖使用!"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid ""
+"Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must "
+"be in (%(valid_attributes)s)"
+msgstr "甘特圖檢視畫面中的屬性(%(invalid_attributes)s)無效。屬性必須在(%(valid_attributes)s)"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_range '%s' in gantt"
+msgstr "甘特圖的 default_range '%s' 無效"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid default_scale '%s' in gantt"
+msgstr "甘特圖的 default_scale '%s' 無效"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/ir_ui_view.py:0
+msgid "Invalid display_mode '%s' in gantt"
+msgstr "甘特圖檢視模式中的顯示模式 display_mode 無效:%s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Name"
+msgstr "名稱"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.xml:0
+msgid "New"
+msgstr "新增"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_controller.js:0
+msgid "Open"
+msgstr "開啟"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Plan"
+msgstr "計劃"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "Reschedule done successfully."
+msgstr "已成功改期。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Start"
+msgstr "開始"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_popover.xml:0
+msgid "Stop"
+msgstr "停止"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "The dependencies are not valid, there is a cycle."
+msgstr "依賴項目無效:存在一個循環。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "There are no valid candidates to re-plan"
+msgstr "沒有有效的候選人可以重新規劃"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This month"
+msgstr "本月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This quarter"
+msgstr "本季度"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This week"
+msgstr "本周"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "This year"
+msgstr "本年度"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "Today"
+msgstr "今天"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "Toolbar menu"
+msgstr "工具列選單"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+msgid "Total"
+msgstr "總計"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_model.js:0
+msgid "Undefined %s"
+msgstr "未定義 %s"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer.js:0
+#: model:ir.model,name:web_gantt.model_ir_ui_view
+msgid "View"
+msgstr "檢視"
+
+#. module: web_gantt
+#: model:ir.model.fields,field_description:web_gantt.field_ir_actions_act_window_view__view_mode
+#: model:ir.model.fields,field_description:web_gantt.field_ir_ui_view__type
+msgid "View Type"
+msgstr "檢視類型"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot move %(record)s towards %(related_record)s."
+msgstr "你不可移動 %(record)s 至 %(related_record)s。"
+
+#. module: web_gantt
+#. odoo-python
+#: code:addons/web_gantt/models/models.py:0
+msgid "You cannot reschedule %(main_record)s towards %(other_record)s."
+msgstr "你不可重新排期 %(main_record)s 至 %(other_record)s。"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "hours"
+msgstr "小時"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "minutes"
+msgstr "分鐘"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_arch_parser.js:0
+msgid "months"
+msgstr "月"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_renderer_controls.xml:0
+msgid "to"
+msgstr "到"
+
+#. module: web_gantt
+#. odoo-javascript
+#: code:addons/web_gantt/static/src/gantt_row_progress_bar.xml:0
+msgid "{{ props.progressBar.warning }}"
+msgstr "{{ props.progressBar.warning }}"
diff --git a/addons_extensions/web_gantt/models/__init__.py b/addons_extensions/web_gantt/models/__init__.py
new file mode 100644
index 000000000..9d9e19686
--- /dev/null
+++ b/addons_extensions/web_gantt/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
+from . import ir_ui_view
+from . import ir_actions
diff --git a/addons_extensions/web_gantt/models/ir_actions.py b/addons_extensions/web_gantt/models/ir_actions.py
new file mode 100644
index 000000000..c75cb0524
--- /dev/null
+++ b/addons_extensions/web_gantt/models/ir_actions.py
@@ -0,0 +1,9 @@
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class ActWindowView(models.Model):
+ _inherit = 'ir.actions.act_window.view'
+
+ view_mode = fields.Selection(selection_add=[('gantt', 'Gantt')], ondelete={'gantt': 'cascade'})
diff --git a/addons_extensions/web_gantt/models/ir_ui_view.py b/addons_extensions/web_gantt/models/ir_ui_view.py
new file mode 100644
index 000000000..f2ad5bd00
--- /dev/null
+++ b/addons_extensions/web_gantt/models/ir_ui_view.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models, _
+from odoo.tools import format_list
+from lxml import etree
+
+GANTT_VALID_ATTRIBUTES = set([
+ '__validate__', # ir.ui.view implementation detail
+ 'date_start',
+ 'date_stop',
+ 'default_scale',
+ 'default_range',
+ 'class',
+ 'js_class',
+ 'form_view_id',
+ 'progress',
+ 'consolidation',
+ 'consolidation_max',
+ 'consolidation_exclude',
+ 'string',
+ 'create',
+ 'on_create',
+ 'cell_create',
+ 'edit',
+ 'delete',
+ 'plan',
+ 'default_group_by',
+ 'dynamic_range',
+ 'display_mode',
+ 'display_unavailability',
+ 'disable_drag_drop',
+ 'total_row',
+ 'collapse_first_level',
+ 'offset',
+ 'scales',
+ 'thumbnails',
+ 'precision',
+ 'color',
+ 'decoration-secondary',
+ 'decoration-success',
+ 'decoration-info',
+ 'decoration-warning',
+ 'decoration-danger',
+ 'sample',
+ 'progress_bar',
+ 'dependency_field',
+ 'dependency_inverted_field',
+ 'pill_label',
+ 'groups_limit'
+])
+
+class View(models.Model):
+ _inherit = 'ir.ui.view'
+
+ type = fields.Selection(selection_add=[('gantt', 'Gantt')])
+
+ def _validate_tag_gantt(self, node, name_manager, node_info):
+ if not node_info['validate']:
+ return
+
+ templates_count = 0
+ for child in node.iterchildren(tag=etree.Element):
+ if child.tag == 'templates':
+ if not templates_count:
+ templates_count += 1
+ else:
+ msg = _('Gantt view can contain only one templates tag')
+ self._raise_view_error(msg, child)
+ elif child.tag != 'field':
+ msg = _('Gantt child can only be field or template, got %s', child.tag)
+ self._raise_view_error(msg, child)
+
+ default_scale = node.get('default_scale')
+ if default_scale:
+ if default_scale not in ('day', 'week', 'week_2', 'month', 'month_3', 'year'):
+ self._raise_view_error(_("Invalid default_scale '%s' in gantt", default_scale), node)
+ default_range = node.get('default_range')
+ if default_range:
+ if default_range not in ('day', 'week', 'month', 'quarter', 'year'):
+ self._raise_view_error(_("Invalid default_range '%s' in gantt", default_range), node)
+ display_mode = node.get('display_mode')
+ if display_mode:
+ if display_mode not in ('dense', 'sparse'):
+ self._raise_view_error(_("Invalid display_mode '%s' in gantt", display_mode), node)
+ attrs = set(node.attrib)
+ if 'date_start' not in attrs:
+ msg = _("Gantt must have a 'date_start' attribute")
+ self._raise_view_error(msg, node)
+
+ if 'date_stop' not in attrs:
+ msg = _("Gantt must have a 'date_stop' attribute")
+ self._raise_view_error(msg, node)
+
+ if 'dependency_field' in attrs and 'dependency_inverted_field' not in attrs:
+ msg = _("Gantt must have a 'dependency_inverted_field' attribute once the 'dependency_field' is specified")
+ self._raise_view_error(msg, node)
+
+ remaining = attrs - GANTT_VALID_ATTRIBUTES
+ if remaining:
+ msg = _(
+ "Invalid attributes (%(invalid_attributes)s) in gantt view. Attributes must be in (%(valid_attributes)s)",
+ invalid_attributes=format_list(self.env, remaining),
+ valid_attributes=format_list(self.env, GANTT_VALID_ATTRIBUTES),
+ )
+ self._raise_view_error(msg, node)
+
+ def _get_view_fields(self, view_type, models):
+ if view_type == 'gantt':
+ models[self._name] = list(self._fields.keys())
+ return models
+ return super()._get_view_fields(view_type, models)
+
+ def _get_view_info(self):
+ return {'gantt': {'icon': 'fa fa-tasks'}} | super()._get_view_info()
+
+ def _is_qweb_based_view(self, view_type):
+ return view_type == 'gantt' or super()._is_qweb_based_view(view_type)
diff --git a/addons_extensions/web_gantt/models/models.py b/addons_extensions/web_gantt/models/models.py
new file mode 100644
index 000000000..966447ced
--- /dev/null
+++ b/addons_extensions/web_gantt/models/models.py
@@ -0,0 +1,780 @@
+# -*- coding: utf-8 -*-
+
+from collections import defaultdict
+from datetime import datetime, timezone, timedelta
+from lxml.builder import E
+
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+from odoo.tools import _, unique, OrderedSet
+
+
+class Base(models.AbstractModel):
+ _inherit = 'base'
+
+ _start_name = 'date_start' # start field to use for default gantt view
+ _stop_name = 'date_stop' # stop field to use for default gantt view
+
+ # action_gantt_reschedule utils
+ _WEB_GANTT_RESCHEDULE_FORWARD = 'forward'
+ _WEB_GANTT_RESCHEDULE_BACKWARD = 'backward'
+ _WEB_GANTT_LOOP_ERROR = 'loop_error'
+ _WEB_GANTT_NO_POSSIBLE_ACTION_ERROR = 'no_possible_action_error'
+
+ @api.model
+ def _get_default_gantt_view(self):
+ """ Generates a default gantt view by trying to infer
+ time-based fields from a number of pre-set attribute names
+
+ :returns: a gantt view
+ :rtype: etree._Element
+ """
+ view = E.gantt(string=self._description)
+
+ gantt_field_names = {
+ '_start_name': ['date_start', 'start_date', 'x_date_start', 'x_start_date'],
+ '_stop_name': ['date_stop', 'stop_date', 'date_end', 'end_date', 'x_date_stop', 'x_stop_date', 'x_date_end', 'x_end_date'],
+ }
+ for name in gantt_field_names.keys():
+ if getattr(self, name) not in self._fields:
+ for dt in gantt_field_names[name]:
+ if dt in self._fields:
+ setattr(self, name, dt)
+ break
+ else:
+ raise UserError(_("Insufficient fields for Gantt View!"))
+ view.set('date_start', self._start_name)
+ view.set('date_stop', self._stop_name)
+
+ return view
+
+ @api.model
+ def get_gantt_data(self, domain, groupby, read_specification, limit=None, offset=0, unavailability_fields=None, progress_bar_fields=None, start_date=None, stop_date=None, scale=None):
+ """
+ Returns the result of a read_group (and optionally search for and read records inside each
+ group), and the total number of groups matching the search domain.
+
+ :param domain: search domain
+ :param groupby: list of field to group on (see ``groupby``` param of ``read_group``)
+ :param read_specification: web_read specification to read records within the groups
+ :param limit: see ``limit`` param of ``read_group``
+ :param offset: see ``offset`` param of ``read_group``
+ :param boolean unavailability_fields
+ :param string start_date: start datetime in utc, e.g. "2024-06-22 23:00:00"
+ :param string stop_date: stop datetime in utc
+ :param string scale: among "day", "week", "month" and "year"
+ :return: {
+ 'groups': [
+ {
+ '': ,
+ ...,
+ '__record_ids': []
+ }
+ ],
+ 'records': []
+ 'length': total number of groups
+ 'unavailabilities': {
+ '': ,
+ ...
+ }
+ 'progress_bars': {
+ '': ,
+ ...
+ }
+ }
+ """
+ # TODO: group_expand doesn't currently respect the limit/offset
+ lazy = not limit and not offset and len(groupby) == 1
+ # Because there is no limit by group, we can fetch record_ids as aggregate
+ final_result = self.web_read_group(
+ domain, ['__record_ids:array_agg(id)'], groupby,
+ limit=limit, offset=offset, lazy=lazy,
+ )
+
+ all_record_ids = tuple(unique(
+ record_id
+ for one_group in final_result['groups']
+ for record_id in one_group['__record_ids']
+ ))
+
+ # Do search_fetch to order records (model order can be no-trivial)
+ all_records = self.with_context(active_test=False).search_fetch([('id', 'in', all_record_ids)], read_specification.keys())
+ final_result['records'] = all_records.with_env(self.env).web_read(read_specification)
+
+ if unavailability_fields is None:
+ unavailability_fields = []
+ if progress_bar_fields is None:
+ progress_bar_fields = []
+
+ ordered_set_ids = OrderedSet(all_records._ids)
+ res_ids_for_unavailabilities = defaultdict(set)
+ res_ids_for_progress_bars = defaultdict(set)
+ for group in final_result['groups']:
+ for field in unavailability_fields:
+ res_id = group[field][0] if group[field] else False
+ if res_id:
+ res_ids_for_unavailabilities[field].add(res_id)
+ for field in progress_bar_fields:
+ res_id = group[field][0] if group[field] else False
+ if res_id:
+ res_ids_for_progress_bars[field].add(res_id)
+ # Reorder __record_ids
+ group['__record_ids'] = list(ordered_set_ids & OrderedSet(group['__record_ids']))
+ # We don't need these in the gantt view
+ del group['__domain']
+ del group[f'{groupby[0]}_count' if lazy else '__count']
+ group.pop('__fold', None)
+
+ if unavailability_fields or progress_bar_fields:
+ start, stop = fields.Datetime.from_string(start_date), fields.Datetime.from_string(stop_date)
+
+ unavailabilities = {}
+ for field in unavailability_fields:
+ unavailabilities[field] = self._gantt_unavailability(field, list(res_ids_for_unavailabilities[field]), start, stop, scale)
+ final_result['unavailabilities'] = unavailabilities
+
+ progress_bars = {}
+ for field in progress_bar_fields:
+ progress_bars[field] = self._gantt_progress_bar(field, list(res_ids_for_progress_bars[field]), start, stop)
+ final_result['progress_bars'] = progress_bars
+
+ return final_result
+
+ @api.model
+ def web_gantt_reschedule(
+ self,
+ direction,
+ master_record_id, slave_record_id,
+ dependency_field_name, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name
+ ):
+ """ Reschedule a record according to the provided parameters.
+
+ :param direction: The direction of the rescheduling 'forward' or 'backward'
+ :param master_record_id: The record that the other one is depending on.
+ :param slave_record_id: The record that is depending on the other one.
+ :param dependency_field_name: The field name of the relation between the master and slave records.
+ :param dependency_inverted_field_name: The field name of the relation between the slave and the parent
+ records.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: dict = {
+ type: notification type,
+ message: notification message,
+ old_vals_per_pill_id: dict = {
+ pill_id: {
+ start_date_field_name: planned_date_begin before rescheduling
+ stop_date_field_name: date_deadline before rescheduling
+ }
+ }
+ }
+ """
+
+ if direction not in (self._WEB_GANTT_RESCHEDULE_FORWARD, self._WEB_GANTT_RESCHEDULE_BACKWARD):
+ raise ValueError("Invalid direction %r" % direction)
+
+ master_record, slave_record = self.env[self._name].browse([master_record_id, slave_record_id])
+
+ search_domain = [(dependency_field_name, 'in', master_record.id), ('id', '=', slave_record.id)]
+ if not self.env[self._name].search_count(search_domain, limit=1):
+ raise ValueError("Record '%r' is not a parent record of '%r'" % (master_record.name, slave_record.name))
+
+ if not self._web_gantt_reschedule_is_relation_candidate(
+ master_record, slave_record, start_date_field_name, stop_date_field_name):
+ return {
+ 'type': 'warning',
+ 'message': _('You cannot reschedule %(main_record)s towards %(other_record)s.',
+ main_record=master_record.name, other_record=slave_record.name),
+ }
+
+ is_master_prior_to_slave = master_record[stop_date_field_name] <= slave_record[start_date_field_name]
+
+ # When records are in conflict, record that is moved is the other one than when there is no conflict.
+ # This might seem strange at first sight but has been decided during first implementation as when in conflict,
+ # and especially when the distance between the pills is big, the arrow is interpreted differently as it comes
+ # from the right to the left (instead of from the left to the right).
+ if is_master_prior_to_slave ^ (direction == self._WEB_GANTT_RESCHEDULE_BACKWARD):
+ trigger_record = master_record
+ related_record = slave_record
+ else:
+ trigger_record = slave_record
+ related_record = master_record
+
+ if not trigger_record._web_gantt_reschedule_is_record_candidate(start_date_field_name, stop_date_field_name):
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'type': 'warning',
+ 'message': _(
+ "You cannot move %(record)s towards %(related_record)s.",
+ record=trigger_record.name,
+ related_record=related_record.name,
+ ),
+ }
+ }
+
+ sp = self.env.cr.savepoint()
+ log_messages, old_vals_per_pill_id = trigger_record._web_gantt_action_reschedule_candidates(dependency_field_name, dependency_inverted_field_name, start_date_field_name, stop_date_field_name, direction, related_record)
+ has_errors = bool(log_messages.get("errors"))
+ sp.close(rollback=has_errors)
+ notification_type = "success"
+ message = _("Reschedule done successfully.")
+ if has_errors or log_messages.get("warnings"):
+ message = self._web_gantt_get_reschedule_message(log_messages)
+ notification_type = "warning" if has_errors else "info"
+ return {
+ "type": notification_type,
+ "message": message,
+ "old_vals_per_pill_id": old_vals_per_pill_id,
+ }
+
+ def action_rollback_scheduling(self, old_vals_per_pill_id):
+ for record in self:
+ vals = old_vals_per_pill_id.get(str(record.id), old_vals_per_pill_id.get(record.id))
+ if vals:
+ record.write(vals)
+
+ @api.model
+ def _gantt_progress_bar(self, field, res_ids, start, stop):
+ """ Get progress bar value per record.
+
+ This method is meant to be overriden by each related model that want to
+ implement this feature on Gantt groups. The progressbar is composed
+ of a value and a max_value given for each groupedby field.
+
+ Example:
+ field = 'foo',
+ res_ids = [1, 2]
+ start_date = 01/01/2000, end_date = 01/07/2000,
+ self = base()
+
+ Result:
+ {
+ 1: {'value': 50, 'max_value': 100},
+ 2: {'value': 25, 'max_value': 200},
+ }
+
+ :param string field: field on which there are progressbars
+ :param list res_ids: res_ids of related records for which we need to compute progress bar
+ :param string start_datetime: start date in utc
+ :param string end_datetime: end date in utc
+ :returns: dict of value and max_value per record
+ """
+ return {}
+
+ @api.model
+ def _gantt_unavailability(self, field, res_ids, start, stop, scale):
+ """ Get unavailabilities data for a given set of resources.
+
+ This method is meant to be overriden by each model that want to
+ implement this feature on a Gantt view. A subslot is considered
+ unavailable (and greyed) when totally covered by an unavailability.
+
+ Example:
+ * start = 01/01/2000 in datetime utc, stop = 01/07/2000 in datetime utc, scale = 'week',
+ field = "empployee_id", res_ids = [3, 9]
+
+ * The expected return value of this function is a dict of the form
+ {
+ value: [{
+ start: ,
+ stop:
+ }, {
+ start: ,
+ stop:
+ }, ...]
+ ...
+ }
+
+ For example Marcel (3) is unavailable January 2 afternoon and
+ January 4 the whole day, the dict should look like this
+ {
+ 3: [{
+ 'start': '2018-01-02 14:00:00',
+ 'stop': '2018-01-02 18:00:00'
+ }, {
+ 'start': '2018-01-04 08:00:00',
+ 'stop': '2018-01-04 18:00:00'
+ }]
+ }
+ Note that John (9) has no unavailabilies and thus 9 is not in
+ returned dict
+
+ :param string field: name of a many2X field
+ :param list res_ids: list of values for field for which we want unavailabilities (a value is either False or an id)
+ :param datetime start: start datetime
+ :param datetime stop: stop datetime
+ :param string scale: among "day", "week", "month" and "year"
+ :returns: dict of unavailabilities
+ """
+ return {}
+
+ def _web_gantt_get_candidates(self,
+ dependency_field_name, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name,
+ related_record, move_forward_without_conflicts,
+ ):
+ result = {
+ 'warnings': [],
+ 'errors': [],
+ }
+ # first get the children of self
+ self_children_ids = []
+ pills_to_plan_before = []
+ pills_to_plan_after = []
+
+ if move_forward_without_conflicts:
+ candidates_to_exclude = {related_record.id}
+ else:
+ candidates_to_exclude = {self.id} | set(related_record[dependency_inverted_field_name].ids)
+
+ if self._web_gantt_check_cycle_existance_and_get_rescheduling_candidates(
+ self_children_ids, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name,
+ candidates_to_exclude,
+ ):
+ result['errors'].append(self._WEB_GANTT_LOOP_ERROR)
+ return (result, pills_to_plan_before, pills_to_plan_after, [])
+
+ # second, get the ancestors of related_record
+ related_record_ancestors_ids = []
+
+ if move_forward_without_conflicts:
+ candidates_to_exclude = {related_record.id} | set(self[dependency_field_name].ids)
+ else:
+ candidates_to_exclude = {self.id}
+
+ if related_record._web_gantt_check_cycle_existance_and_get_rescheduling_candidates(
+ related_record_ancestors_ids, dependency_field_name,
+ start_date_field_name, stop_date_field_name,
+ candidates_to_exclude,
+ ):
+ result['errors'].append(self._WEB_GANTT_LOOP_ERROR)
+ return (result, pills_to_plan_before, pills_to_plan_after, [])
+
+ # third, get the intersection between self children and related_record ancestors
+ if move_forward_without_conflicts:
+ all_pills_ids, pills_to_check_from_ids = self_children_ids, set(related_record_ancestors_ids)
+ else:
+ related_record_ancestors_ids.reverse()
+ all_pills_ids, pills_to_check_from_ids = related_record_ancestors_ids, self_children_ids
+
+ for pill_id in all_pills_ids:
+ if pill_id in pills_to_check_from_ids:
+ (pills_to_plan_before if move_forward_without_conflicts else pills_to_plan_after).append(pill_id)
+ else:
+ (pills_to_plan_after if move_forward_without_conflicts else pills_to_plan_before).append(pill_id)
+
+ return (result, pills_to_plan_before, pills_to_plan_after, all_pills_ids)
+
+ def _web_gantt_get_reschedule_message_per_key(self, key, params=None):
+ if key == self._WEB_GANTT_LOOP_ERROR:
+ return _("The dependencies are not valid, there is a cycle.")
+ elif key == self._WEB_GANTT_NO_POSSIBLE_ACTION_ERROR:
+ return _("There are no valid candidates to re-plan")
+ elif key == "past_error":
+ if params: # params is the record that is in the past
+ return _("%s cannot be scheduled in the past", params.display_name)
+ else:
+ return _("Impossible to schedule in the past.")
+ else:
+ return ""
+
+ def _web_gantt_get_reschedule_message(self, log_messages):
+ def get_messages(logs):
+ messages = []
+ for key in logs:
+ message = self._web_gantt_get_reschedule_message_per_key(key, log_messages.get(key))
+ if message:
+ messages.append(message)
+ return messages
+
+ messages = []
+ errors = log_messages.get("errors")
+ if errors:
+ messages = get_messages(log_messages.get("errors"))
+ else:
+ messages = get_messages(log_messages.get("warnings", []))
+ return "\n".join(messages)
+
+ def _web_gantt_action_reschedule_candidates(
+ self,
+ dependency_field_name, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name,
+ direction, related_record,
+ ):
+ """ Prepare the candidates according to the provided parameters and move them.
+
+ :param dependency_field_name: The field name of the relation between the master and slave records.
+ :param dependency_inverted_field_name: The field name of the relation between the slave and the parent
+ records.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :param direction: The direction of the rescheduling 'forward' or 'backward'
+ :param related_record: The record that self will be moving to
+ :return: tuple(valid, message) (valid = True if Successful, message = None or contains the notification text if
+ text if valid = True or the error text if valid = False.
+ """
+ search_forward = direction == self._WEB_GANTT_RESCHEDULE_FORWARD
+ # moving forward without conflicts
+ if search_forward and self[stop_date_field_name] <= related_record[start_date_field_name] and related_record in self[dependency_inverted_field_name]:
+ log_messages, pills_to_plan_before_related_record, pills_to_plan_after_related_record, all_candidates_ids = self._web_gantt_get_candidates(
+ dependency_field_name, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name,
+ related_record, True,
+ )
+
+ if log_messages.get("errors") or not pills_to_plan_before_related_record:
+ return log_messages, {}
+
+ # plan self_children backward from related_record
+ pills_to_plan_before_related_record.reverse()
+ log_messages, old_vals_per_pill_id = self._web_gantt_move_candidates(
+ start_date_field_name, stop_date_field_name,
+ dependency_field_name, dependency_inverted_field_name,
+ False, pills_to_plan_before_related_record,
+ related_record[start_date_field_name],
+ all_candidates_ids, True,
+ )
+
+ if log_messages.get("errors") or not pills_to_plan_after_related_record:
+ return log_messages, {} if log_messages.get("errors") else old_vals_per_pill_id
+
+ # plan related_record_ancestors forward from related_record
+ new_log_messages, second_old_vals_per_pill_id = self._web_gantt_move_candidates(
+ start_date_field_name, stop_date_field_name,
+ dependency_field_name, dependency_inverted_field_name,
+ True, pills_to_plan_after_related_record,
+ self[stop_date_field_name]
+ )
+
+ log_messages.setdefault("errors", []).extend(new_log_messages.get("errors", []))
+ log_messages.setdefault("warnings", []).extend(new_log_messages.get("warnings", []))
+
+ return log_messages, old_vals_per_pill_id | second_old_vals_per_pill_id
+ # moving backward without conflicts
+ elif related_record[stop_date_field_name] <= self[start_date_field_name] and related_record in self[dependency_field_name]:
+ log_messages, pills_to_plan_before_related_record, pills_to_plan_after_related_record, all_candidates_ids = related_record._web_gantt_get_candidates(
+ dependency_field_name, dependency_inverted_field_name,
+ start_date_field_name, stop_date_field_name,
+ self, False,
+ )
+
+ if log_messages.get("errors") or not pills_to_plan_after_related_record:
+ return log_messages, {}
+
+ # plan related_record_children_ids forward from related_record
+ log_messages, old_vals_per_pill_id = self._web_gantt_move_candidates(
+ start_date_field_name, stop_date_field_name,
+ dependency_field_name, dependency_inverted_field_name,
+ True, pills_to_plan_after_related_record,
+ related_record[stop_date_field_name],
+ all_candidates_ids, True,
+ )
+
+ if log_messages.get("errors") or not pills_to_plan_before_related_record:
+ return log_messages, {} if log_messages.get("errors") else old_vals_per_pill_id
+
+ # plan self_ancestors_ids backward from related_record
+ pills_to_plan_before_related_record.reverse()
+ new_log_messages, second_old_vals_per_pill_id = self._web_gantt_move_candidates(
+ start_date_field_name, stop_date_field_name,
+ dependency_field_name, dependency_inverted_field_name,
+ False, pills_to_plan_before_related_record,
+ self[start_date_field_name]
+ )
+
+ log_messages.setdefault("errors", []).extend(new_log_messages.get("errors", []))
+ log_messages.setdefault("warnings", []).extend(new_log_messages.get("warnings", []))
+
+ return log_messages, old_vals_per_pill_id | second_old_vals_per_pill_id
+ # moving forward or backward with conflicts
+ else:
+ candidates_ids = []
+ dependency = dependency_inverted_field_name if search_forward else dependency_field_name
+ if self._web_gantt_check_cycle_existance_and_get_rescheduling_candidates(
+ candidates_ids, dependency,
+ start_date_field_name, stop_date_field_name,
+ ):
+ log_messages['errors'].append(self._WEB_GANTT_LOOP_ERROR)
+ return {
+ "errors": [self._WEB_GANTT_LOOP_ERROR],
+ }, {}
+
+ if not candidates_ids:
+ return {
+ "errors": [self._WEB_GANTT_NO_POSSIBLE_ACTION_ERROR],
+ }, {}
+
+ return self._web_gantt_move_candidates(
+ start_date_field_name, stop_date_field_name,
+ dependency_field_name, dependency_inverted_field_name,
+ search_forward, candidates_ids,
+ related_record[stop_date_field_name if search_forward else start_date_field_name]
+ )
+
+ def _web_gantt_is_candidate_in_conflict(self, start_date_field_name, stop_date_field_name, dependency_field_name, dependency_inverted_field_name):
+ return (
+ any(r[start_date_field_name] and r[stop_date_field_name] and self[start_date_field_name] < r[stop_date_field_name] for r in self[dependency_field_name])
+ or any(r[start_date_field_name] and r[stop_date_field_name] and self[stop_date_field_name] > r[start_date_field_name] for r in self[dependency_inverted_field_name])
+ )
+
+ def _web_gantt_move_candidates(self, start_date_field_name, stop_date_field_name, dependency_field_name, dependency_inverted_field_name, search_forward, candidates_ids, date_candidate=None, all_candidates_ids=None, move_not_in_conflicts_candidates=False):
+ """ Move candidates according to the provided parameters.
+
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :param dependency_field_name: The field name of the relation between the master and slave records.
+ :param dependency_inverted_field_name: The field name of the relation between the slave and the parent
+ records.
+ search_forward, candidates_ids, date_candidate
+ :param search_forward: True if the direction = 'forward'
+ :param candidates_ids: The candidates to reschdule
+ :param date_candidate: The first possible date for the rescheduling
+ :param all_candidates_ids: moving without conflicts is done in 2 steps, candidates_ids contains the candidates
+ to schedule during the step, and all_candidates_ids contains the candidates to schedule in the 2 steps
+ :return: dict of list containing 2 keys, errors and warnings
+ """
+ result = {
+ "errors": [],
+ "warnings": [],
+ }
+ old_vals_per_pill_id = {}
+ candidates = self.browse(candidates_ids)
+
+ for i, candidate in enumerate(candidates):
+ if not move_not_in_conflicts_candidates and not candidate._web_gantt_is_candidate_in_conflict(start_date_field_name, stop_date_field_name, dependency_field_name, dependency_inverted_field_name):
+ continue
+
+ start_date, end_date = candidate._web_gantt_reschedule_compute_dates(
+ date_candidate,
+ search_forward,
+ start_date_field_name, stop_date_field_name
+ )
+ start_date, end_date = start_date.astimezone(timezone.utc), end_date.astimezone(timezone.utc)
+ old_start_date, old_end_date = candidate[start_date_field_name], candidate[stop_date_field_name]
+ if not candidate._web_gantt_reschedule_write_new_dates(
+ start_date, end_date,
+ start_date_field_name, stop_date_field_name
+ ):
+ result["errors"].append("past_error")
+ result["past_error"] = candidate
+ return result, {}
+ else:
+ old_vals_per_pill_id[candidate.id] = {
+ start_date_field_name: old_start_date,
+ stop_date_field_name: old_end_date,
+ }
+
+ if i + 1 < len(candidates):
+ next_candidate = candidates[i + 1]
+ if search_forward:
+ ancestors = next_candidate[dependency_field_name]
+ if ancestors:
+ date_candidate = max(ancestors.mapped(stop_date_field_name))
+ else:
+ date_candidate = end_date
+ else:
+ children = next_candidate[dependency_inverted_field_name]
+ if children:
+ date_candidate = min(children.mapped(start_date_field_name))
+ else:
+ date_candidate = start_date
+
+ return result, old_vals_per_pill_id
+
+ def _web_gantt_check_cycle_existance_and_get_rescheduling_candidates(self,
+ candidates_ids, dependency_field_name,
+ start_date_field_name, stop_date_field_name,
+ candidates_to_exclude=None, visited=None, ancestors=None,
+ ):
+ """ Get the current records' related records rescheduling candidates (explained in details
+ in case 1 and case 2 in the below example)
+
+ This method Executes a dfs (depth first search algorithm) on the dependencies tree to:
+ 1- detect cycles (detect if it's not a valid tree)
+ 2- return the topological sorting of the candidates to reschedule
+
+ Example:
+
+ [4]->[6]
+ |
+ v
+ --->[0]->[1]->[2] [5]->[7]->[8]-----------------
+ | | | |
+ | v v |
+ | [3] [9]->[10] |
+ | |
+ -------------------------------------------------
+
+ [0]->[1]: pill 0 should be done before 1
+ <: left arrow to move pill 8 backward pill 0
+ >: right arrow to move pill 0 forward pill 8
+ x: delete the dependence
+
+ Case 1:
+ If the right arrow is clicked, pill 0 should move forward. And as 1, 2, 3 are children of 0, they should be done after it,
+ they should also be moved forward.
+ This method will return False (no cycles) and a valid order of candidates = [0, 1, 2, 3] that should be scheduled
+
+ Case 2:
+ If the left arrow is clicked, pill 8 should move backward task 0, as 4, 6, 5, 7 are ancestors for 8, they should be done
+ before it, they should be moved backward also. 9 and 10 should not be impacted as they are not ancestors of 8.
+ This method will return False (no cycles) and a valid order of candidates = [5, 4, 6, 7, 8] that should be scheduled
+
+ Example 2:
+ modify the previous tree by adding an edge from pill 2 to pill 0 (no more a tree after this added edge)
+ -----------
+ | |
+ v |
+ [0]->[1]->[2]
+
+ This method will return True because there is the cycle illustrated above
+
+ :param candidates_ids: empty list that will contain the candidates at the end
+ :param dependency_field_name: The field name of the relation between the master and slave records.
+ :param dependency_inverted_field_name: The field name of the relation between the slave and the parent
+ records.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :param candidates_to_exclude: candidates to exclude
+ :param visited: set containing all the visited pills
+ :param ancestors: set containing the visited ancestors for the current pill
+ :return: bool, True if there is a cycle, else False.
+ candidates_id will also contain the pills to plan in a valid topological order
+ """
+ if candidates_to_exclude is None:
+ candidates_to_exclude = []
+ if visited is None:
+ visited = set()
+ if ancestors is None:
+ ancestors = []
+ visited.add(self.id)
+ ancestors.append(self.id)
+ for child in self[dependency_field_name]:
+ if child.id in ancestors:
+ return True
+
+ if child.id not in visited and child.id not in candidates_to_exclude and child._web_gantt_check_cycle_existance_and_get_rescheduling_candidates(candidates_ids, dependency_field_name, start_date_field_name, stop_date_field_name, candidates_to_exclude, visited, ancestors):
+ return True
+
+ ancestors.pop()
+ if self._web_gantt_reschedule_is_record_candidate(start_date_field_name, stop_date_field_name) and self.id not in candidates_to_exclude:
+ candidates_ids.insert(0, self.id)
+
+ return False
+
+ def _web_gantt_reschedule_compute_dates(
+ self, date_candidate, search_forward, start_date_field_name, stop_date_field_name
+ ):
+ """ Compute start_date and end_date according to the provided arguments.
+ This method is meant to be overridden when we need to add constraints that have to be taken into account
+ in the computing of the start_date and end_date.
+
+ :param date_candidate: The optimal date, which does not take any constraint into account.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: a tuple of (start_date, end_date)
+ :rtype: tuple(datetime, datetime)
+ """
+ search_factor = (1 if search_forward else -1)
+ duration = search_factor * (self[stop_date_field_name] - self[start_date_field_name])
+ return sorted([date_candidate, date_candidate + duration])
+
+ @api.model
+ def _web_gantt_reschedule_is_in_conflict(self, master, slave, start_date_field_name, stop_date_field_name):
+ """ Get whether the dependency relation between a master and a slave record is in conflict.
+ This check is By-passed for slave records if moving records forwards and the for
+ master records if moving records backwards (see _web_gantt_get_rescheduling_candidates and
+ _web_gantt_reschedule_is_in_conflict_or_force). In order to add condition that would not be
+ by-passed, rather consider _web_gantt_reschedule_is_relation_candidate.
+
+ :param master: The master record.
+ :param slave: The slave record.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: True if there is a conflict, False if not.
+ :rtype: bool
+ """
+ return master[stop_date_field_name] > slave[start_date_field_name]
+
+ @api.model
+ def _web_gantt_reschedule_is_in_conflict_or_force(
+ self, master, slave, start_date_field_name, stop_date_field_name, force
+ ):
+ """ Get whether the dependency relation between a master and a slave record is in conflict.
+ This check is By-passed for slave records if moving records forwards and the for
+ master records if moving records backwards. In order to add condition that would not be
+ by-passed, rather consider _web_gantt_reschedule_is_relation_candidate.
+
+ This def purpose is to be able to prevent the default behavior in some modules by overriding
+ the def and forcing / preventing the rescheduling il all circumstances if needed.
+ See _web_gantt_get_rescheduling_candidates.
+
+ :param master: The master record.
+ :param slave: The slave record.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :param force: Force returning True
+ :return: True if there is a conflict, False if not.
+ :rtype: bool
+ """
+ return force or self._web_gantt_reschedule_is_in_conflict(
+ master, slave, start_date_field_name, stop_date_field_name
+ )
+
+ def _web_gantt_reschedule_is_record_candidate(self, start_date_field_name, stop_date_field_name):
+ """ Get whether the record is a candidate for the rescheduling. This method is meant to be overridden when
+ we need to add a constraint in order to prevent some records to be rescheduled. This method focuses on the
+ record itself (if you need to have information on the relation (master and slave) rather override
+ _web_gantt_reschedule_is_relation_candidate).
+
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: True if record can be rescheduled, False if not.
+ :rtype: bool
+ """
+ self.ensure_one()
+ return self[start_date_field_name] and self[stop_date_field_name] \
+ and self[start_date_field_name].replace(tzinfo=timezone.utc) > datetime.now(timezone.utc)
+
+ def _web_gantt_reschedule_is_relation_candidate(self, master, slave, start_date_field_name, stop_date_field_name):
+ """ Get whether the relation between master and slave is a candidate for the rescheduling. This method is meant
+ to be overridden when we need to add a constraint in order to prevent some records to be rescheduled.
+ This method focuses on the relation between records (if your logic is rather on one record, rather override
+ _web_gantt_reschedule_is_record_candidate).
+
+ :param master: The master record we need to evaluate whether it is a candidate for rescheduling or not.
+ :param slave: The slave record.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: True if record can be rescheduled, False if not.
+ :rtype: bool
+ """
+ return True
+
+ def _web_gantt_reschedule_write_new_dates(
+ self, new_start_date, new_stop_date, start_date_field_name, stop_date_field_name
+ ):
+ """ Write the dates values if new_start_date is in the future.
+
+ :param new_start_date: The start_date to write.
+ :param new_stop_date: The stop_date to write.
+ :param start_date_field_name: The start date field used in the gantt view.
+ :param stop_date_field_name: The stop date field used in the gantt view.
+ :return: True if successful, False if not.
+ :rtype: bool
+
+ epsilon = 30 seconds was added because the first valid interval can be now and because of some seconds, it will become < now() at the comparaison moment
+ it's a matter of some seconds
+ """
+ new_start_date = new_start_date.astimezone(timezone.utc).replace(tzinfo=None)
+ if new_start_date < datetime.now() + timedelta(seconds=-30):
+ return False
+
+ self.write({
+ start_date_field_name: new_start_date,
+ stop_date_field_name: new_stop_date.astimezone(timezone.utc).replace(tzinfo=None)
+ })
+ return True
diff --git a/addons_extensions/web_gantt/static/src/gantt_arch_parser.js b/addons_extensions/web_gantt/static/src/gantt_arch_parser.js
new file mode 100644
index 000000000..deeb1eb0a
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_arch_parser.js
@@ -0,0 +1,332 @@
+import { getLocalYearAndWeek } from "@web/core/l10n/dates";
+import { _t } from "@web/core/l10n/translation";
+import { evaluateExpr } from "@web/core/py_js/py";
+import { exprToBoolean } from "@web/core/utils/strings";
+import { visitXML } from "@web/core/utils/xml";
+import { getActiveActions } from "@web/views/utils";
+
+const DECORATIONS = [
+ "decoration-danger",
+ "decoration-info",
+ "decoration-secondary",
+ "decoration-success",
+ "decoration-warning",
+];
+const PARTS = { full: 1, half: 2, quarter: 4 };
+const SCALES = {
+ day: {
+ // determines subcolumns
+ cellPrecisions: { full: 60, half: 30, quarter: 15 },
+ defaultPrecision: "full",
+ time: "minute",
+ unitDescription: _t("minutes"),
+
+ // determines columns
+ interval: "hour",
+ minimalColumnWidth: 40,
+
+ // determines column groups
+ unit: "day",
+ groupHeaderFormatter: (date) => date.toFormat("dd MMMM yyyy"),
+
+ defaultRange: { unit: "day", count: 3 },
+ },
+ week: {
+ cellPrecisions: { full: 24, half: 12 },
+ defaultPrecision: "half",
+ time: "hour",
+ unitDescription: _t("hours"),
+
+ interval: "day",
+ minimalColumnWidth: 192,
+ colHeaderFormatter: (date) => date.toFormat("dd"),
+
+ unit: "week",
+ groupHeaderFormatter: formatLocalWeekYear,
+
+ defaultRange: { unit: "week", count: 3 },
+ },
+ week_2: {
+ cellPrecisions: { full: 24, half: 12 },
+ defaultPrecision: "half",
+ time: "hour",
+ unitDescription: _t("hours"),
+
+ interval: "day",
+ minimalColumnWidth: 96,
+ colHeaderFormatter: (date) => date.toFormat("dd"),
+
+ unit: "week",
+ groupHeaderFormatter: formatLocalWeekYear,
+
+ defaultRange: { unit: "week", count: 6 },
+ },
+ month: {
+ cellPrecisions: { full: 24, half: 12 },
+ defaultPrecision: "half",
+ time: "hour",
+ unitDescription: _t("hours"),
+
+ interval: "day",
+ minimalColumnWidth: 50,
+ colHeaderFormatter: (date) => date.toFormat("dd"),
+
+ unit: "month",
+ groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? "MMM yyyy" : "MMMM yyyy"),
+
+ defaultRange: { unit: "month", count: 3 },
+ },
+ month_3: {
+ cellPrecisions: { full: 24, half: 12 },
+ defaultPrecision: "half",
+ time: "hour",
+ unitDescription: _t("hours"),
+
+ interval: "day",
+ minimalColumnWidth: 18,
+ colHeaderFormatter: (date) => date.toFormat("dd"),
+
+ unit: "month",
+ groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? "MMM yyyy" : "MMMM yyyy"),
+
+ defaultRange: { unit: "month", count: 6 },
+ },
+ year: {
+ cellPrecisions: { full: 1 },
+ defaultPrecision: "full",
+ time: "month",
+ unitDescription: _t("months"),
+
+ interval: "month",
+ minimalColumnWidth: 60,
+ colHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? "MMM" : "MMMM"),
+
+ unit: "year",
+ groupHeaderFormatter: (date) => date.toFormat("yyyy"),
+
+ defaultRange: { unit: "year", count: 1 },
+ },
+};
+
+/**
+ * Formats a date to a `'W'W kkkk` datetime string, in the user's locale settings.
+ *
+ * @param {Date|luxon.DateTime} date
+ * @returns {string}
+ */
+function formatLocalWeekYear(date) {
+ const { year, week } = getLocalYearAndWeek(date);
+ return `W${week} ${year}`;
+}
+
+function getPreferedScaleId(scaleId, scales) {
+ // we assume that scales is not empty
+ if (scaleId in scales) {
+ return scaleId;
+ }
+ const scaleIds = Object.keys(SCALES);
+ const index = scaleIds.findIndex((id) => id === scaleId);
+ for (let j = index - 1; j >= 0; j--) {
+ const id = scaleIds[j];
+ if (id in scales) {
+ return id;
+ }
+ }
+ for (let j = index + 1; j < scaleIds.length; j++) {
+ const id = scaleIds[j];
+ if (id in scales) {
+ return id;
+ }
+ }
+}
+
+const RANGES = {
+ day: { scaleId: "day", description: _t("Today") },
+ week: { scaleId: "week", description: _t("This week") },
+ month: { scaleId: "month", description: _t("This month") },
+ quarter: { scaleId: "month_3", description: _t("This quarter") },
+ year: { scaleId: "year", description: _t("This year") },
+};
+
+export class GanttArchParser {
+ parse(arch) {
+ let infoFromRootNode;
+ const decorationFields = [];
+ const popoverArchParams = {
+ displayGenericButtons: true,
+ bodyTemplate: null,
+ footerTemplate: null,
+ };
+
+ visitXML(arch, (node) => {
+ switch (node.tagName) {
+ case "gantt": {
+ infoFromRootNode = getInfoFromRootNode(node);
+ break;
+ }
+ case "field": {
+ const fieldName = node.getAttribute("name");
+ decorationFields.push(fieldName);
+ break;
+ }
+ case "templates": {
+ const body = node.querySelector("[t-name=gantt-popover]") || null;
+ if (body) {
+ popoverArchParams.bodyTemplate = body.cloneNode(true);
+ popoverArchParams.bodyTemplate.removeAttribute("t-name");
+ const footer = popoverArchParams.bodyTemplate.querySelector("footer");
+ if (footer) {
+ popoverArchParams.displayGenericButtons = false;
+ footer.remove();
+ const footerTemplate = new Document().createElement("t");
+ footerTemplate.append(...footer.children);
+ popoverArchParams.footerTemplate = footerTemplate;
+ const replace = footer.getAttribute("replace");
+ if (replace && !exprToBoolean(replace)) {
+ popoverArchParams.displayGenericButtons = true;
+ }
+ }
+ }
+ }
+ }
+ });
+
+ return {
+ ...infoFromRootNode,
+ decorationFields,
+ popoverArchParams,
+ };
+ }
+}
+
+function getInfoFromRootNode(rootNode) {
+ const attrs = {};
+ for (const { name, value } of rootNode.attributes) {
+ attrs[name] = value;
+ }
+
+ const { create: canCreate, delete: canDelete, edit: canEdit } = getActiveActions(rootNode);
+ const canCellCreate = exprToBoolean(attrs.cell_create, true) && canCreate;
+ const canPlan = exprToBoolean(attrs.plan, true) && canEdit;
+
+ let consolidationMaxField;
+ let consolidationMaxValue;
+ const consolidationMax = attrs.consolidation_max ? evaluateExpr(attrs.consolidation_max) : {};
+ if (Object.keys(consolidationMax).length > 0) {
+ consolidationMaxField = Object.keys(consolidationMax)[0];
+ consolidationMaxValue = consolidationMax[consolidationMaxField];
+ }
+
+ const consolidationParams = {
+ excludeField: attrs.consolidation_exclude,
+ field: attrs.consolidation,
+ maxField: consolidationMaxField,
+ maxValue: consolidationMaxValue,
+ };
+
+ const dependencyField = attrs.dependency_field || null;
+ const dependencyEnabled = !!dependencyField;
+ const dependencyInvertedField = attrs.dependency_inverted_field || null;
+
+ const allowedScales = [];
+ if (attrs.scales) {
+ for (const key of attrs.scales.split(",")) {
+ if (SCALES[key]) {
+ allowedScales.push(key);
+ }
+ }
+ }
+ if (allowedScales.length === 0) {
+ allowedScales.push(...Object.keys(SCALES));
+ }
+
+ // Cell precision
+ const cellPrecisions = {};
+
+ // precision = {'day': 'hour:half', 'week': 'day:half', 'month': 'day', 'year': 'month:quarter'}
+ const precisionAttrs = attrs.precision ? evaluateExpr(attrs.precision) : {};
+ for (const scaleId in SCALES) {
+ if (precisionAttrs[scaleId]) {
+ const precision = precisionAttrs[scaleId].split(":"); // hour:half
+ // Note that precision[0] (which is the cell interval) is not
+ // taken into account right now because it is no customizable.
+ if (
+ precision[1] &&
+ Object.keys(SCALES[scaleId].cellPrecisions).includes(precision[1])
+ ) {
+ cellPrecisions[scaleId] = precision[1];
+ }
+ }
+ cellPrecisions[scaleId] ||= SCALES[scaleId].defaultPrecision;
+ }
+
+ const scales = {};
+ for (const scaleId of allowedScales) {
+ const precision = cellPrecisions[scaleId];
+ const referenceScale = SCALES[scaleId];
+ scales[scaleId] = {
+ ...referenceScale,
+ cellPart: PARTS[precision],
+ cellTime: referenceScale.cellPrecisions[precision],
+ id: scaleId,
+ unitDescription: referenceScale.unitDescription.toString(),
+ };
+ // protect SCALES content
+ delete scales[scaleId].cellPrecisions;
+ }
+
+ const ranges = {};
+ for (const rangeId in RANGES) {
+ const referenceRange = RANGES[rangeId];
+ ranges[rangeId] = {
+ ...referenceRange,
+ id: rangeId,
+ scaleId: getPreferedScaleId(referenceRange.scaleId, scales),
+ description: referenceRange.description.toString(),
+ };
+ }
+
+ let pillDecorations = null;
+ for (const decoration of DECORATIONS) {
+ if (decoration in attrs) {
+ if (!pillDecorations) {
+ pillDecorations = {};
+ }
+ pillDecorations[decoration] = attrs[decoration];
+ }
+ }
+
+ return {
+ canCellCreate,
+ canCreate,
+ canDelete,
+ canEdit,
+ canPlan,
+ colorField: attrs.color,
+ computePillDisplayName: !!attrs.pill_label,
+ consolidationParams,
+ createAction: attrs.on_create || null,
+ dateStartField: attrs.date_start,
+ dateStopField: attrs.date_stop,
+ defaultGroupBy: attrs.default_group_by ? attrs.default_group_by.split(",") : [],
+ defaultRange: attrs.default_range,
+ defaultScale: attrs.default_scale || "month",
+ dependencyEnabled,
+ dependencyField,
+ dependencyInvertedField,
+ disableDrag: exprToBoolean(attrs.disable_drag_drop),
+ displayMode: attrs.display_mode || "dense",
+ displayTotalRow: exprToBoolean(attrs.total_row),
+ displayUnavailability: exprToBoolean(attrs.display_unavailability),
+ formViewId: attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false,
+ offset: attrs.offset,
+ pagerLimit: attrs.groups_limit ? parseInt(attrs.groups_limit, 10) : null,
+ pillDecorations,
+ progressBarFields: attrs.progress_bar ? attrs.progress_bar.split(",") : null,
+ progressField: attrs.progress || null,
+ ranges,
+ scales,
+ string: attrs.string || _t("Gantt View").toString(),
+ thumbnails: attrs.thumbnails ? evaluateExpr(attrs.thumbnails) : {},
+ };
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_compiler.js b/addons_extensions/web_gantt/static/src/gantt_compiler.js
new file mode 100644
index 000000000..692f481b7
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_compiler.js
@@ -0,0 +1,20 @@
+import { ViewCompiler } from "@web/views/view_compiler";
+
+export class GanttCompiler extends ViewCompiler {}
+GanttCompiler.OWL_DIRECTIVE_WHITELIST = [
+ ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,
+ "t-name",
+ "t-esc",
+ "t-out",
+ "t-set",
+ "t-value",
+ "t-if",
+ "t-else",
+ "t-elif",
+ "t-foreach",
+ "t-as",
+ "t-key",
+ "t-att.*",
+ "t-call",
+ "t-translation",
+];
diff --git a/addons_extensions/web_gantt/static/src/gantt_connector.js b/addons_extensions/web_gantt/static/src/gantt_connector.js
new file mode 100644
index 000000000..39cf9cfe4
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_connector.js
@@ -0,0 +1,294 @@
+import { Component, onWillRender, useEffect, useRef } from "@odoo/owl";
+
+/**
+ * @typedef {"error" | "warning"} ConnectorAlert
+ * @typedef {`__connector__${number | "new"}`} ConnectorId
+ * @typedef {import("./gantt_renderer").Point} Point
+ *
+ * @typedef ConnectorProps
+ * @property {ConnectorId} id
+ * @property {ConnectorAlert | null} alert
+ * @property {boolean} highlighted
+ * @property {boolean} displayButtons
+ * @property {Point | () => Point | null} sourcePoint
+ * @property {Point | () => Point | null} targetPoint
+ *
+ * @typedef {Object} PathInfo
+ * @property {Point} sourceControlPoint
+ * @property {Point} targetControlPoint
+ * @property {Point} removeButtonPosition
+ *
+ * @typedef Point
+ * @property {number} [x]
+ * @property {number} [y]
+ */
+
+/**
+ * Gets the stroke's rgba css string corresponding to the provided parameters for both the stroke and its
+ * hovered state.
+ *
+ * @param {number} r [0, 255]
+ * @param {number} g [0, 255]
+ * @param {number} b [0, 255]
+ * @return {{ stroke: string, hoveredStroke: string }} the css colors.
+ */
+export function getStrokeAndHoveredStrokeColor(r, g, b) {
+ return {
+ color: `rgba(${r},${g},${b},0.5)`,
+ highlightedColor: `rgba(${r},${g},${b},1)`,
+ };
+}
+
+export const COLORS = {
+ default: getStrokeAndHoveredStrokeColor(143, 143, 143),
+ error: getStrokeAndHoveredStrokeColor(211, 65, 59),
+ warning: getStrokeAndHoveredStrokeColor(236, 151, 31),
+ outline: getStrokeAndHoveredStrokeColor(255, 255, 255),
+};
+
+/** @extends {Component<{ reactive: ConnectorProps }, any>} */
+export class GanttConnector extends Component {
+ static props = {
+ reactive: {
+ type: Object,
+ shape: {
+ id: String,
+ alert: {
+ type: [{ value: "error" }, { value: "warning" }, { value: null }],
+ optional: true,
+ },
+ highlighted: { type: Boolean, optional: true },
+ displayButtons: { type: Boolean, optional: true },
+ sourcePoint: [
+ { value: null },
+ Function,
+ { type: Object, shape: { left: Number, top: Number } },
+ ],
+ targetPoint: [
+ { value: null },
+ Function,
+ { type: Object, shape: { left: Number, top: Number } },
+ ],
+ },
+ },
+ onLeftButtonClick: { type: Function, optional: true },
+ onRemoveButtonClick: { type: Function, optional: true },
+ onRightButtonClick: { type: Function, optional: true },
+ };
+ static defaultProps = {
+ highlighted: false,
+ displayButtons: false,
+ };
+ static template = "web_gantt.GanttConnector";
+
+ rootRef = useRef("root");
+ style = {
+ hoverEaseWidth: 10,
+ slackness: 0.9,
+ stroke: { width: 2 },
+ outlineStroke: { width: 1 },
+ };
+
+ get alert() {
+ return this.props.reactive.alert;
+ }
+
+ get displayButtons() {
+ return this.props.reactive.displayButtons;
+ }
+
+ get highlighted() {
+ return this.props.reactive.highlighted;
+ }
+
+ get id() {
+ return this.props.reactive.id;
+ }
+
+ get isNew() {
+ return this.id.endsWith("new");
+ }
+
+ get sourcePoint() {
+ return this.props.reactive.sourcePoint;
+ }
+
+ get targetPoint() {
+ return this.props.reactive.targetPoint;
+ }
+
+ setup() {
+ onWillRender(this.onWillRender);
+
+ useEffect(
+ (el, sourceLeft, sourceTop, targetLeft, targetTop) => {
+ if (!el) {
+ return;
+ }
+ const { sourceControlPoint, targetControlPoint, removeButtonPosition } =
+ this.getPathInfo(
+ { left: sourceLeft, top: sourceTop },
+ { left: targetLeft, top: targetTop },
+ this.style.slackness
+ );
+
+ const drawingCommands = [
+ `M`,
+ `${sourceLeft},${sourceTop}`,
+ `C`,
+ `${sourceControlPoint.left},${sourceControlPoint.top}`,
+ `${targetControlPoint.left},${targetControlPoint.top}`,
+ `${targetLeft},${targetTop}`,
+ ].join(" ");
+
+ const paths = el.querySelectorAll(
+ ".o_connector_stroke, .o_connector_stroke_hover_ease"
+ );
+ for (const path of paths) {
+ path.setAttribute("d", drawingCommands);
+ }
+
+ const svgButtons = el.querySelector(".o_connector_stroke_buttons");
+ if (svgButtons) {
+ svgButtons.setAttribute("x", removeButtonPosition.left - 24);
+ svgButtons.setAttribute("y", removeButtonPosition.top - 8);
+ }
+ },
+ () => this.getEffectDependencies()
+ );
+ }
+
+ /**
+ * Refreshes the connector properties from the props.
+ *
+ * @param {ConnectorProps} props
+ */
+ computeStyle({ alert, highlighted }) {
+ const key = highlighted ? "highlightedColor" : "color";
+ const strokeType = alert || "default";
+ this.style = {
+ hoverEaseWidth: 10,
+ slackness: 0.9,
+ stroke: {
+ color: COLORS[strokeType][key],
+ width: 2,
+ },
+ outlineStroke: {
+ color: COLORS.outline[key],
+ width: 1,
+ },
+ };
+ }
+
+ getEffectDependencies() {
+ let sourcePoint = this.sourcePoint || { left: 0, top: 0 };
+ if (typeof sourcePoint === "function") {
+ sourcePoint = sourcePoint();
+ }
+ let targetPoint = this.targetPoint || { left: 0, top: 0 };
+ if (typeof targetPoint === "function") {
+ targetPoint = targetPoint();
+ }
+ const { x, y } = this.rootRef.el?.getBoundingClientRect() || { x: 0, y: 0 };
+
+ return [
+ this.rootRef.el,
+ sourcePoint.left - x,
+ sourcePoint.top - y,
+ targetPoint.left - x,
+ targetPoint.top - y,
+ this.displayButtons,
+ ];
+ }
+
+ /**
+ * Returns the linear interpolation for a point to be found somewhere on the line startingPoint, endingPoint.
+ *
+ * @param {Point} startingPoint
+ * @param {Point} endingPoint
+ * @param {number} lambda
+ * @returns {Point}
+ */
+ getLinearInterpolation(startingPoint, endingPoint, lambda = 0.5) {
+ return {
+ left: lambda * startingPoint.left + (1 - lambda) * endingPoint.left,
+ top: lambda * startingPoint.top + (1 - lambda) * endingPoint.top,
+ };
+ }
+
+ /**
+ * Returns the parameters of both the single Bezier curve as well as is decomposition into two beziers curves
+ * (which allows to get the middle position of the single Bezier curve) for the provided source, target and
+ * slackness (0 being a straight line).
+ *
+ * @param {Point} sourcePoint
+ * @param {Point} targetPoint
+ * @param {number} slackness [0, 1]
+ * @returns {PathInfo}
+ */
+ getPathInfo(sourcePoint, targetPoint, slackness) {
+ // If the source is on the left of the target, we need to invert the control points.
+ const xDelta = targetPoint.left - sourcePoint.left;
+ const yDelta = targetPoint.top - sourcePoint.top;
+ const directionFactor = Math.sign(xDelta);
+
+ // What follows can be seen as magic numbers. And those are indeed such numbers as they have been determined
+ // by observing their shape while creating short and long connectors. These seems to allow keeping the same
+ // kind of shape amongst short and long connectors.
+ const xInc = 100 + (Math.abs(xDelta) * slackness) / 10;
+ const yInc =
+ Math.abs(yDelta) < 16 && directionFactor === -1 ? 15 - 0.001 * xDelta * slackness : 0;
+
+ const b = {
+ left: sourcePoint.left + xInc,
+ top: sourcePoint.top + yInc,
+ };
+
+ // Prevent having the air pin effect when in creation and having target on the left of the source
+ const c = {
+ left: targetPoint.left + (this.isNew && directionFactor === -1 ? xInc : -xInc),
+ top: targetPoint.top + yInc,
+ };
+
+ const e = this.getLinearInterpolation(sourcePoint, b);
+ const f = this.getLinearInterpolation(b, c);
+ const g = this.getLinearInterpolation(c, targetPoint);
+ const h = this.getLinearInterpolation(e, f);
+ const i = this.getLinearInterpolation(f, g);
+ const j = this.getLinearInterpolation(h, i);
+
+ return {
+ sourceControlPoint: b,
+ targetControlPoint: c,
+ removeButtonPosition: j,
+ };
+ }
+
+ //-------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ onLeftButtonClick() {
+ if (this.props.onLeftButtonClick) {
+ this.props.onLeftButtonClick();
+ }
+ }
+
+ onRemoveButtonClick() {
+ if (this.props.onRemoveButtonClick) {
+ this.props.onRemoveButtonClick();
+ }
+ }
+
+ onRightButtonClick() {
+ if (this.props.onRightButtonClick) {
+ this.props.onRightButtonClick();
+ }
+ }
+
+ onWillRender() {
+ const key = this.highlighted ? "highlightedColor" : "color";
+ this.style.stroke.color = COLORS[this.alert || "default"][key];
+ this.style.outlineStroke.color = COLORS.outline[key];
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_connector.xml b/addons_extensions/web_gantt/static/src/gantt_connector.xml
new file mode 100644
index 000000000..7b579f157
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_connector.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_controller.js b/addons_extensions/web_gantt/static/src/gantt_controller.js
new file mode 100644
index 000000000..fc3b869d3
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_controller.js
@@ -0,0 +1,193 @@
+import { _t } from "@web/core/l10n/translation";
+import { Component, onWillUnmount, useEffect, useRef, useSubEnv } from "@odoo/owl";
+import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
+import { FormViewDialog } from "@web/views/view_dialogs/form_view_dialog";
+import { Layout } from "@web/search/layout";
+import { standardViewProps } from "@web/views/standard_view_props";
+import { useModelWithSampleData } from "@web/model/model";
+import { usePager } from "@web/search/pager_hook";
+import { useService } from "@web/core/utils/hooks";
+import { SearchBar } from "@web/search/search_bar/search_bar";
+import { useSearchBarToggler } from "@web/search/search_bar/search_bar_toggler";
+import { CogMenu } from "@web/search/cog_menu/cog_menu";
+import { CallbackRecorder, useSetupAction } from "@web/search/action_hook";
+
+export class GanttController extends Component {
+ static components = {
+ CogMenu,
+ Layout,
+ SearchBar,
+ };
+ static props = {
+ ...standardViewProps,
+ Model: Function,
+ Renderer: Function,
+ buttonTemplate: String,
+ modelParams: Object,
+ scrollPosition: { type: Object, optional: true },
+ };
+ static template = "web_gantt.GanttController";
+
+ setup() {
+ this.actionService = useService("action");
+ this.dialogService = useService("dialog");
+ this.orm = useService("orm");
+
+ useSubEnv({
+ getCurrentFocusDateCallBackRecorder: new CallbackRecorder(),
+ });
+
+ const rootRef = useRef("root");
+
+ this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);
+ useSetupAction({
+ rootRef,
+ getLocalState: () => {
+ return { metaData: this.model.metaData, displayParams: this.model.displayParams };
+ },
+ });
+
+ onWillUnmount(() => this.closeDialog?.());
+
+ usePager(() => {
+ const { groupedBy, pagerLimit, pagerOffset } = this.model.metaData;
+ const { count } = this.model.data;
+ if (pagerLimit !== null && groupedBy.length) {
+ return {
+ offset: pagerOffset,
+ limit: pagerLimit,
+ total: count,
+ onUpdate: async ({ offset, limit }) => {
+ await this.model.updatePagerParams({ offset, limit });
+ },
+ };
+ }
+ });
+
+ useEffect(
+ (showNoContentHelp) => {
+ if (showNoContentHelp) {
+ const realRows = [
+ ...rootRef.el.querySelectorAll(
+ ".o_gantt_row_header:not(.o_sample_data_disabled)"
+ ),
+ ];
+ // interactive rows created in extensions (fromServer undefined)
+ const headerContainerWidth =
+ rootRef.el.querySelector(".o_gantt_header_groups").clientHeight +
+ rootRef.el.querySelector(".o_gantt_header_columns").clientHeight;
+
+ const offset = realRows.reduce(
+ (current, el) => current + el.clientHeight,
+ headerContainerWidth
+ );
+
+ const noContentHelperEl = rootRef.el.querySelector(".o_view_nocontent");
+ noContentHelperEl.style.top = `${offset}px`;
+ }
+ },
+ () => [this.showNoContentHelp]
+ );
+ this.searchBarToggler = useSearchBarToggler();
+ }
+
+ get className() {
+ if (this.env.isSmall) {
+ const classList = (this.props.className || "").split(" ");
+ classList.push("o_action_delegate_scroll");
+ return classList.join(" ");
+ }
+ return this.props.className;
+ }
+
+ get showNoContentHelp() {
+ return this.model.useSampleModel;
+ }
+
+ /**
+ * @param {Record} [context]
+ */
+ create(context) {
+ const { createAction } = this.model.metaData;
+ if (createAction) {
+ this.actionService.doAction(createAction, {
+ additionalContext: context,
+ onClose: () => {
+ this.model.fetchData();
+ },
+ });
+ } else {
+ this.openDialog({ context });
+ }
+ }
+
+ /**
+ * Opens dialog to add/edit/view a record
+ *
+ * @param {Record} props FormViewDialog props
+ * @param {Record} [options={}]
+ */
+ openDialog(props, options = {}) {
+ const { canDelete, canEdit, resModel, formViewId: viewId } = this.model.metaData;
+
+ const title = props.title || (props.resId ? _t("Open") : _t("Create"));
+
+ let removeRecord;
+ if (canDelete && props.resId) {
+ removeRecord = () => {
+ return new Promise((resolve) => {
+ this.dialogService.add(ConfirmationDialog, {
+ body: _t("Are you sure to delete this record?"),
+ confirm: async () => {
+ await this.orm.unlink(resModel, [props.resId]);
+ resolve();
+ },
+ cancel: () => {},
+ });
+ });
+ };
+ }
+
+ this.closeDialog = this.dialogService.add(
+ FormViewDialog,
+ {
+ title,
+ resModel,
+ viewId,
+ resId: props.resId,
+ size: props.size,
+ mode: canEdit ? "edit" : "readonly",
+ context: props.context,
+ removeRecord,
+ },
+ {
+ ...options,
+ onClose: () => {
+ this.closeDialog = null;
+ this.model.fetchData();
+ },
+ }
+ );
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ onAddClicked() {
+ const { scale } = this.model.metaData;
+ const focusDate = this.getCurrentFocusDate();
+ const start = focusDate.startOf(scale.unit);
+ const stop = focusDate.endOf(scale.unit).plus({ millisecond: 1 });
+ const context = this.model.getDialogContext({ start, stop, withDefault: true });
+ this.create(context);
+ }
+
+ getCurrentFocusDate() {
+ const { callbacks } = this.env.getCurrentFocusDateCallBackRecorder;
+ if (callbacks.length) {
+ return callbacks[0]();
+ }
+ return this.model.metaData.focusDate;
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_controller.xml b/addons_extensions/web_gantt/static/src/gantt_controller.xml
new file mode 100644
index 000000000..aa798f22b
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_controller.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_helpers.js b/addons_extensions/web_gantt/static/src/gantt_helpers.js
new file mode 100644
index 000000000..173aa08c1
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_helpers.js
@@ -0,0 +1,745 @@
+import { onWillUnmount, status, useComponent, useEffect, useEnv } from "@odoo/owl";
+import { getEndOfLocalWeek, getStartOfLocalWeek } from "@web/core/l10n/dates";
+import { makePopover, usePopover } from "@web/core/popover/popover_hook";
+import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder_owl";
+import { useService } from "@web/core/utils/hooks";
+import { clamp } from "@web/core/utils/numbers";
+import { pick } from "@web/core/utils/objects";
+import { GanttPopoverInDialog } from "./gantt_popover_in_dialog";
+
+/** @typedef {luxon.DateTime} DateTime */
+
+/**
+ * @param {number} target
+ * @param {number[]} values
+ * @returns {number}
+ */
+function closest(target, values) {
+ return values.reduce(
+ (prev, val) => (Math.abs(val - target) < Math.abs(prev - target) ? val : prev),
+ Infinity
+ );
+}
+
+/**
+ * Adds a time diff to a date keeping the same value even if the offset changed
+ * during the manipulation. This is typically needed with timezones using DayLight
+ * Saving offset changes.
+ *
+ * @example dateAddFixedOffset(luxon.DateTime.local(), { hour: 1 });
+ * @param {DateTime} date
+ * @param {Record} plusParams
+ */
+export function dateAddFixedOffset(date, plusParams) {
+ const shouldApplyOffset = Object.keys(plusParams).some((key) =>
+ /^(hour|minute|second)s?$/i.test(key)
+ );
+ const result = date.plus(plusParams);
+ if (shouldApplyOffset) {
+ const initialOffset = date.offset;
+ const diff = initialOffset - result.offset;
+ if (diff) {
+ const adjusted = result.plus({ minute: diff });
+ return adjusted.offset === initialOffset ? result : adjusted;
+ }
+ }
+ return result;
+}
+
+export function diffColumn(col1, col2, unit) {
+ return col2.diff(col1, unit).values[`${unit}s`];
+}
+
+export function getRangeFromDate(rangeId, date) {
+ const startDate = localStartOf(date, rangeId);
+ const stopDate = startDate.plus({ [rangeId]: 1 }).minus({ day: 1 });
+ return { focusDate: date, startDate, stopDate, rangeId };
+}
+
+export function localStartOf(date, unit) {
+ return unit === "week" ? getStartOfLocalWeek(date) : date.startOf(unit);
+}
+
+export function localEndOf(date, unit) {
+ return unit === "week" ? getEndOfLocalWeek(date) : date.endOf(unit);
+}
+
+/**
+ * @param {number} cellPart
+ * @param {(0 | 1)[]} subSlotUnavailabilities
+ * @param {boolean} isToday
+ * @returns {string | null}
+ */
+export function getCellColor(cellPart, subSlotUnavailabilities, isToday) {
+ const sum = subSlotUnavailabilities.reduce((acc, d) => acc + d);
+ if (!sum) {
+ return null;
+ }
+ switch (cellPart) {
+ case sum: {
+ return `background-color:${getCellPartColor(sum, isToday)}`;
+ }
+ case 2: {
+ const [c0, c1] = subSlotUnavailabilities.map((d) => getCellPartColor(d, isToday));
+ return `background:linear-gradient(90deg,${c0}49%,${c1}50%)`;
+ }
+ case 4: {
+ const [c0, c1, c2, c3] = subSlotUnavailabilities.map((d) =>
+ getCellPartColor(d, isToday)
+ );
+ return `background:linear-gradient(90deg,${c0}24%,${c1}25%,${c1}49%,${c2}50%,${c2}74%,${c3}75%)`;
+ }
+ }
+}
+
+/**
+ * @param {0 | 1} availability
+ * @param {boolean} isToday
+ * @returns {string}
+ */
+export function getCellPartColor(availability, isToday) {
+ if (availability) {
+ return "var(--Gantt__DayOff-background-color)";
+ } else if (isToday) {
+ return "var(--Gantt__DayOffToday-background-color)";
+ } else {
+ return "var(--Gantt__Day-background-color)";
+ }
+}
+
+/**
+ * @param {number | [number, string]} value
+ * @returns {number}
+ */
+export function getColorIndex(value) {
+ if (typeof value === "number") {
+ return Math.round(value) % NB_GANTT_RECORD_COLORS;
+ } else if (Array.isArray(value)) {
+ return value[0] % NB_GANTT_RECORD_COLORS;
+ }
+ return 0;
+}
+
+/**
+ * Intervals are supposed to intersect (intersection duration >= 1 milliseconds)
+ *
+ * @param {[DateTime, DateTime]} interval
+ * @param {[DateTime, DateTime]} otherInterval
+ * @returns {[DateTime, DateTime]}
+ */
+export function getIntersection(interval, otherInterval) {
+ const [start, end] = interval;
+ const [otherStart, otherEnd] = otherInterval;
+ return [start >= otherStart ? start : otherStart, end <= otherEnd ? end : otherEnd];
+}
+
+/**
+ * Computes intersection of a closed interval with a union of closed intervals ordered and disjoint
+ * = a union of intersections
+ *
+ * @param {[DateTime, DateTime]} interval
+ * @param {[DateTime, DateTime]} intervals
+ * @returns {[DateTime, DateTime][]}
+ */
+export function getUnionOfIntersections(interval, intervals) {
+ const [start, end] = interval;
+ const intersecting = intervals.filter((otherInterval) => {
+ const [otheStart, otherEnd] = otherInterval;
+ return otherEnd > start && end > otheStart;
+ });
+ const len = intersecting.length;
+ if (len === 0) {
+ return [];
+ }
+ const union = [];
+ const first = getIntersection(interval, intersecting[0]);
+ union.push(first);
+ if (len >= 2) {
+ const last = getIntersection(interval, intersecting[len - 1]);
+ union.push(...intersecting.slice(1, len - 1), last);
+ }
+ return union;
+}
+
+/**
+ * @param {Object} params
+ * @param {Ref} params.ref
+ * @param {string} params.selector
+ * @param {string} params.related
+ * @param {string} params.className
+ */
+export function useMultiHover({ ref, selector, related, className }) {
+ /**
+ * @param {HTMLElement} el
+ */
+ const findSiblings = (el) =>
+ ref.el.querySelectorAll(
+ related
+ .map((attr) => `[${attr}='${el.getAttribute(attr).replace(/'/g, "\\'")}']`)
+ .join("")
+ );
+
+ /**
+ * @param {PointerEvent} ev
+ */
+ const onPointerEnter = (ev) => {
+ for (const sibling of findSiblings(ev.target)) {
+ sibling.classList.add(...classList);
+ classedEls.add(sibling);
+ }
+ };
+
+ /**
+ * @param {PointerEvent} ev
+ */
+ const onPointerLeave = (ev) => {
+ for (const sibling of findSiblings(ev.target)) {
+ sibling.classList.remove(...classList);
+ classedEls.delete(sibling);
+ }
+ };
+
+ const classList = className.split(/\s+/g);
+ const classedEls = new Set();
+
+ useEffect(
+ (...targets) => {
+ if (targets.length) {
+ for (const target of targets) {
+ target.addEventListener("pointerenter", onPointerEnter);
+ target.addEventListener("pointerleave", onPointerLeave);
+ }
+ return () => {
+ for (const el of classedEls) {
+ el.classList.remove(...classList);
+ }
+ classedEls.clear();
+ for (const target of targets) {
+ target.removeEventListener("pointerenter", onPointerEnter);
+ target.removeEventListener("pointerleave", onPointerLeave);
+ }
+ };
+ }
+ },
+ () => [...ref.el.querySelectorAll(selector)]
+ );
+}
+
+const NB_GANTT_RECORD_COLORS = 12;
+
+function getElementCenter(el) {
+ const { x, y, width, height } = el.getBoundingClientRect();
+ return {
+ x: x + width / 2,
+ y: y + height / 2,
+ };
+}
+
+// Resizable hook handles
+
+const HANDLE_CLASS_START = "o_handle_start";
+const HANDLE_CLASS_END = "o_handle_end";
+const handles = {
+ start: document.createElement("div"),
+ end: document.createElement("div"),
+};
+
+// Draggable hooks
+
+export const useGanttConnectorDraggable = makeDraggableHook({
+ name: "useGanttConnectorDraggable",
+ acceptedParams: {
+ parentWrapper: [String],
+ },
+ onComputeParams({ ctx, params }) {
+ ctx.parentWrapper = params.parentWrapper;
+ ctx.followCursor = false;
+ },
+ onDragStart: ({ ctx, addStyle }) => {
+ const { current } = ctx;
+ const parent = current.element.closest(ctx.parentWrapper);
+ if (!parent) {
+ return;
+ }
+ for (const otherParent of ctx.ref.el.querySelectorAll(ctx.parentWrapper)) {
+ if (otherParent !== parent) {
+ addStyle(otherParent, { pointerEvents: "auto" });
+ }
+ }
+ return { sourcePill: parent, ...current.connectorCenter };
+ },
+ onDrag: ({ ctx }) => {
+ ctx.current.connectorCenter = getElementCenter(ctx.current.element);
+ return pick(ctx.current, "connectorCenter");
+ },
+ onDragEnd: ({ ctx }) => pick(ctx.current, "element"),
+ onDrop: ({ ctx, target }) => {
+ const { current } = ctx;
+ const parent = current.element.closest(ctx.parentWrapper);
+ const targetParent = target.closest(ctx.parentWrapper);
+ if (!targetParent || targetParent === parent) {
+ return;
+ }
+ return { target: targetParent };
+ },
+ onWillStartDrag: ({ ctx }) => {
+ ctx.current.connectorCenter = getElementCenter(ctx.current.element);
+ },
+});
+
+function getCoordinate(style, name) {
+ return +style.getPropertyValue(name).slice(1);
+}
+
+function getColumnStart(style) {
+ return getCoordinate(style, "grid-column-start");
+}
+
+function getColumnEnd(style) {
+ return getCoordinate(style, "grid-column-end");
+}
+
+export const useGanttDraggable = makeDraggableHook({
+ name: "useGanttDraggable",
+ acceptedParams: {
+ cells: [String, Function],
+ cellDragClassName: [String, Function],
+ ghostClassName: [String, Function],
+ hoveredCell: [Object],
+ addStickyCoordinates: [Function],
+ },
+ onComputeParams({ ctx, params }) {
+ ctx.cellSelector = params.cells;
+ ctx.ghostClassName = params.ghostClassName;
+ ctx.cellDragClassName = params.cellDragClassName;
+ ctx.hoveredCell = params.hoveredCell;
+ ctx.addStickyCoordinates = params.addStickyCoordinates;
+ },
+ onDragStart({ ctx }) {
+ const { current, ghostClassName } = ctx;
+ current.element.before(current.placeHolder);
+ if (ghostClassName) {
+ current.placeHolder.classList.add(ghostClassName);
+ }
+ return { pill: current.element };
+ },
+ onDrag({ ctx, addStyle }) {
+ const { cellSelector, current, hoveredCell } = ctx;
+ let { el: cell, part } = hoveredCell;
+
+ const isDifferentCell = cell !== current.cell.el;
+ const isDifferentPart = part !== current.cell.part;
+
+ if (cell && !cell.matches(cellSelector)) {
+ cell = null; // Not a cell
+ }
+
+ current.cell.el = cell;
+ current.cell.part = part;
+
+ if (cell) {
+ // Recompute cell style if in a different cell
+ if (isDifferentCell) {
+ const style = getComputedStyle(cell);
+ current.cell.gridRow = style.getPropertyValue("grid-row");
+ current.cell.gridColumnStart = getColumnStart(style) + current.gridColumnOffset;
+ }
+ // Assign new grid coordinates if in different cell or different cell part
+ if (isDifferentCell || isDifferentPart) {
+ const { pillSpan } = current;
+ const { gridRow, gridColumnStart: start } = current.cell;
+ const gridColumnStart = clamp(start + part, 1, current.maxGridColumnStart);
+ const gridColumnEnd = gridColumnStart + pillSpan;
+
+ addStyle(current.cellGhost, {
+ gridRow,
+ gridColumn: `c${gridColumnStart} / c${gridColumnEnd}`,
+ });
+
+ const [gridRowStart, gridRowEnd] = /r(\d+) \/ r(\d+)/g.exec(gridRow).slice(1);
+ ctx.addStickyCoordinates(
+ [gridRowStart, gridRowEnd],
+ [gridColumnStart, gridColumnEnd]
+ );
+ current.cell.col = gridColumnStart;
+ }
+ } else {
+ current.cell.col = null;
+ }
+
+ // Attach or remove cell ghost
+ if (isDifferentCell) {
+ if (cell) {
+ cell.after(current.cellGhost);
+ } else {
+ current.cellGhost.remove();
+ }
+ }
+
+ return { pill: current.element };
+ },
+ onDragEnd({ ctx }) {
+ return { pill: ctx.current.element };
+ },
+ onDrop({ ctx }) {
+ const { cell, element, initialCol } = ctx.current;
+ if (cell.col !== null) {
+ return {
+ pill: element,
+ cell: cell.el,
+ diff: cell.col - initialCol,
+ };
+ }
+ },
+ onWillStartDrag({ ctx, addCleanup, addClass }) {
+ const { current } = ctx;
+ const { el: cell, part } = ctx.hoveredCell;
+
+ current.placeHolder = current.element.cloneNode(true);
+ current.cellGhost = document.createElement("div");
+ current.cellGhost.className = ctx.cellDragClassName;
+ current.cell = { el: null, index: null, part: 0 };
+
+ const gridStyle = getComputedStyle(cell.parentElement);
+ const pillStyle = getComputedStyle(current.element);
+ const cellStyle = getComputedStyle(cell);
+
+ const gridTemplateColumns = gridStyle.getPropertyValue("grid-template-columns");
+ const pGridColumnStart = getColumnStart(pillStyle);
+ const pGridColumnEnd = getColumnEnd(pillStyle);
+ const cGridColumnStart = getColumnStart(cellStyle) + part;
+
+ let highestGridCol;
+ for (const e of gridTemplateColumns.split(/\s+/).reverse()) {
+ const res = /\[c(\d+)\]/g.exec(e);
+ if (res) {
+ highestGridCol = +res[1];
+ break;
+ }
+ }
+
+ const pillSpan = pGridColumnEnd - pGridColumnStart;
+
+ current.initialCol = pGridColumnStart;
+ current.maxGridColumnStart = highestGridCol - pillSpan;
+ current.gridColumnOffset = pGridColumnStart - cGridColumnStart;
+ current.pillSpan = pillSpan;
+
+ addClass(ctx.ref.el, "pe-auto");
+ addCleanup(() => {
+ current.placeHolder.remove();
+ current.cellGhost.remove();
+ });
+ },
+});
+
+export const useGanttUndraggable = makeDraggableHook({
+ name: "useGanttUndraggable",
+ onDragStart({ ctx }) {
+ return { pill: ctx.current.element };
+ },
+ onDragEnd({ ctx }) {
+ return { pill: ctx.current.element };
+ },
+ onWillStartDrag({ ctx, addCleanup, addClass, addStyle, getRect }) {
+ const { x, y, width, height } = getRect(ctx.current.element);
+ ctx.current.container = document.createElement("div");
+
+ addClass(ctx.ref.el, "pe-auto");
+ addStyle(ctx.current.container, {
+ position: "fixed",
+ left: `${x}px`,
+ top: `${y}px`,
+ width: `${width}px`,
+ height: `${height}px`,
+ });
+
+ ctx.current.element.after(ctx.current.container);
+ addCleanup(() => ctx.current.container.remove());
+ },
+});
+
+export const useGanttResizable = makeDraggableHook({
+ name: "useGanttResizable",
+ requiredParams: ["handles"],
+ acceptedParams: {
+ innerPills: [String, Function],
+ handles: [String, Function],
+ hoveredCell: [Object],
+ rtl: [Boolean, Function],
+ cells: [String, Function],
+ precision: [Number, Function],
+ showHandles: [Function],
+ },
+ onComputeParams({ ctx, params, addCleanup, addEffectCleanup, getRect }) {
+ const onElementPointerEnter = (ev) => {
+ if (ctx.dragging || ctx.willDrag) {
+ return;
+ }
+
+ const pill = ev.target;
+ const innerPill = pill.querySelector(params.innerPills);
+
+ const pillRect = getRect(innerPill);
+
+ for (const el of Object.values(handles)) {
+ el.style.height = `${pillRect.height}px`;
+ }
+
+ const showHandles = params.showHandles ? params.showHandles(pill) : {};
+ if ("start" in showHandles && !showHandles.start) {
+ handles.start.remove();
+ } else {
+ innerPill.appendChild(handles.start);
+ }
+ if ("end" in showHandles && !showHandles.end) {
+ handles.end.remove();
+ } else {
+ innerPill.appendChild(handles.end);
+ }
+ };
+
+ const onElementPointerLeave = () => {
+ const remove = () => Object.values(handles).forEach((h) => h.remove());
+ if (ctx.dragging || ctx.current.element) {
+ addCleanup(remove);
+ } else {
+ remove();
+ }
+ };
+
+ ctx.cellSelector = params.cells;
+ ctx.hoveredCell = params.hoveredCell;
+ ctx.precision = params.precision;
+ ctx.rtl = params.rtl;
+
+ for (const el of ctx.ref.el.querySelectorAll(params.elements)) {
+ el.addEventListener("pointerenter", onElementPointerEnter);
+ el.addEventListener("pointerleave", onElementPointerLeave);
+ addEffectCleanup(() => {
+ el.removeEventListener("pointerenter", onElementPointerEnter);
+ el.removeEventListener("pointerleave", onElementPointerLeave);
+ });
+ }
+
+ handles.start.className = `${params.handles} ${HANDLE_CLASS_START}`;
+ handles.start.style.cursor = `${params.rtl ? "e" : "w"}-resize`;
+
+ handles.end.className = `${params.handles} ${HANDLE_CLASS_END}`;
+ handles.end.style.cursor = `${params.rtl ? "w" : "e"}-resize`;
+
+ // Override "full" and "element" selectors: we want the draggable feature
+ // to apply to the handles
+ ctx.pillSelector = ctx.elementSelector;
+ ctx.fullSelector = ctx.elementSelector = `.${params.handles}`;
+
+ // Force the handles to stay in place
+ ctx.followCursor = false;
+ },
+ onDragStart({ ctx, addStyle }) {
+ addStyle(ctx.current.pill, { zIndex: 15 });
+ return { pill: ctx.current.pill };
+ },
+ onDrag({ ctx, addStyle, getRect }) {
+ const { cellSelector, current, hoveredCell, pointer, precision, rtl, ref } = ctx;
+ let { el: cell, part } = hoveredCell;
+
+ const point = [pointer.x, current.initialPosition.y];
+ if (!cell) {
+ let rect;
+ cell = document.elementsFromPoint(...point).find((el) => el.matches(cellSelector));
+ if (!cell) {
+ const cells = Array.from(ref.el.querySelectorAll(".o_gantt_cells .o_gantt_cell"));
+ if (pointer.x < current.initialPosition.x) {
+ cell = rtl ? cells.at(-1) : cells[0];
+ } else {
+ cell = rtl ? cells[0] : cells.at(-1);
+ }
+ rect = getRect(cell);
+ point[0] = rtl ? rect.right - 1 : rect.left + 1;
+ } else {
+ rect = getRect(cell);
+ }
+ const x = Math.floor(rect.x);
+ const width = Math.floor(rect.width);
+ part = Math.floor((point[0] - x) / (width / precision));
+ }
+
+ const cellStyle = getComputedStyle(cell);
+ const cGridColStart = getColumnStart(cellStyle);
+
+ const { x, width } = getRect(cell);
+ const coef = ((rtl ? -1 : 1) * width) / precision;
+ const startBorder = (rtl ? x + width : x) + part * coef;
+ const endBorder = startBorder + coef;
+
+ const theClosest = closest(point[0], [startBorder, endBorder]);
+
+ let diff =
+ cGridColStart +
+ part +
+ (theClosest === startBorder ? 0 : 1) -
+ (current.isStart ? current.firstCol : current.lastCol);
+
+ if (diff === current.lastDiff) {
+ return;
+ }
+
+ if (current.isStart) {
+ diff = Math.min(diff, current.initialDiff - 1);
+ addStyle(current.pill, { "grid-column-start": `c${current.firstCol + diff}` });
+ } else {
+ diff = Math.max(diff, 1 - current.initialDiff);
+ addStyle(current.pill, { "grid-column-end": `c${current.lastCol + diff}` });
+ }
+ current.lastDiff = diff;
+
+ const isLeftHandle = rtl ? !current.isStart : current.isStart;
+ const grabbedHandle = isLeftHandle ? "left" : "right";
+ diff = current.isStart ? -diff : diff;
+ return { pill: current.pill, grabbedHandle, diff };
+ },
+ onDragEnd({ ctx }) {
+ const { current, pillSelector } = ctx;
+ const pill = current.element.closest(pillSelector);
+ return { pill };
+ },
+ onDrop({ ctx }) {
+ const { current } = ctx;
+
+ if (!current.lastDiff) {
+ return;
+ }
+
+ const direction = current.isStart ? "start" : "end";
+ return { pill: current.pill, diff: current.lastDiff, direction };
+ },
+ onWillStartDrag({ ctx, addClass }) {
+ const { current, pillSelector } = ctx;
+
+ const pill = ctx.current.element.closest(pillSelector);
+ current.pill = pill;
+
+ const pillStyle = getComputedStyle(pill);
+ current.firstCol = getColumnStart(pillStyle);
+ current.lastCol = getColumnEnd(pillStyle);
+ current.initialDiff = current.lastCol - current.firstCol;
+
+ ctx.cursor = getComputedStyle(current.element).cursor;
+
+ current.isStart = current.element.classList.contains(HANDLE_CLASS_START);
+
+ addClass(ctx.ref.el, "pe-auto");
+ },
+});
+
+function getCellsOnRow(refEl, rowId) {
+ return refEl.querySelectorAll(
+ `.o_gantt_cell:not(.o_gantt_group)[data-row-id='${CSS.escape(rowId)}']`
+ );
+}
+
+function getMinMax(a, b) {
+ return a <= b ? [a, b] : [b, a];
+}
+
+export const useGanttSelectable = makeDraggableHook({
+ name: "useGanttSelectable",
+ acceptedParams: {
+ hoveredCell: [Object],
+ rtl: [Boolean, Function],
+ },
+ onComputeParams({ ctx, params }) {
+ ctx.followCursor = false;
+ ctx.hoveredCell = params.hoveredCell;
+ ctx.rtl = params.rtl;
+ },
+ onDrag({ ctx, addClass, getRect, removeClass }) {
+ const { current, hoveredCell, pointer, ref, rtl } = ctx;
+ let { el: cell } = hoveredCell;
+ if (!cell) {
+ const point = [pointer.x, current.initialPosition.y];
+ cell = document.elementsFromPoint(...point).find((el) => el.matches(".o_gantt_cell"));
+ if (!cell) {
+ const cells = Array.from(ref.el.querySelectorAll(".o_gantt_cells .o_gantt_cell"));
+ if (pointer.x < current.initialPosition.x) {
+ cell = rtl ? cells.at(-1) : cells[0];
+ } else {
+ cell = rtl ? cells[0] : cells.at(-1);
+ }
+ }
+ }
+ const col = +cell.dataset.col;
+ const lastSelectedCol = current.lastSelectedCol;
+ current.lastSelectedCol = col;
+ if (lastSelectedCol === col) {
+ return;
+ }
+ const [startCol, stopCol] = getMinMax(current.initialCol, col);
+ for (const cell of getCellsOnRow(ref.el, current.rowId)) {
+ const cellCol = +cell.dataset.col;
+ if (cellCol < startCol || cellCol > stopCol) {
+ removeClass(cell, "o_drag_hover");
+ } else {
+ addClass(cell, "o_drag_hover");
+ }
+ }
+ },
+ onDrop({ ctx }) {
+ const { current } = ctx;
+ const { rowId, initialCol, lastSelectedCol } = current;
+ const [startCol, stopCol] = getMinMax(initialCol, lastSelectedCol);
+ return { rowId, startCol, stopCol };
+ },
+ onWillStartDrag({ ctx, addClass }) {
+ const { current, hoveredCell, ref } = ctx;
+ const { el: cell } = hoveredCell;
+ current.rowId = cell.dataset.rowId;
+ current.initialCol = +cell.dataset.col;
+ addClass(ref.el, "pe-auto");
+ addClass(cell, "pe-auto");
+ },
+});
+
+/**
+ * Same as usePopover, but replaces the popover by a dialog when display size is small.
+ *
+ * @param {typeof import("@odoo/owl").Component} component
+ * @param {import("@web/core/popover/popover_service").PopoverServiceAddOptions} [options]
+ * @returns {import("@web/core/popover/popover_hook").PopoverHookReturnType}
+ */
+export function useGanttResponsivePopover(dialogTitle, component, options = {}) {
+ const dialogService = useService("dialog");
+ const env = useEnv();
+ const owner = useComponent();
+ const popover = usePopover(component, options);
+ const onClose = () => {
+ if (status(owner) !== "destroyed") {
+ options.onClose?.();
+ }
+ };
+ const dialogAddFn = (_, comp, props, options) => dialogService.add(comp, props, options);
+ const popoverInDialog = makePopover(dialogAddFn, GanttPopoverInDialog, { onClose });
+ const ganttReponsivePopover = {
+ open: (target, props) => {
+ if (env.isSmall) {
+ popoverInDialog.open(target, {
+ component: component,
+ componentProps: props,
+ dialogTitle,
+ });
+ } else {
+ popover.open(target, props);
+ }
+ },
+ close: () => {
+ popover.close();
+ popoverInDialog.close();
+ },
+ get isOpen() {
+ return popover.isOpen || popoverInDialog.isOpen;
+ },
+ };
+ onWillUnmount(ganttReponsivePopover.close);
+ return ganttReponsivePopover;
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_mock_server.js b/addons_extensions/web_gantt/static/src/gantt_mock_server.js
new file mode 100644
index 000000000..c9aa23c17
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_mock_server.js
@@ -0,0 +1,35 @@
+import { registry } from "@web/core/registry";
+
+function _mockGetGanttData(_, { model, kwargs }) {
+ const lazy = !kwargs.limit && !kwargs.offset && kwargs.groupby.length === 1;
+ const { groups, length } = this.mockWebReadGroup(model, {
+ ...kwargs,
+ lazy,
+ fields: ["__record_ids:array_agg(id)"],
+ });
+
+ const recordIds = [];
+ for (const group of groups) {
+ recordIds.push(...(group.__record_ids || []));
+ }
+
+ const { records } = this.mockWebSearchReadUnity(model, [], {
+ domain: [["id", "in", recordIds]],
+ context: kwargs.context,
+ specification: kwargs.read_specification,
+ });
+
+ const unavailabilities = {};
+ for (const fieldName of kwargs.unavailability_fields || []) {
+ unavailabilities[fieldName] = {};
+ }
+
+ const progress_bars = {};
+ for (const fieldName of kwargs.progress_bar_fields || []) {
+ progress_bars[fieldName] = {};
+ }
+
+ return { groups, length, records, unavailabilities, progress_bars };
+}
+
+registry.category("mock_server").add("get_gantt_data", _mockGetGanttData);
diff --git a/addons_extensions/web_gantt/static/src/gantt_model.js b/addons_extensions/web_gantt/static/src/gantt_model.js
new file mode 100644
index 000000000..13fcc0bef
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_model.js
@@ -0,0 +1,1121 @@
+import { browser } from "@web/core/browser/browser";
+import { Domain } from "@web/core/domain";
+import { _t } from "@web/core/l10n/translation";
+import {
+ deserializeDate,
+ deserializeDateTime,
+ serializeDate,
+ serializeDateTime,
+} from "@web/core/l10n/dates";
+import { x2ManyCommands } from "@web/core/orm_service";
+import { registry } from "@web/core/registry";
+import { groupBy, unique } from "@web/core/utils/arrays";
+import { KeepLast, Mutex } from "@web/core/utils/concurrency";
+import { pick } from "@web/core/utils/objects";
+import { sprintf } from "@web/core/utils/strings";
+import { Model } from "@web/model/model";
+import { formatFloatTime, formatPercentage } from "@web/views/fields/formatters";
+import { getRangeFromDate, localStartOf } from "./gantt_helpers";
+
+const { DateTime } = luxon;
+
+/**
+ * @typedef {luxon.DateTime} DateTime
+ * @typedef {`[{${string}}]`} RowId
+ * @typedef {import("./gantt_arch_parser").Scale} Scale
+ * @typedef {import("./gantt_arch_parser").ScaleId} ScaleId
+ *
+ * @typedef ConsolidationParams
+ * @property {string} excludeField
+ * @property {string} field
+ * @property {string} [maxField]
+ * @property {string} [maxValue]
+ *
+ * @typedef Data
+ * @property {Record[]} records
+ * @property {Row[]} rows
+ *
+ * @typedef Field
+ * @property {string} name
+ * @property {string} type
+ * @property {[any, string][]} [selection]
+ *
+ * @typedef MetaData
+ * @property {ConsolidationParams} consolidationParams
+ * @property {string} dateStartField
+ * @property {string} dateStopField
+ * @property {string[]} decorationFields
+ * @property {ScaleId} defaultScale
+ * @property {string} dependencyField
+ * @property {boolean} dynamicRange
+ * @property {Record} fields
+ * @property {DateTime} focusDate
+ * @property {number | false} formViewId
+ * @property {string[]} groupedBy
+ * @property {Element | null} popoverTemplate
+ * @property {string} resModel
+ * @property {Scale} scale
+ * @property {Scale[]} scales
+ * @property {DateTime} startDate
+ * @property {DateTime} stopDate
+ *
+ * @typedef ProgressBar
+ * @property {number} value_formatted
+ * @property {number} max_value_formatted
+ * @property {number} ratio
+ * @property {string} warning
+ *
+ * @typedef Row
+ * @property {RowId} id
+ * @property {boolean} consolidate
+ * @property {boolean} fromServer
+ * @property {string[]} groupedBy
+ * @property {string} groupedByField
+ * @property {number} groupLevel
+ * @property {string} name
+ * @property {number[]} recordIds
+ * @property {ProgressBar} [progressBar]
+ * @property {number | false} resId
+ * @property {Row[]} [rows]
+ */
+
+function firstColumnBefore(date, unit) {
+ return localStartOf(date, unit);
+}
+
+function firstColumnAfter(date, unit) {
+ const start = localStartOf(date, unit);
+ if (date.equals(start)) {
+ return date;
+ }
+ return start.plus({ [unit]: 1 });
+}
+
+/**
+ * @param {Record} fields
+ * @param {Record} values
+ */
+export function parseServerValues(fields, values) {
+ /** @type {Record} */
+ const parsedValues = {};
+ if (!values) {
+ return parsedValues;
+ }
+ for (const fieldName in values) {
+ const field = fields[fieldName];
+ const value = values[fieldName];
+ switch (field.type) {
+ case "date": {
+ parsedValues[fieldName] = value ? deserializeDate(value) : false;
+ break;
+ }
+ case "datetime": {
+ parsedValues[fieldName] = value ? deserializeDateTime(value) : false;
+ break;
+ }
+ case "selection": {
+ if (value === false) {
+ // process selection: convert false to 0, if 0 is a valid key
+ const hasKey0 = field.selection.some((option) => option[0] === 0);
+ parsedValues[fieldName] = hasKey0 ? 0 : value;
+ } else {
+ parsedValues[fieldName] = value;
+ }
+ break;
+ }
+ case "many2one": {
+ parsedValues[fieldName] = value ? [value.id, value.display_name] : false;
+ break;
+ }
+ default: {
+ parsedValues[fieldName] = value;
+ }
+ }
+ }
+ return parsedValues;
+}
+
+export class GanttModel extends Model {
+ static services = ["notification"];
+
+ setup(params, services) {
+ this.notification = services.notification;
+
+ /** @type {Data} */
+ this.data = {};
+ /** @type {MetaData} */
+ this.metaData = params.metaData;
+ this.displayParams = params.displayParams;
+
+ this.searchParams = null;
+
+ /** @type {Set} */
+ this.closedRows = new Set();
+
+ // concurrency management
+ this.keepLast = new KeepLast();
+ this.mutex = new Mutex();
+ /** @type {MetaData | null} */
+ this._nextMetaData = null;
+ }
+
+ /**
+ * @param {SearchParams} searchParams
+ */
+ async load(searchParams) {
+ this.searchParams = searchParams;
+
+ const metaData = this._buildMetaData();
+
+ const params = {
+ groupedBy: this._getGroupedBy(metaData, searchParams),
+ pagerOffset: 0,
+ };
+
+ if (!metaData.scale || !metaData.startDate || !metaData.stopDate) {
+ Object.assign(
+ params,
+ this._getInitialRangeParams(this._buildMetaData(params), searchParams)
+ );
+ }
+
+ await this._fetchData(this._buildMetaData(params));
+ }
+
+ //-------------------------------------------------------------------------
+ // Public
+ //-------------------------------------------------------------------------
+
+ collapseRows() {
+ const collapse = (rows) => {
+ for (const row of rows) {
+ this.closedRows.add(row.id);
+ if (row.rows) {
+ collapse(row.rows);
+ }
+ }
+ };
+ collapse(this.data.rows);
+ this.notify();
+ }
+
+ /**
+ * Create a copy of a task with defaults determined by schedule.
+ *
+ * @param {number} id
+ * @param {Record} schedule
+ * @param {(result: any) => any} [callback]
+ */
+ copy(id, schedule, callback) {
+ const { resModel } = this.metaData;
+ const { context } = this.searchParams;
+ const data = this._scheduleToData(schedule);
+ return this.mutex.exec(async () => {
+ const result = await this.orm.call(resModel, "copy", [[id]], {
+ context,
+ default: data,
+ });
+ if (callback) {
+ callback(result[0]);
+ }
+ this.fetchData();
+ });
+ }
+
+ /**
+ * Adds a dependency between masterId and slaveId (slaveId depends
+ * on masterId).
+ *
+ * @param {number} masterId
+ * @param {number} slaveId
+ */
+ async createDependency(masterId, slaveId) {
+ const { dependencyField, resModel } = this.metaData;
+ const writeCommand = {
+ [dependencyField]: [x2ManyCommands.link(masterId)],
+ };
+ await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));
+ await this.fetchData();
+ }
+
+ dateStartFieldIsDate(metaData = this.metaData) {
+ return metaData?.fields[metaData.dateStartField].type === "date";
+ }
+
+ dateStopFieldIsDate(metaData = this.metaData) {
+ return metaData?.fields[metaData.dateStopField].type === "date";
+ }
+
+ expandRows() {
+ this.closedRows.clear();
+ this.notify();
+ }
+
+ async fetchData(params) {
+ await this._fetchData(this._buildMetaData(params));
+ this.useSampleModel = false;
+ this.notify();
+ }
+
+ /**
+ * @param {Object} params
+ * @param {RowId} [params.rowId]
+ * @param {DateTime} [params.start]
+ * @param {DateTime} [params.stop]
+ * @param {boolean} [params.withDefault]
+ * @returns {Record}
+ */
+ getDialogContext(params) {
+ /** @type {Record} */
+ const context = { ...this.getSchedule(params) };
+
+ if (params.withDefault) {
+ for (const k in context) {
+ context[sprintf("default_%s", k)] = context[k];
+ }
+ }
+
+ return Object.assign({}, this.searchParams.context, context);
+ }
+
+ /**
+ * @param {Object} params
+ * @param {RowId} [params.rowId]
+ * @param {DateTime} [params.start]
+ * @param {DateTime} [params.stop]
+ * @returns {Record}
+ */
+ getSchedule({ rowId, start, stop } = {}) {
+ const { dateStartField, dateStopField, fields, groupedBy } = this.metaData;
+
+ /** @type {Record} */
+ const schedule = {};
+
+ if (start) {
+ schedule[dateStartField] = this.dateStartFieldIsDate()
+ ? serializeDate(start)
+ : serializeDateTime(start);
+ }
+ if (stop && dateStartField !== dateStopField) {
+ schedule[dateStopField] = this.dateStopFieldIsDate()
+ ? serializeDate(stop)
+ : serializeDateTime(stop);
+ }
+ if (rowId) {
+ const group = Object.assign({}, ...JSON.parse(rowId));
+ for (const fieldName of groupedBy) {
+ if (fieldName in group) {
+ const value = group[fieldName];
+ if (Array.isArray(value)) {
+ const { type } = fields[fieldName];
+ schedule[fieldName] = type === "many2many" ? [value[0]] : value[0];
+ } else {
+ schedule[fieldName] = value;
+ }
+ }
+ }
+ }
+
+ return schedule;
+ }
+
+ /**
+ * @override
+ * @returns {boolean}
+ */
+ hasData() {
+ return Boolean(this.data.records.length);
+ }
+
+ /**
+ * @param {RowId} rowId
+ * @returns {boolean}
+ */
+ isClosed(rowId) {
+ return this.closedRows.has(rowId);
+ }
+
+ /**
+ * Removes the dependency between masterId and slaveId (slaveId is no
+ * more dependent on masterId).
+ *
+ * @param {number} masterId
+ * @param {number} slaveId
+ */
+ async removeDependency(masterId, slaveId) {
+ const { dependencyField, resModel } = this.metaData;
+ const writeCommand = {
+ [dependencyField]: [x2ManyCommands.unlink(masterId)],
+ };
+ await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));
+ await this.fetchData();
+ }
+
+ /**
+ * Removes from 'data' the fields holding the same value as the records targetted
+ * by 'ids'.
+ *
+ * @template {Record} T
+ * @param {T} data
+ * @param {number[]} ids
+ * @returns {Partial}
+ */
+ removeRedundantData(data, ids) {
+ const records = this.data.records.filter((rec) => ids.includes(rec.id));
+ if (!records.length) {
+ return data;
+ }
+
+ /**
+ *
+ * @param {Record} record
+ * @param {Field} field
+ */
+ const isSameValue = (record, { name, type }) => {
+ const recordValue = record[name];
+ let newValue = data[name];
+ if (Array.isArray(newValue)) {
+ [newValue] = newValue;
+ }
+ if (Array.isArray(recordValue)) {
+ if (type === "many2many") {
+ return recordValue.includes(newValue);
+ } else {
+ return recordValue[0] === newValue;
+ }
+ } else if (type === "date") {
+ return serializeDate(recordValue) === newValue;
+ } else if (type === "datetime") {
+ return serializeDateTime(recordValue) === newValue;
+ } else {
+ return recordValue === newValue;
+ }
+ };
+
+ /** @type {Partial} */
+ const trimmed = { ...data };
+
+ for (const fieldName in data) {
+ const field = this.metaData.fields[fieldName];
+ if (records.every((rec) => isSameValue(rec, field))) {
+ // All the records already have the given value.
+ delete trimmed[fieldName];
+ }
+ }
+
+ return trimmed;
+ }
+
+ /**
+ * Reschedule a task to the given schedule.
+ *
+ * @param {number | number[]} ids
+ * @param {Record} schedule
+ * @param {(result: any) => any} [callback]
+ */
+ async reschedule(ids, schedule, callback) {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
+ }
+ const allData = this._scheduleToData(schedule);
+ const data = this.removeRedundantData(allData, ids);
+ const context = this._getRescheduleContext();
+ return this.mutex.exec(async () => {
+ try {
+ const result = await this._reschedule(ids, data, context);
+ if (callback) {
+ await callback(result);
+ }
+ } finally {
+ this.fetchData();
+ }
+ });
+ }
+
+ async _reschedule(ids, data, context) {
+ return this.orm.write(this.metaData.resModel, ids, data, {
+ context,
+ });
+ }
+
+ toggleHighlightPlannedFilter(ids) {}
+
+ /**
+ * Reschedule masterId or slaveId according to the direction
+ *
+ * @param {"forward" | "backward"} direction
+ * @param {number} masterId
+ * @param {number} slaveId
+ * @returns {Promise}
+ */
+ async rescheduleAccordingToDependency(
+ direction,
+ masterId,
+ slaveId,
+ rescheduleAccordingToDependencyCallback
+ ) {
+ const {
+ dateStartField,
+ dateStopField,
+ dependencyField,
+ dependencyInvertedField,
+ resModel,
+ } = this.metaData;
+
+ return await this.mutex.exec(async () => {
+ try {
+ const result = await this.orm.call(resModel, "web_gantt_reschedule", [
+ direction,
+ masterId,
+ slaveId,
+ dependencyField,
+ dependencyInvertedField,
+ dateStartField,
+ dateStopField,
+ ]);
+ if (rescheduleAccordingToDependencyCallback) {
+ await rescheduleAccordingToDependencyCallback(result);
+ }
+ } finally {
+ this.fetchData();
+ }
+ });
+ }
+
+ /**
+ * @param {string} rowId
+ */
+ toggleRow(rowId) {
+ if (this.isClosed(rowId)) {
+ this.closedRows.delete(rowId);
+ } else {
+ this.closedRows.add(rowId);
+ }
+ this.notify();
+ }
+
+ async toggleDisplayMode() {
+ this.displayParams.displayMode =
+ this.displayParams.displayMode === "dense" ? "sparse" : "dense";
+ this.notify();
+ }
+
+ async updatePagerParams({ limit, offset }) {
+ await this.fetchData({ pagerLimit: limit, pagerOffset: offset });
+ }
+
+ //-------------------------------------------------------------------------
+ // Protected
+ //-------------------------------------------------------------------------
+
+ /**
+ * Return a copy of this.metaData or of the last copy, extended with optional
+ * params. This is useful for async methods that need to modify this.metaData,
+ * but it can't be done in place directly for the model to be concurrency
+ * proof (so they work on a copy and commit it at the end).
+ *
+ * @protected
+ * @param {Object} params
+ * @param {DateTime} [params.focusDate]
+ * @param {DateTime} [params.startDate]
+ * @param {DateTime} [params.stopDate]
+ * @param {string[]} [params.groupedBy]
+ * @param {ScaleId} [params.scaleId]
+ * @returns {MetaData}
+ */
+ _buildMetaData(params = {}) {
+ this._nextMetaData = { ...(this._nextMetaData || this.metaData) };
+
+ if (params.groupedBy) {
+ this._nextMetaData.groupedBy = params.groupedBy;
+ }
+ if (params.scaleId) {
+ browser.localStorage.setItem(this._getLocalStorageKey(), params.scaleId);
+ this._nextMetaData.scale = { ...this._nextMetaData.scales[params.scaleId] };
+ }
+ if (params.focusDate) {
+ this._nextMetaData.focusDate = params.focusDate;
+ }
+ if (params.startDate) {
+ this._nextMetaData.startDate = params.startDate;
+ }
+ if (params.stopDate) {
+ this._nextMetaData.stopDate = params.stopDate;
+ }
+ if (params.rangeId) {
+ this._nextMetaData.rangeId = params.rangeId;
+ }
+
+ if ("pagerLimit" in params) {
+ this._nextMetaData.pagerLimit = params.pagerLimit;
+ }
+ if ("pagerOffset" in params) {
+ this._nextMetaData.pagerOffset = params.pagerOffset;
+ }
+
+ if ("scaleId" in params || "startDate" in params || "stopDate" in params) {
+ // we assume that scale, startDate, and stopDate are already set in this._nextMetaData
+
+ let exchange = false;
+ if (this._nextMetaData.startDate > this._nextMetaData.stopDate) {
+ exchange = true;
+ const temp = this._nextMetaData.startDate;
+ this._nextMetaData.startDate = this._nextMetaData.stopDate;
+ this._nextMetaData.stopDate = temp;
+ }
+ const { interval } = this._nextMetaData.scale;
+
+ const rightLimit = this._nextMetaData.startDate.plus({ year: 10, day: -1 });
+ if (this._nextMetaData.stopDate > rightLimit) {
+ if (exchange) {
+ this._nextMetaData.startDate = this._nextMetaData.stopDate.minus({
+ year: 10,
+ day: -1,
+ });
+ } else {
+ this._nextMetaData.stopDate = this._nextMetaData.startDate.plus({
+ year: 10,
+ day: -1,
+ });
+ }
+ }
+ this._nextMetaData.globalStart = firstColumnBefore(
+ this._nextMetaData.startDate,
+ interval
+ );
+ this._nextMetaData.globalStop = firstColumnAfter(
+ this._nextMetaData.stopDate.plus({ day: 1 }),
+ interval
+ );
+
+ if (params.currentFocusDate) {
+ this._nextMetaData.focusDate = params.currentFocusDate;
+ if (this._nextMetaData.focusDate < this._nextMetaData.startDate) {
+ this._nextMetaData.focusDate = this._nextMetaData.startDate;
+ } else if (this._nextMetaData.stopDate < this._nextMetaData.focusDate) {
+ this._nextMetaData.focusDate = this._nextMetaData.stopDate;
+ }
+ }
+ }
+
+ return this._nextMetaData;
+ }
+
+ /**
+ * Fetches records to display (and groups if necessary).
+ *
+ * @protected
+ * @param {MetaData} metaData
+ * @param {Object} [additionalContext]
+ */
+ async _fetchData(metaData, additionalContext) {
+ const { globalStart, globalStop, groupedBy, pagerLimit, pagerOffset, resModel, scale } =
+ metaData;
+ const context = {
+ ...this.searchParams.context,
+ group_by: groupedBy,
+ ...additionalContext,
+ };
+ const domain = this._getDomain(metaData);
+ const fields = this._getFields(metaData);
+ const specification = {};
+ for (const fieldName of fields) {
+ specification[fieldName] = {};
+ if (metaData.fields[fieldName].type === "many2one") {
+ specification[fieldName].fields = { display_name: {} };
+ }
+ }
+
+ const { length, groups, records, progress_bars, unavailabilities } =
+ await this.keepLast.add(
+ this.orm.call(resModel, "get_gantt_data", [], {
+ domain,
+ groupby: groupedBy,
+ read_specification: specification,
+ scale: scale.unit,
+ start_date: serializeDateTime(globalStart),
+ stop_date: serializeDateTime(globalStop),
+ unavailability_fields: this._getUnavailabilityFields(metaData),
+ progress_bar_fields: this._getProgressBarFields(metaData),
+ context,
+ limit: pagerLimit,
+ offset: pagerOffset,
+ })
+ );
+
+ groups.forEach((g) => (g.fromServer = true));
+
+ const data = { count: length };
+
+ data.records = this._parseServerData(metaData, records);
+ data.rows = this._generateRows(metaData, {
+ groupedBy,
+ groups,
+ parentGroup: [],
+ });
+ data.unavailabilities = this._processUnavailabilities(unavailabilities);
+ data.progressBars = this._processProgressBars(progress_bars);
+
+ await this.keepLast.add(this._fetchDataPostProcess(metaData, data));
+
+ this.data = data;
+ this.metaData = metaData;
+ this._nextMetaData = null;
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @param {Data} data
+ */
+ async _fetchDataPostProcess(metaData, data) {}
+
+ /**
+ * Remove date in groupedBy field
+ *
+ * @protected
+ * @param {MetaData} metaData
+ * @param {string[]} groupedBy
+ * @returns {string[]}
+ */
+ _filterDateIngroupedBy(metaData, groupedBy) {
+ return groupedBy.filter((gb) => {
+ const [fieldName] = gb.split(":");
+ const { type } = metaData.fields[fieldName];
+ return !["date", "datetime"].includes(type);
+ });
+ }
+
+ /**
+ * @protected
+ * @param {number} floatVal
+ * @param {string}
+ */
+ _formatTime(floatVal) {
+ const timeStr = formatFloatTime(floatVal, { noLeadingZeroHour: true });
+ const [hourStr, minuteStr] = timeStr.split(":");
+ const hour = parseInt(hourStr, 10);
+ const minute = parseInt(minuteStr, 10);
+ return minute ? _t("%(hour)sh%(minute)s", { hour, minute }) : _t("%sh", hour);
+ }
+
+ /**
+ * Process groups to generate a recursive structure according
+ * to groupedBy fields. Note that there might be empty groups (filled by
+ * read_goup with group_expand) that also need to be processed.
+ *
+ * @protected
+ * @param {MetaData} metaData
+ * @param {Object} params
+ * @param {Object[]} params.groups
+ * @param {string[]} params.groupedBy
+ * @param {Object[]} params.parentGroup
+ * @returns {Row[]}
+ */
+ _generateRows(metaData, params) {
+ const groupedBy = params.groupedBy;
+ const groups = params.groups;
+ const groupLevel = metaData.groupedBy.length - groupedBy.length;
+ const parentGroup = params.parentGroup;
+
+ if (!groupedBy.length || !groups.length) {
+ const recordIds = [];
+ for (const g of groups) {
+ recordIds.push(...(g.__record_ids || []));
+ }
+ const part = parentGroup.at(-1);
+ const [[parentGroupedField, value]] = part ? Object.entries(part) : [[]];
+ return [
+ {
+ groupLevel,
+ id: JSON.stringify([...parentGroup, {}]),
+ name: "",
+ recordIds: unique(recordIds),
+ parentGroupedField,
+ parentResId: Array.isArray(value) ? value[0] : value,
+ __extra__: true,
+ },
+ ];
+ }
+
+ /** @type {Row[]} */
+ const rows = [];
+
+ // Some groups might be empty (thanks to expand_groups), so we can't
+ // simply group the data, we need to keep all returned groups
+ const groupedByField = groupedBy[0];
+ const currentLevelGroups = groupBy(groups, (g) => {
+ if (g[groupedByField] === undefined) {
+ // we want to group the groups with undefined values for groupedByField with the ones
+ // with false value for the same field.
+ // we also want to be sure that stringification keeps groupedByField:
+ // JSON.stringify({ key: undefined }) === "{}"
+ // see construction of id below.
+ g[groupedByField] = false;
+ }
+ return g[groupedByField];
+ });
+ const { maxField } = metaData.consolidationParams;
+ const consolidate = groupLevel === 0 && groupedByField === maxField;
+ const generateSubRow = maxField ? true : groupedBy.length > 1;
+ for (const key in currentLevelGroups) {
+ const subGroups = currentLevelGroups[key];
+ const value = subGroups[0][groupedByField];
+ const part = {};
+ part[groupedByField] = value;
+ const fakeGroup = [...parentGroup, part];
+ const id = JSON.stringify(fakeGroup);
+ const resId = Array.isArray(value) ? value[0] : value; // not really a resId
+ const fromServer = subGroups.some((g) => g.fromServer);
+ const recordIds = [];
+ for (const g of subGroups) {
+ recordIds.push(...(g.__record_ids || []));
+ }
+ const row = {
+ consolidate,
+ fromServer,
+ groupedBy,
+ groupedByField,
+ groupLevel,
+ id,
+ name: this._getRowName(metaData, groupedByField, value),
+ resId, // not really a resId
+ recordIds: unique(recordIds),
+ };
+ if (generateSubRow) {
+ row.rows = this._generateRows(metaData, {
+ ...params,
+ groupedBy: groupedBy.slice(1),
+ groups: subGroups,
+ parentGroup: fakeGroup,
+ });
+ }
+ if (resId === false) {
+ rows.unshift(row);
+ } else {
+ rows.push(row);
+ }
+ }
+
+ return rows;
+ }
+
+ /**
+ * Get domain of records to display in the gantt view.
+ *
+ * @protected
+ * @param {MetaData} metaData
+ * @returns {any[]}
+ */
+ _getDomain(metaData) {
+ const { dateStartField, dateStopField, globalStart, globalStop } = metaData;
+ const domain = Domain.and([
+ this.searchParams.domain,
+ [
+ "&",
+ [
+ dateStartField,
+ "<",
+ this.dateStopFieldIsDate(metaData)
+ ? serializeDate(globalStop)
+ : serializeDateTime(globalStop),
+ ],
+ [
+ dateStopField,
+ this.dateStartFieldIsDate(metaData) ? ">=" : ">",
+ this.dateStartFieldIsDate(metaData)
+ ? serializeDate(globalStart)
+ : serializeDateTime(globalStart),
+ ],
+ ],
+ ]);
+ return domain.toList();
+ }
+
+ /**
+ * Format field value to display purpose.
+ *
+ * @protected
+ * @param {any} value
+ * @param {Object} field
+ * @returns {string} formatted field value
+ */
+ _getFieldFormattedValue(value, field) {
+ if (field.type === "boolean") {
+ return value ? "True" : "False";
+ } else if (!value) {
+ return _t("Undefined %s", field.string);
+ } else if (field.type === "many2many") {
+ return value[1];
+ }
+ const formatter = registry.category("formatters").get(field.type);
+ return formatter(value, field);
+ }
+
+ /**
+ * Get all the fields needed.
+ *
+ * @protected
+ * @param {MetaData} metaData
+ * @returns {string[]}
+ */
+ _getFields(metaData) {
+ const fields = new Set([
+ "display_name",
+ metaData.dateStartField,
+ metaData.dateStopField,
+ ...metaData.groupedBy,
+ ...metaData.decorationFields,
+ ]);
+ if (metaData.colorField) {
+ fields.add(metaData.colorField);
+ }
+ if (metaData.consolidationParams.field) {
+ fields.add(metaData.consolidationParams.field);
+ }
+ if (metaData.consolidationParams.excludeField) {
+ fields.add(metaData.consolidationParams.excludeField);
+ }
+ if (metaData.dependencyField) {
+ fields.add(metaData.dependencyField);
+ }
+ if (metaData.progressField) {
+ fields.add(metaData.progressField);
+ }
+ return [...fields];
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @param {{ groupBy: string[] }} searchParams
+ * @returns {string[]}
+ */
+ _getGroupedBy(metaData, searchParams) {
+ let groupedBy = [...searchParams.groupBy];
+ groupedBy = groupedBy.filter((gb) => {
+ const [fieldName] = gb.split(".");
+ const field = metaData.fields[fieldName];
+ return field?.type !== "properties";
+ });
+ groupedBy = this._filterDateIngroupedBy(metaData, groupedBy);
+ if (!groupedBy.length) {
+ groupedBy = metaData.defaultGroupBy;
+ }
+ return groupedBy;
+ }
+
+ _getDefaultFocusDate(metaData, searchParams, scaleId) {
+ const { context } = searchParams;
+ let focusDate =
+ "initialDate" in context ? deserializeDateTime(context.initialDate) : DateTime.local();
+ focusDate = focusDate.startOf("day");
+ if (metaData.offset) {
+ const { unit } = metaData.scales[scaleId];
+ focusDate = focusDate.plus({ [unit]: metaData.offset });
+ }
+ return focusDate;
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @param {{ context: Record }} searchParams
+ * @returns {{ focusDate: DateTime, scaleId: ScaleId, startDate: DateTime, stopDate: DateTime }}
+ */
+ _getInitialRangeParams(metaData, searchParams) {
+ const { context } = searchParams;
+ const localScaleId = this._getScaleIdFromLocalStorage(metaData);
+ /** @type {ScaleId} */
+ const scaleId = localScaleId || context.default_scale || metaData.defaultScale;
+ const { defaultRange } = metaData.scales[scaleId];
+
+ const rangeId =
+ context.default_range in metaData.ranges
+ ? context.range_type
+ : metaData.defaultRange || "custom";
+ let focusDate;
+ if (rangeId in metaData.ranges) {
+ focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);
+ return { scaleId, ...getRangeFromDate(rangeId, focusDate) };
+ }
+ let startDate = context.default_start_date && deserializeDate(context.default_start_date);
+ let stopDate = context.default_stop_date && deserializeDate(context.default_stop_date);
+ if (!startDate && !stopDate) {
+ /** @type {DateTime} */
+ focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);
+ startDate = firstColumnBefore(focusDate, defaultRange.unit);
+ stopDate = startDate
+ .plus({ [defaultRange.unit]: defaultRange.count })
+ .minus({ day: 1 });
+ } else if (startDate && !stopDate) {
+ const column = firstColumnBefore(startDate, defaultRange.unit);
+ focusDate = startDate;
+ stopDate = column.plus({ [defaultRange.unit]: defaultRange.count }).minus({ day: 1 });
+ } else if (!startDate && stopDate) {
+ const column = firstColumnAfter(stopDate, defaultRange.unit);
+ focusDate = stopDate;
+ startDate = column.minus({ [defaultRange.unit]: defaultRange.count });
+ } else {
+ focusDate = DateTime.local();
+ if (focusDate < startDate) {
+ focusDate = startDate;
+ } else if (focusDate > stopDate) {
+ focusDate = stopDate;
+ }
+ }
+
+ return { focusDate, scaleId, startDate, stopDate, rangeId };
+ }
+
+ _getLocalStorageKey() {
+ return `scaleOf-viewId-${this.env.config.viewId}`;
+ }
+
+ _getProgressBarFields(metaData) {
+ if (metaData.progressBarFields && !this.orm.isSample) {
+ return metaData.progressBarFields.filter(
+ (fieldName) =>
+ metaData.groupedBy.includes(fieldName) &&
+ ["many2many", "many2one"].includes(metaData.fields[fieldName]?.type)
+ );
+ }
+ return [];
+ }
+
+ _getRescheduleContext() {
+ return { ...this.searchParams.context };
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @param {string} groupedByField
+ * @param {any} value
+ * @returns {string}
+ */
+ _getRowName(metaData, groupedByField, value) {
+ const field = metaData.fields[groupedByField];
+ return this._getFieldFormattedValue(value, field);
+ }
+
+ _getScaleIdFromLocalStorage(metaData) {
+ const { scales } = metaData;
+ const localScaleId = browser.localStorage.getItem(this._getLocalStorageKey());
+ return localScaleId in scales ? localScaleId : null;
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @returns {string[]}
+ */
+ _getUnavailabilityFields(metaData) {
+ if (metaData.displayUnavailability && !this.orm.isSample && metaData.groupedBy.length) {
+ const lastGroupBy = metaData.groupedBy.at(-1);
+ const { type } = metaData.fields[lastGroupBy] || {};
+ if (["many2many", "many2one"].includes(type)) {
+ return [lastGroupBy];
+ }
+ }
+ return [];
+ }
+
+ /**
+ * @protected
+ * @param {MetaData} metaData
+ * @param {Record[]} records the server records to parse
+ * @returns {Record[]}
+ */
+ _parseServerData(metaData, records) {
+ const { dateStartField, dateStopField, fields, globalStart, globalStop } = metaData;
+ /** @type {Record[]} */
+ const parsedRecords = [];
+ for (const record of records) {
+ const parsedRecord = parseServerValues(fields, record);
+ const dateStart = parsedRecord[dateStartField];
+ const dateStop = parsedRecord[dateStopField];
+ if (this.orm.isSample) {
+ // In sample mode, we want enough data to be displayed, so we
+ // swap the dates as the records are randomly generated anyway.
+ if (dateStart > dateStop) {
+ parsedRecord[dateStartField] = dateStop;
+ parsedRecord[dateStopField] = dateStart;
+ }
+ // Record could also be outside the displayed range since the
+ // sample server doesn't take the domain into account
+ if (parsedRecord[dateStopField] < globalStart) {
+ parsedRecord[dateStopField] = globalStart;
+ }
+ if (parsedRecord[dateStartField] > globalStop) {
+ parsedRecord[dateStartField] = globalStop;
+ }
+ parsedRecords.push(parsedRecord);
+ } else if (dateStart <= dateStop) {
+ parsedRecords.push(parsedRecord);
+ }
+ }
+ return parsedRecords;
+ }
+
+ _processProgressBar(progressBar, warning) {
+ const processedProgressBar = {
+ ...progressBar,
+ value_formatted: this._formatTime(progressBar.value),
+ max_value_formatted: this._formatTime(progressBar.max_value),
+ ratio: progressBar.max_value ? (progressBar.value / progressBar.max_value) * 100 : 0,
+ warning,
+ };
+ if (processedProgressBar?.max_value) {
+ processedProgressBar.ratio_formatted = formatPercentage(
+ processedProgressBar.ratio / 100
+ );
+ }
+ return processedProgressBar;
+ }
+
+ _processProgressBars(progressBars) {
+ const processedProgressBars = {};
+ for (const fieldName in progressBars) {
+ processedProgressBars[fieldName] = {};
+ const progressBarInfo = progressBars[fieldName];
+ for (const [resId, progressBar] of Object.entries(progressBarInfo)) {
+ processedProgressBars[fieldName][resId] = this._processProgressBar(
+ progressBar,
+ progressBarInfo.warning
+ );
+ }
+ }
+ return processedProgressBars;
+ }
+
+ _processUnavailabilities(unavailabilities) {
+ const processedUnavailabilities = {};
+ for (const fieldName in unavailabilities) {
+ processedUnavailabilities[fieldName] = {};
+ for (const [resId, resUnavailabilities] of Object.entries(
+ unavailabilities[fieldName]
+ )) {
+ processedUnavailabilities[fieldName][resId] = resUnavailabilities.map((u) => ({
+ start: deserializeDateTime(u.start),
+ stop: deserializeDateTime(u.stop),
+ }));
+ }
+ }
+ return processedUnavailabilities;
+ }
+
+ /**
+ * @template {Record} T
+ * @param {T} schedule
+ * @returns {Partial}
+ */
+ _scheduleToData(schedule) {
+ const allowedFields = [
+ this.metaData.dateStartField,
+ this.metaData.dateStopField,
+ ...this.metaData.groupedBy,
+ ];
+ return pick(schedule, ...allowedFields);
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_popover.js b/addons_extensions/web_gantt/static/src/gantt_popover.js
new file mode 100644
index 000000000..2d109b634
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_popover.js
@@ -0,0 +1,59 @@
+import { Component, useRef } from "@odoo/owl";
+import { ViewButton } from "@web/views/view_button/view_button";
+import { useViewButtons } from "@web/views/view_button/view_button_hook";
+import { useViewCompiler } from "@web/views/view_compiler";
+import { GanttCompiler } from "./gantt_compiler";
+
+export class GanttPopover extends Component {
+ static template = "web_gantt.GanttPopover";
+ static components = { ViewButton };
+ static props = [
+ "title",
+ "displayGenericButtons",
+ "bodyTemplate?",
+ "footerTemplate?",
+ "resModel",
+ "resId",
+ "context",
+ "close",
+ "reload",
+ "buttons",
+ ];
+
+ setup() {
+ this.rootRef = useRef("root");
+
+ this.templates = { body: "web_gantt.GanttPopover.default" };
+ const toCompile = {};
+ const { bodyTemplate, footerTemplate } = this.props;
+ if (bodyTemplate) {
+ toCompile.body = bodyTemplate;
+ if (footerTemplate) {
+ toCompile.footer = footerTemplate;
+ }
+ }
+ Object.assign(
+ this.templates,
+ useViewCompiler(GanttCompiler, toCompile, { recordExpr: "__record__" })
+ );
+
+ useViewButtons(this.rootRef, {
+ reload: async () => {
+ await this.props.reload();
+ this.props.close();
+ },
+ });
+ }
+
+ get renderingContext() {
+ return Object.assign({}, this.props.context, {
+ __comp__: this,
+ __record__: { resModel: this.props.resModel, resId: this.props.resId },
+ });
+ }
+
+ async onClick(button) {
+ await button.onClick();
+ this.props.close();
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_popover.xml b/addons_extensions/web_gantt/static/src/gantt_popover.xml
new file mode 100644
index 000000000..bf5e07b19
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_popover.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+ -
+ Name:
+
+ -
+ Start:
+
+ -
+ Stop:
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.js b/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.js
new file mode 100644
index 000000000..80109507a
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.js
@@ -0,0 +1,11 @@
+import { Component } from "@odoo/owl";
+import { Dialog } from "@web/core/dialog/dialog";
+
+export class GanttPopoverInDialog extends Component {
+ static components = { Dialog };
+ static props = ["close", "component", "componentProps", "dialogTitle"];
+ static template = "web_gantt.GanttPopoverInDialog";
+ get componentProps() {
+ return { ...this.props.componentProps, close: this.props.close };
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.xml b/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.xml
new file mode 100644
index 000000000..461f957ae
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_popover_in_dialog.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_renderer.js b/addons_extensions/web_gantt/static/src/gantt_renderer.js
new file mode 100644
index 000000000..6edc4b391
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_renderer.js
@@ -0,0 +1,2538 @@
+import {
+ Component,
+ onWillRender,
+ onWillStart,
+ onWillUpdateProps,
+ reactive,
+ useEffect,
+ useExternalListener,
+ useRef,
+ markup,
+} from "@odoo/owl";
+import { hasTouch, isMobileOS } from "@web/core/browser/feature_detection";
+import { Domain } from "@web/core/domain";
+import {
+ getStartOfLocalWeek,
+ is24HourFormat,
+ serializeDate,
+ serializeDateTime,
+} from "@web/core/l10n/dates";
+import { localization } from "@web/core/l10n/localization";
+import { _t } from "@web/core/l10n/translation";
+import { usePopover } from "@web/core/popover/popover_hook";
+import { evaluateBooleanExpr } from "@web/core/py_js/py";
+import { user } from "@web/core/user";
+import { useService } from "@web/core/utils/hooks";
+import { omit, pick } from "@web/core/utils/objects";
+import { debounce, throttleForAnimation } from "@web/core/utils/timing";
+import { url } from "@web/core/utils/urls";
+import { escape } from "@web/core/utils/strings";
+import { useVirtualGrid } from "@web/core/virtual_grid_hook";
+import { formatFloatTime } from "@web/views/fields/formatters";
+import { SelectCreateDialog } from "@web/views/view_dialogs/select_create_dialog";
+import { GanttConnector } from "./gantt_connector";
+import {
+ dateAddFixedOffset,
+ diffColumn,
+ getCellColor,
+ getColorIndex,
+ localEndOf,
+ localStartOf,
+ useGanttConnectorDraggable,
+ useGanttDraggable,
+ useGanttResizable,
+ useGanttSelectable,
+ useGanttUndraggable,
+ useMultiHover,
+} from "./gantt_helpers";
+import { GanttPopover } from "./gantt_popover";
+import { GanttRendererControls } from "./gantt_renderer_controls";
+import { GanttResizeBadge } from "./gantt_resize_badge";
+import { GanttRowProgressBar } from "./gantt_row_progress_bar";
+import { clamp } from "@web/core/utils/numbers";
+
+const { DateTime } = luxon;
+
+/**
+ * @typedef {`__column__${number}`} ColumnId
+ * @typedef {`__connector__${number | "new"}`} ConnectorId
+ * @typedef {import("./gantt_connector").ConnectorProps} ConnectorProps
+ * @typedef {luxon.DateTime} DateTime
+ * @typedef {"copy" | "reschedule"} DragActionMode
+ * @typedef {"drag" | "locked" | "resize"} InteractionMode
+ * @typedef {`__pill__${number}`} PillId
+ * @typedef {import("./gantt_model").RowId} RowId
+ *
+ * @typedef Column
+ * @property {ColumnId} id
+ * @property {GridPosition} grid
+ * @property {boolean} [isToday]
+ * @property {DateTime} start
+ * @property {DateTime} stop
+ *
+ * @typedef GridPosition
+ * @property {number | number[]} [row]
+ * @property {number | number[]} [column]
+ *
+ * @typedef Group
+ * @property {boolean} break
+ * @property {number} col
+ * @property {Pill[]} pills
+ * @property {number} aggregateValue
+ * @property {GridPosition} grid
+ *
+ * @typedef GanttRendererProps
+ * @property {import("./gantt_model").GanttModel} model
+ * @property {Document} arch
+ * @property {string} class
+ * @property {(context: Record)} create
+ * @property {{ content?: Point }} [scrollPosition]
+ * @property {{ el: HTMLDivElement | null }} [contentRef]
+ *
+ * @typedef HoveredInfo
+ * @property {Element | null} connector
+ * @property {HTMLElement | null} hoverable
+ * @property {HTMLElement | null} pill
+ *
+ * @typedef Interaction
+ * @property {InteractionMode | null} mode
+ * @property {DragActionMode} dragAction
+ *
+ * @typedef Pill
+ * @property {PillId} id
+ * @property {boolean} disableStartResize
+ * @property {boolean} disableStopResize
+ * @property {boolean} highlighted
+ * @property {number} leftMargin
+ * @property {number} level
+ * @property {string} name
+ * @property {DateTime} startDate
+ * @property {DateTime} stopDate
+ * @property {GridPosition} grid
+ * @property {RelationalRecord} record
+ * @property {number} _color
+ * @property {number} _progress
+ *
+ * @typedef Point
+ * @property {number} [x]
+ * @property {number} [y]
+ *
+ * @typedef {Record} RelationalRecord
+ * @property {number | false} id
+ *
+ * @typedef ResizeBadge
+ * @property {Point & { right?: number }} position
+ * @property {number} diff
+ * @property {string} scale
+ *
+ * @typedef {import("./gantt_model").Row & {
+ * grid: GridPosition,
+ * pills: Pill[],
+ * cellColors?: Record,
+ * thumbnailUrl?: string
+ * }} Row
+ *
+ * @typedef SubColumn
+ * @property {ColumnId} columnId
+ * @property {boolean} [isToday]
+ * @property {DateTime} start
+ * @property {DateTime} stop
+ */
+
+/** @type {[Omit | DragActionMode, string][]} */
+const INTERACTION_CLASSNAMES = [
+ ["connect", "o_connect"],
+ ["copy", "o_copying"],
+ ["locked", "o_grabbing_locked"],
+ ["reschedule", "o_grabbing"],
+ ["resize", "o_resizing"],
+];
+const NEW_CONNECTOR_ID = "__connector__new";
+
+/**
+ * Gantt Renderer
+ *
+ * @extends {Component}
+ */
+export class GanttRenderer extends Component {
+ static components = {
+ GanttConnector,
+ GanttRendererControls,
+ GanttResizeBadge,
+ GanttRowProgressBar,
+ Popover: GanttPopover,
+ };
+ static props = [
+ "model",
+ "arch",
+ "class",
+ "create",
+ "openDialog",
+ "scrollPosition?",
+ "contentRef?",
+ ];
+
+ static template = "web_gantt.GanttRenderer";
+ static connectorCreatorTemplate = "web_gantt.GanttRenderer.ConnectorCreator";
+ static headerTemplate = "web_gantt.GanttRenderer.Header";
+ static pillTemplate = "web_gantt.GanttRenderer.Pill";
+ static groupPillTemplate = "web_gantt.GanttRenderer.GroupPill";
+ static rowContentTemplate = "web_gantt.GanttRenderer.RowContent";
+ static rowHeaderTemplate = "web_gantt.GanttRenderer.RowHeader";
+ static totalRowTemplate = "web_gantt.GanttRenderer.TotalRow";
+
+ static getRowHeaderWidth = (width) => 100 / (width > 768 ? 6 : 3);
+
+ setup() {
+ this.model = this.props.model;
+
+ this.gridRef = useRef("grid");
+ this.cellContainerRef = useRef("cellContainer");
+
+ this.actionService = useService("action");
+ this.dialogService = useService("dialog");
+ this.notificationService = useService("notification");
+
+ this.is24HourFormat = is24HourFormat();
+
+ /** @type {HoveredInfo} */
+ this.hovered = {
+ connector: null,
+ hoverable: null,
+ pill: null,
+ };
+
+ /** @type {Interaction} */
+ this.interaction = reactive(
+ {
+ mode: null,
+ dragAction: "reschedule",
+ },
+ () => this.onInteractionChange()
+ );
+ this.onInteractionChange(); // Used to hook into "interaction"
+ /** @type {Record} */
+ this.connectors = reactive({});
+ this.progressBarsReactive = reactive({ hoveredRowId: null });
+ /** @type {ResizeBadge} */
+ this.resizeBadgeReactive = reactive({});
+
+ /** @type {Object[]} */
+ this.columnsGroups = [];
+ /** @type {Column[]} */
+ this.columns = [];
+ /** @type {Pill[]} */
+ this.extraPills = [];
+ /** @type {Record} */
+ this.pills = {}; // mapping to retrieve pills from pill ids
+ /** @type {Row[]} */
+ this.rows = [];
+ /** @type {SubColumn[]} */
+ this.subColumns = [];
+ /** @type {Record} */
+ this.rowPills = {};
+
+ this.mappingColToColumn = new Map();
+ this.mappingColToSubColumn = new Map();
+ this.cursorPosition = {
+ x: 0,
+ y: 0,
+ };
+ const position = "bottom";
+ this.popover = usePopover(this.constructor.components.Popover, {
+ position,
+ onPositioned: (el, { direction }) => {
+ if (direction !== position) {
+ return;
+ }
+ const { left, right } = el.getBoundingClientRect();
+ if ((0 <= left && right <= window.innerWidth) || window.innerWidth < right - left) {
+ return;
+ }
+ const { left: pillLeft, right: pillRight } =
+ this.popover.target.getBoundingClientRect();
+ const middle =
+ (clamp(pillLeft, 0, window.innerWidth) +
+ clamp(pillRight, 0, window.innerWidth)) /
+ 2;
+ el.style.left = `0px`;
+ const { width } = el.getBoundingClientRect();
+ el.style.left = `${middle - width / 2}px`;
+ },
+ onClose: () => {
+ delete this.popover.target;
+ },
+ });
+
+ this.throttledComputeHoverParams = throttleForAnimation((ev) =>
+ this.computeHoverParams(ev)
+ );
+
+ useExternalListener(window, "keydown", (ev) => this.onWindowKeyDown(ev));
+ useExternalListener(window, "keyup", (ev) => this.onWindowKeyUp(ev));
+
+ useExternalListener(
+ window,
+ "resize",
+ debounce(() => {
+ this.shouldComputeSomeWidths = true;
+ this.render();
+ }, 100)
+ );
+
+ useMultiHover({
+ ref: this.gridRef,
+ selector: ".o_gantt_group",
+ related: ["data-row-id"],
+ className: "o_gantt_group_hovered",
+ });
+
+ // Draggable pills
+ this.cellForDrag = { el: null, part: 0 };
+ const dragState = useGanttDraggable({
+ enable: () => Boolean(this.cellForDrag.el),
+ // Refs and selectors
+ ref: this.gridRef,
+ hoveredCell: this.cellForDrag,
+ elements: ".o_draggable",
+ ignore: ".o_resize_handle,.o_connector_creator_bullet",
+ cells: ".o_gantt_cell",
+ // Style classes
+ cellDragClassName: "o_gantt_cell o_drag_hover",
+ ghostClassName: "o_dragged_pill_ghost",
+ addStickyCoordinates: (rows, columns) => {
+ this.stickyGridRows = Object.assign({}, ...rows.map((row) => ({ [row]: true })));
+ this.stickyGridColumns = Object.assign(
+ {},
+ ...columns.map((column) => ({ [column]: true }))
+ );
+ this.setSomeGridStyleProperties();
+ },
+ // Handlers
+ onDragStart: ({ pill }) => {
+ this.popover.close();
+ this.setStickyPill(pill);
+ this.interaction.mode = "drag";
+ },
+ onDragEnd: () => {
+ this.setStickyPill();
+ this.interaction.mode = null;
+ },
+ onDrop: (params) => this.dragPillDrop(params),
+ });
+
+ // Un-draggable pills
+ const unDragState = useGanttUndraggable({
+ // Refs and selectors
+ ref: this.gridRef,
+ elements: ".o_undraggable",
+ ignore: ".o_resize_handle,.o_connector_creator_bullet",
+ edgeScrolling: { enabled: false },
+ // Handlers
+ onDragStart: () => {
+ this.interaction.mode = "locked";
+ },
+ onDragEnd: () => {
+ this.interaction.mode = null;
+ },
+ });
+
+ // Cells selection
+ const selectState = useGanttSelectable({
+ enable: () => {
+ const { canCellCreate, canPlan } = this.model.metaData;
+ return Boolean(this.cellForDrag.el) && (canCellCreate || canPlan);
+ },
+ ref: this.gridRef,
+ hoveredCell: this.cellForDrag,
+ elements: ".o_gantt_cell:not(.o_gantt_group)",
+ edgeScrolling: { speed: 40, threshold: 150, direction: "horizontal" },
+ rtl: () => localization.direction === "rtl",
+ onDrop: ({ rowId, startCol, stopCol }) => {
+ const { canPlan } = this.model.metaData;
+ if (canPlan) {
+ this.onPlan(rowId, startCol, stopCol);
+ } else {
+ this.onCreate(rowId, startCol, stopCol);
+ }
+ },
+ });
+
+ // Resizable pills
+ const resizeState = useGanttResizable({
+ // Refs and selectors
+ ref: this.gridRef,
+ hoveredCell: this.cellForDrag,
+ elements: ".o_resizable",
+ innerPills: ".o_gantt_pill",
+ cells: ".o_gantt_cell",
+ // Other params
+ handles: "o_resize_handle",
+ edgeScrolling: { speed: 40, threshold: 150, direction: "horizontal" },
+ showHandles: (pillEl) => {
+ const pill = this.pills[pillEl.dataset.pillId];
+ const hideHandles = this.connectorDragState.dragging;
+ return {
+ start: !pill.disableStartResize && !hideHandles,
+ end: !pill.disableStopResize && !hideHandles,
+ };
+ },
+ rtl: () => localization.direction === "rtl",
+ precision: () => this.model.metaData.scale.cellPart,
+ // Handlers
+ onDragStart: ({ pill, addClass }) => {
+ this.popover.close();
+ this.setStickyPill(pill);
+ addClass(pill, "o_resized");
+ this.interaction.mode = "resize";
+ },
+ onDrag: ({ pill, grabbedHandle, diff }) => {
+ const rect = pill.getBoundingClientRect();
+ const position = { top: rect.y + rect.height };
+ if (grabbedHandle === "left") {
+ position.left = rect.x;
+ } else {
+ position.right = document.body.offsetWidth - rect.x - rect.width;
+ }
+ const { cellTime, unitDescription } = this.model.metaData.scale;
+ Object.assign(this.resizeBadgeReactive, {
+ position,
+ diff: diff * cellTime,
+ scale: unitDescription,
+ });
+ },
+ onDragEnd: ({ pill, removeClass }) => {
+ delete this.resizeBadgeReactive.position;
+ delete this.resizeBadgeReactive.diff;
+ delete this.resizeBadgeReactive.scale;
+ this.setStickyPill();
+ removeClass(pill, "o_resized");
+ this.interaction.mode = null;
+ },
+ onDrop: (params) => this.resizePillDrop(params),
+ });
+
+ // Draggable connector
+ let initialPillId;
+ this.connectorDragState = useGanttConnectorDraggable({
+ ref: this.gridRef,
+ elements: ".o_connector_creator_bullet",
+ parentWrapper: ".o_gantt_cells .o_gantt_pill_wrapper",
+ onDragStart: ({ sourcePill, x, y, addClass }) => {
+ this.popover.close();
+ initialPillId = sourcePill.dataset.pillId;
+ addClass(sourcePill, "o_connector_creator_lock");
+ this.setConnector({
+ id: NEW_CONNECTOR_ID,
+ highlighted: true,
+ sourcePoint: { left: x, top: y },
+ targetPoint: { left: x, top: y },
+ });
+ this.setStickyPill(sourcePill);
+ this.interaction.mode = "connect";
+ },
+ onDrag: ({ connectorCenter, x, y }) => {
+ this.setConnector({
+ id: NEW_CONNECTOR_ID,
+ sourcePoint: { left: connectorCenter.x, top: connectorCenter.y },
+ targetPoint: { left: x, top: y },
+ });
+ },
+ onDragEnd: () => {
+ this.setConnector({ id: NEW_CONNECTOR_ID, sourcePoint: null, targetPoint: null });
+ this.setStickyPill();
+ this.interaction.mode = null;
+ },
+ onDrop: ({ target }) => {
+ if (initialPillId === target.dataset.pillId) {
+ return;
+ }
+ const { id: masterId } = this.pills[initialPillId].record;
+ const { id: slaveId } = this.pills[target.dataset.pillId].record;
+ this.model.createDependency(masterId, slaveId);
+ },
+ });
+
+ this.dragStates = [dragState, unDragState, resizeState, selectState];
+
+ onWillStart(this.computeDerivedParams);
+ onWillUpdateProps(this.computeDerivedParams);
+
+ this.virtualGrid = useVirtualGrid({
+ scrollableRef: this.props.contentRef,
+ initialScroll: this.props.scrollPosition,
+ bufferCoef: 0.1,
+ onChange: (changed) => {
+ if ("columnsIndexes" in changed) {
+ this.shouldComputeGridColumns = true;
+ }
+ if ("rowsIndexes" in changed) {
+ this.shouldComputeGridRows = true;
+ }
+ this.render();
+ },
+ });
+
+ onWillRender(this.onWillRender);
+
+ useEffect(
+ (content) => {
+ content.addEventListener("scroll", this.throttledComputeHoverParams);
+ return () => {
+ content.removeEventListener("scroll", this.throttledComputeHoverParams);
+ };
+ },
+ () => [this.gridRef.el?.parentElement]
+ );
+
+ useEffect(() => {
+ if (this.useFocusDate) {
+ this.useFocusDate = false;
+ this.focusDate(this.model.metaData.focusDate);
+ }
+ });
+
+ this.env.getCurrentFocusDateCallBackRecorder.add(this, this.getCurrentFocusDate.bind(this));
+ }
+
+ //-------------------------------------------------------------------------
+ // Getters
+ //-------------------------------------------------------------------------
+
+ get controlsProps() {
+ return {
+ displayExpandCollapseButtons: this.rows[0]?.isGroup, // all rows on same level have same type
+ model: this.model,
+ focusToday: () => this.focusToday(),
+ getCurrentFocusDate: () => this.getCurrentFocusDate(),
+ };
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ get hasRowHeaders() {
+ const { groupedBy } = this.model.metaData;
+ const { displayMode } = this.model.displayParams;
+ return groupedBy.length || displayMode === "sparse";
+ }
+
+ get isDragging() {
+ return this.dragStates.some((s) => s.dragging);
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ get isTouchDevice() {
+ return isMobileOS() || hasTouch();
+ }
+
+ //-------------------------------------------------------------------------
+ // Methods
+ //-------------------------------------------------------------------------
+
+ /**
+ *
+ * @param {Object} param
+ * @param {Object} param.grid
+ */
+ addCoordinatesToCoarseGrid({ grid }) {
+ if (grid.row) {
+ this.coarseGridRows[this.getFirstGridRow({ grid })] = true;
+ this.coarseGridRows[this.getLastGridRow({ grid })] = true;
+ }
+ if (grid.column) {
+ this.coarseGridCols[this.getFirstGridCol({ grid })] = true;
+ this.coarseGridCols[this.getLastGridCol({ grid })] = true;
+ }
+ }
+
+ /**
+ * @param {Pill} pill
+ * @param {Group} group
+ */
+ addTo(pill, group) {
+ group.pills.push(pill);
+ group.aggregateValue++; // pill count
+ return true;
+ }
+
+ /**
+ * Conditional function for aggregating pills when grouping the gantt view
+ * The first, unused parameter is added in case it's needed when overwriting the method.
+ * @param {Row} row
+ * @param {Group} group
+ * @returns {boolean}
+ */
+ shouldAggregate(row, group) {
+ return Boolean(group.pills.length);
+ }
+
+ /**
+ * Aggregates overlapping pills in group rows.
+ *
+ * @param {Pill[]} pills
+ * @param {Row} row
+ */
+ aggregatePills(pills, row) {
+ /** @type {Record} */
+ const groups = {};
+ function getGroup(col) {
+ if (!(col in groups)) {
+ groups[col] = {
+ break: false,
+ col,
+ pills: [],
+ aggregateValue: 0,
+ grid: { column: [col, col + 1] },
+ };
+ // group.break = true means that the group cannot be merged with the previous one
+ // We will merge groups that can be merged together (if this.shouldMergeGroups returns true)
+ }
+ return groups[col];
+ }
+
+ for (const pill of pills) {
+ let addedInPreviousCol = false;
+ let col;
+ for (col = this.getFirstGridCol(pill); col < this.getLastGridCol(pill); col++) {
+ const group = getGroup(col);
+ const added = this.addTo(pill, group);
+ if (addedInPreviousCol !== added) {
+ group.break = true;
+ }
+ addedInPreviousCol = added;
+ }
+ // here col = this.getLastGridCol(pill)
+ if (addedInPreviousCol && col <= this.columnCount) {
+ const group = getGroup(col);
+ group.break = true;
+ }
+ }
+
+ const filteredGroups = Object.values(groups).filter((g) => this.shouldAggregate(row, g));
+
+ if (this.shouldMergeGroups()) {
+ return this.mergeGroups(filteredGroups);
+ }
+
+ return filteredGroups;
+ }
+
+ /**
+ * Compute minimal levels required to display all pills without overlapping.
+ * Side effect: level key is modified in pills.
+ *
+ * @param {Pill[]} pills
+ */
+ calculatePillsLevel(pills) {
+ const firstPill = pills[0];
+ firstPill.level = 0;
+ const levels = [
+ {
+ pills: [firstPill],
+ maxCol: this.getLastGridCol(firstPill) - 1,
+ },
+ ];
+ for (const currentPill of pills.slice(1)) {
+ const lastCol = this.getLastGridCol(currentPill) - 1;
+ for (let l = 0; l < levels.length; l++) {
+ const level = levels[l];
+ if (this.getFirstGridCol(currentPill) > level.maxCol) {
+ currentPill.level = l;
+ level.pills.push(currentPill);
+ level.maxCol = lastCol;
+ break;
+ }
+ }
+ if (isNaN(currentPill.level)) {
+ currentPill.level = levels.length;
+ levels.push({
+ pills: [currentPill],
+ maxCol: lastCol,
+ });
+ }
+ }
+ return levels.length;
+ }
+
+ makeSubColumn(start, delta, cellTime, time) {
+ const subCellStart = dateAddFixedOffset(start, { [time]: delta * cellTime });
+ const subCellStop = dateAddFixedOffset(start, {
+ [time]: (delta + 1) * cellTime,
+ seconds: -1,
+ });
+ return { start: subCellStart, stop: subCellStop };
+ }
+
+ computeVisibleColumns() {
+ const [firstIndex, lastIndex] = this.virtualGrid.columnsIndexes;
+ this.columnsGroups = [];
+ this.columns = [];
+ this.subColumns = [];
+ this.coarseGridCols = {
+ 1: true,
+ [this.columnCount * this.model.metaData.scale.cellPart + 1]: true,
+ };
+
+ const { globalStart, globalStop, scale } = this.model.metaData;
+ const { cellPart, interval, unit } = scale;
+
+ const now = DateTime.local();
+
+ const nowStart = now.startOf(interval);
+ const nowEnd = now.endOf(interval);
+
+ const groupsLeftBound = DateTime.max(
+ globalStart,
+ localStartOf(globalStart.plus({ [interval]: firstIndex }), unit)
+ );
+ const groupsRightBound = DateTime.min(
+ localEndOf(globalStart.plus({ [interval]: lastIndex }), unit),
+ globalStop
+ );
+ let currentGroup = null;
+ for (let j = firstIndex; j <= lastIndex; j++) {
+ const columnId = `__column__${j + 1}`;
+ const col = j * cellPart + 1;
+ const { start, stop } = this.getColumnFromColNumber(col);
+ const column = {
+ id: columnId,
+ grid: { column: [col, col + cellPart] },
+ start,
+ stop,
+ };
+ const isToday = nowStart <= start && start <= nowEnd;
+ if (isToday) {
+ column.isToday = true;
+ }
+ this.columns.push(column);
+
+ for (let i = 0; i < cellPart; i++) {
+ const subColumn = this.getSubColumnFromColNumber(col + i);
+ this.subColumns.push({ ...subColumn, isToday, columnId });
+ this.coarseGridCols[col + i] = true;
+ }
+
+ const groupStart = localStartOf(start, unit);
+ if (!currentGroup || !groupStart.equals(currentGroup.start)) {
+ const groupId = `__group__${this.columnsGroups.length + 1}`;
+ const startingBound = DateTime.max(groupsLeftBound, groupStart);
+ const endingBound = DateTime.min(groupsRightBound, localEndOf(groupStart, unit));
+ const [groupFirstCol, groupLastCol] = this.getGridColumnFromDates(
+ startingBound,
+ endingBound
+ );
+ currentGroup = {
+ id: groupId,
+ grid: { column: [groupFirstCol, groupLastCol] },
+ start: groupStart,
+ };
+ this.columnsGroups.push(currentGroup);
+ this.coarseGridCols[groupFirstCol] = true;
+ this.coarseGridCols[groupLastCol] = true;
+ }
+ }
+ }
+
+ computeVisibleRows() {
+ this.coarseGridRows = {
+ 1: true,
+ [this.getLastGridRow(this.rows[this.rows.length - 1])]: true,
+ };
+ const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;
+ this.rowsToRender = new Set();
+ for (const row of this.rows) {
+ const [first, last] = row.grid.row;
+ if (last <= rowStart + 1 || first > rowEnd + 1) {
+ continue;
+ }
+ this.addToRowsToRender(row);
+ }
+ }
+
+ getFirstGridCol({ grid }) {
+ const [first] = grid.column;
+ return first;
+ }
+
+ getLastGridCol({ grid }) {
+ const [, last] = grid.column;
+ return last;
+ }
+
+ getFirstGridRow({ grid }) {
+ const [first] = grid.row;
+ return first;
+ }
+
+ getLastGridRow({ grid }) {
+ const [, last] = grid.row;
+ return last;
+ }
+
+ addToPillsToRender(pill) {
+ this.pillsToRender.add(pill);
+ this.addCoordinatesToCoarseGrid(pill);
+ }
+
+ addToRowsToRender(row) {
+ this.rowsToRender.add(row);
+ const [first, last] = row.grid.row;
+ for (let i = first; i <= last; i++) {
+ this.coarseGridRows[i] = true;
+ }
+ }
+
+ /**
+ * give bounds only
+ */
+ getVisibleCols() {
+ const [columnStart, columnEnd] = this.virtualGrid.columnsIndexes;
+ const { cellPart } = this.model.metaData.scale;
+ const firstVisibleCol = 1 + cellPart * columnStart;
+ const lastVisibleCol = 1 + cellPart * (columnEnd + 1);
+ return [firstVisibleCol, lastVisibleCol];
+ }
+
+ /**
+ * give bounds only
+ */
+ getVisibleRows() {
+ const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;
+ const firstVisibleRow = rowStart + 1;
+ const lastVisibleRow = rowEnd + 1;
+ return [firstVisibleRow, lastVisibleRow];
+ }
+
+ computeVisiblePills() {
+ this.pillsToRender = new Set();
+
+ const [firstVisibleCol, lastVisibleCol] = this.getVisibleCols();
+ const [firstVisibleRow, lastVisibleRow] = this.getVisibleRows();
+
+ const isOut = (pill, filterOnRow = true) =>
+ this.getFirstGridCol(pill) > lastVisibleCol ||
+ this.getLastGridCol(pill) < firstVisibleCol ||
+ (filterOnRow &&
+ (this.getFirstGridRow(pill) > lastVisibleRow ||
+ this.getLastGridRow(pill) - 1 < firstVisibleRow));
+
+ const getRowPills = (row, filterOnRow) =>
+ (this.rowPills[row.id] || []).filter((pill) => !isOut(pill, filterOnRow));
+
+ for (const row of this.rowsToRender) {
+ for (const rowPill of getRowPills(row)) {
+ this.addToPillsToRender(rowPill);
+ }
+ if (!row.isGroup && row.unavailabilities?.length) {
+ row.cellColors = this.getRowCellColors(row);
+ }
+ }
+
+ if (this.stickyPillId) {
+ this.addToPillsToRender(this.pills[this.stickyPillId]);
+ }
+
+ if (this.totalRow) {
+ this.totalRow.pills = getRowPills(this.totalRow, false);
+ for (const pill of this.totalRow.pills) {
+ this.addCoordinatesToCoarseGrid({ grid: omit(pill.grid, "row") });
+ }
+ }
+ }
+
+ computeVisibleConnectors() {
+ const visibleConnectorIds = new Set([NEW_CONNECTOR_ID]);
+
+ for (const pill of this.pillsToRender) {
+ const row = this.getRowFromPill(pill);
+ if (row.isGroup) {
+ continue;
+ }
+ for (const connectorId of this.mappingPillToConnectors[pill.id] || []) {
+ visibleConnectorIds.add(connectorId);
+ }
+ }
+
+ this.connectorsToRender = [];
+ for (const connectorId in this.connectors) {
+ if (!visibleConnectorIds.has(connectorId)) {
+ continue;
+ }
+ this.connectorsToRender.push(this.connectors[connectorId]);
+ const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];
+ if (sourcePillId) {
+ this.addToPillsToRender(this.pills[sourcePillId]);
+ }
+ if (targetPillId) {
+ this.addToPillsToRender(this.pills[targetPillId]);
+ }
+ }
+ }
+
+ getRowFromPill(pill) {
+ return this.rowByIds[pill.rowId];
+ }
+
+ getColInCoarseGridKeys() {
+ return Object.keys({ ...this.coarseGridCols, ...this.stickyGridColumns });
+ }
+
+ getRowInCoarseGridKeys() {
+ return Object.keys({ ...this.coarseGridRows, ...this.stickyGridRows });
+ }
+
+ computeColsTemplate() {
+ const colsTemplate = [];
+ const colInCoarseGridKeys = this.getColInCoarseGridKeys();
+ for (let i = 0; i < colInCoarseGridKeys.length - 1; i++) {
+ const x = +colInCoarseGridKeys[i];
+ const y = +colInCoarseGridKeys[i + 1];
+ const colName = `c${x}`;
+ const width = (y - x) * this.cellPartWidth;
+ colsTemplate.push(`[${colName}]minmax(${width}px,1fr)`);
+ }
+ colsTemplate.push(`[c${colInCoarseGridKeys.at(-1)}]`);
+ return colsTemplate.join("");
+ }
+
+ computeRowsTemplate() {
+ const rowsTemplate = [];
+ const rowInCoarseGridKeys = this.getRowInCoarseGridKeys();
+ for (let i = 0; i < rowInCoarseGridKeys.length - 1; i++) {
+ const x = +rowInCoarseGridKeys[i];
+ const y = +rowInCoarseGridKeys[i + 1];
+ const rowName = `r${x}`;
+ const height = this.gridRows.slice(x - 1, y - 1).reduce((a, b) => a + b, 0);
+ rowsTemplate.push(`[${rowName}]${height}px`);
+ }
+ rowsTemplate.push(`[r${rowInCoarseGridKeys.at(-1)}]`);
+ return rowsTemplate.join("");
+ }
+
+ computeSomeWidths() {
+ const { cellPart, minimalColumnWidth } = this.model.metaData.scale;
+ this.contentRefWidth = this.props.contentRef.el?.clientWidth ?? document.body.clientWidth;
+ const rowHeaderWidthPercentage = this.hasRowHeaders
+ ? this.constructor.getRowHeaderWidth(this.contentRefWidth)
+ : 0;
+ this.rowHeaderWidth = this.hasRowHeaders
+ ? Math.round((rowHeaderWidthPercentage * this.contentRefWidth) / 100)
+ : 0;
+ const cellContainerWidth = this.contentRefWidth - this.rowHeaderWidth;
+ const columnWidth = Math.floor(cellContainerWidth / this.columnCount);
+ const rectifiedColumnWidth = Math.max(columnWidth, minimalColumnWidth);
+ this.cellPartWidth = Math.floor(rectifiedColumnWidth / cellPart);
+ this.columnWidth = this.cellPartWidth * cellPart;
+ if (columnWidth <= minimalColumnWidth) {
+ // overflow
+ this.totalWidth = this.rowHeaderWidth + this.columnWidth * this.columnCount;
+ } else {
+ this.totalWidth = null;
+ }
+ }
+
+ computeDerivedParams() {
+ const { rows: modelRows } = this.model.data;
+
+ if (this.shouldRenderConnectors()) {
+ /** @type {Record }>} */
+ this.mappingRecordToPillsByRow = {};
+ /** @type {Record>} */
+ this.mappingRowToPillsByRecord = {};
+ /** @type {Record} */
+ this.mappingConnectorToPills = {};
+ /** @type {Record} */
+ this.mappingPillToConnectors = {};
+ }
+
+ const { globalStart, globalStop, scale, startDate, stopDate } = this.model.metaData;
+ this.columnCount = diffColumn(globalStart, globalStop, scale.interval);
+ if (
+ !this.currentStartDate ||
+ diffColumn(this.currentStartDate, startDate, "day") ||
+ diffColumn(this.currentStopDate, stopDate, "day") ||
+ this.currentScaleId !== scale.id
+ ) {
+ this.useFocusDate = true;
+ this.mappingColToColumn = new Map();
+ this.mappingColToSubColumn = new Map();
+ }
+ this.currentStartDate = startDate;
+ this.currentStopDate = stopDate;
+ this.currentScaleId = scale.id;
+
+ this.currentGridRow = 1;
+ this.gridRows = [];
+ this.nextPillId = 1;
+
+ this.pills = {}; // mapping to retrieve pills from pill ids
+ this.rows = [];
+ this.rowPills = {};
+ this.rowByIds = {};
+
+ const prePills = this.getPills();
+
+ let pillsToProcess = [...prePills];
+ for (const row of modelRows) {
+ const result = this.processRow(row, pillsToProcess);
+ this.rows.push(...result.rows);
+ pillsToProcess = result.pillsToProcess;
+ }
+
+ const { displayTotalRow } = this.model.metaData;
+ if (displayTotalRow) {
+ this.totalRow = this.getTotalRow(prePills);
+ }
+
+ if (this.shouldRenderConnectors()) {
+ this.initializeConnectors();
+ this.generateConnectors();
+ }
+
+ this.shouldComputeSomeWidths = true;
+ this.shouldComputeGridColumns = true;
+ this.shouldComputeGridRows = true;
+ }
+
+ computeDerivedParamsFromHover() {
+ const { scale } = this.model.metaData;
+
+ const { connector, hoverable, pill } = this.hovered;
+
+ // Update cell in drag
+ const isCellHovered = hoverable?.matches(".o_gantt_cell");
+ this.cellForDrag.el = isCellHovered ? hoverable : null;
+ this.cellForDrag.part = 0;
+ if (isCellHovered && scale.cellPart > 1) {
+ const rect = hoverable.getBoundingClientRect();
+ const x = Math.floor(rect.x);
+ const width = Math.floor(rect.width);
+ this.cellForDrag.part = Math.floor(
+ (this.cursorPosition.x - x) / (width / scale.cellPart)
+ );
+ if (localization.direction === "rtl") {
+ this.cellForDrag.part = scale.cellPart - 1 - this.cellForDrag.part;
+ }
+ }
+
+ if (this.isDragging) {
+ this.progressBarsReactive.hoveredRowId = null;
+ return;
+ }
+
+ if (!this.connectorDragState.dragging) {
+ // Highlight connector
+ const hoveredConnectorId = connector?.dataset.connectorId;
+ for (const connectorId in this.connectors) {
+ if (connectorId !== hoveredConnectorId) {
+ this.toggleConnectorHighlighting(connectorId, false);
+ }
+ }
+ if (hoveredConnectorId) {
+ this.progressBarsReactive.hoveredRowId = null;
+ return this.toggleConnectorHighlighting(hoveredConnectorId, true);
+ }
+ }
+
+ // Highlight pill
+ const hoveredPillId = pill?.dataset.pillId;
+ for (const pillId in this.pills) {
+ if (pillId !== hoveredPillId) {
+ this.togglePillHighlighting(pillId, false);
+ }
+ }
+ this.togglePillHighlighting(hoveredPillId, true);
+
+ // Update progress bars
+ this.progressBarsReactive.hoveredRowId = hoverable ? hoverable.dataset.rowId : null;
+ }
+
+ /**
+ * @param {ConnectorId} connectorId
+ */
+ deleteConnector(connectorId) {
+ delete this.connectors[connectorId];
+ delete this.mappingConnectorToPills[connectorId];
+ }
+
+ /**
+ * @param {Object} params
+ * @param {Element} params.pill
+ * @param {Element} params.cell
+ * @param {number} params.diff
+ */
+ async dragPillDrop({ pill, cell, diff }) {
+ const { rowId } = cell.dataset;
+ const { dateStartField, dateStopField, scale } = this.model.metaData;
+ const { cellTime, time } = scale;
+ const { record } = this.pills[pill.dataset.pillId];
+ const params = this.getScheduleParams(pill);
+
+ params.start =
+ diff && dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });
+ params.stop =
+ diff && dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });
+ params.rowId = rowId;
+
+ const schedule = this.model.getSchedule(params);
+
+ if (this.interaction.dragAction === "copy") {
+ await this.model.copy(record.id, schedule, this.openPlanDialogCallback);
+ } else {
+ await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);
+ }
+
+ // If the pill lands on a closed group -> open it
+ if (cell.classList.contains("o_gantt_group") && this.model.isClosed(rowId)) {
+ this.model.toggleRow(rowId);
+ }
+ }
+
+ /**
+ * @param {Partial} pill
+ * @returns {Pill}
+ */
+ enrichPill(pill) {
+ const { colorField, fields, pillDecorations, progressField } = this.model.metaData;
+
+ pill.displayName = this.getDisplayName(pill);
+
+ const classes = [];
+
+ if (pillDecorations) {
+ const pillContext = Object.assign({}, user.context);
+ for (const [fieldName, value] of Object.entries(pill.record)) {
+ const field = fields[fieldName];
+ switch (field.type) {
+ case "date": {
+ pillContext[fieldName] = value ? serializeDate(value) : false;
+ break;
+ }
+ case "datetime": {
+ pillContext[fieldName] = value ? serializeDateTime(value) : false;
+ break;
+ }
+ default: {
+ pillContext[fieldName] = value;
+ }
+ }
+ }
+
+ for (const decoration in pillDecorations) {
+ const expr = pillDecorations[decoration];
+ if (evaluateBooleanExpr(expr, pillContext)) {
+ classes.push(decoration);
+ }
+ }
+ }
+
+ if (colorField) {
+ pill._color = getColorIndex(pill.record[colorField]);
+ classes.push(`o_gantt_color_${pill._color}`);
+ }
+
+ if (progressField) {
+ pill._progress = pill.record[progressField] || 0;
+ }
+
+ pill.className = classes.join(" ");
+
+ return pill;
+ }
+
+ focusDate(date, ifInBounds) {
+ const { globalStart, globalStop } = this.model.metaData;
+ const diff = date.diff(globalStart);
+ const totalDiff = globalStop.diff(globalStart);
+ const factor = diff / totalDiff;
+ if (ifInBounds && (factor < 0 || 1 < factor)) {
+ return false;
+ }
+ const rtlFactor = localization.direction === "rtl" ? -1 : 1;
+ const scrollLeft =
+ factor * this.cellContainerRef.el.clientWidth +
+ this.rowHeaderWidth -
+ (this.contentRefWidth + this.rowHeaderWidth) / 2;
+ this.props.contentRef.el.scrollLeft = rtlFactor * scrollLeft;
+ return true;
+ }
+
+ focusFirstPill(rowId) {
+ const pill = this.rowPills[rowId][0];
+ if (pill) {
+ const col = this.getFirstGridCol(pill);
+ const { start: date } = this.getColumnFromColNumber(col);
+ this.focusDate(date);
+ }
+ }
+
+ focusToday() {
+ return this.focusDate(DateTime.local().startOf("day"), true);
+ }
+
+ generateConnectors() {
+ this.nextConnectorId = 1;
+ this.setConnector({
+ id: NEW_CONNECTOR_ID,
+ highlighted: true,
+ sourcePoint: null,
+ targetPoint: null,
+ });
+ for (const slaveId in this.mappingRecordToPillsByRow) {
+ const { masterIds, pills: slavePills } = this.mappingRecordToPillsByRow[slaveId];
+ for (const masterId of masterIds) {
+ if (!(masterId in this.mappingRecordToPillsByRow)) {
+ continue;
+ }
+ const { pills: masterPills } = this.mappingRecordToPillsByRow[masterId];
+ for (const [slaveRowId, targetPill] of Object.entries(slavePills)) {
+ for (const [masterRowId, sourcePill] of Object.entries(masterPills)) {
+ if (
+ masterRowId === slaveRowId ||
+ !(
+ slaveId in this.mappingRowToPillsByRecord[masterRowId] ||
+ masterId in this.mappingRowToPillsByRecord[slaveRowId]
+ ) ||
+ Object.keys(this.mappingRecordToPillsByRow[slaveId].pills).every(
+ (rowId) =>
+ rowId !== masterRowId &&
+ masterId in this.mappingRowToPillsByRecord[rowId]
+ ) ||
+ Object.keys(this.mappingRecordToPillsByRow[masterId].pills).every(
+ (rowId) =>
+ rowId !== slaveRowId &&
+ slaveId in this.mappingRowToPillsByRecord[rowId]
+ )
+ ) {
+ const masterRecord = sourcePill.record;
+ const slaveRecord = targetPill.record;
+ this.setConnector(
+ { alert: this.getConnectorAlert(masterRecord, slaveRecord) },
+ sourcePill.id,
+ targetPill.id
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param {Group} group
+ * @param {Group} previousGroup
+ */
+ getAggregateValue(group, previousGroup) {
+ // both groups have the same pills by construction
+ // here the aggregateValue is the pill count
+ return group.aggregateValue;
+ }
+
+ /**
+ * @param {number} startCol
+ * @param {number} stopCol
+ * @param {boolean} [roundUpStop=true]
+ */
+ getColumnStartStop(startCol, stopCol, roundUpStop = true) {
+ const { start } = this.getColumnFromColNumber(startCol);
+ let { stop } = this.getColumnFromColNumber(stopCol);
+ if (roundUpStop) {
+ stop = stop.plus({ millisecond: 1 });
+ }
+ return { start, stop };
+ }
+
+ /**
+ *
+ * @param {number} masterRecord
+ * @param {number} slaveRecord
+ * @returns {import("./gantt_connector").ConnectorAlert | null}
+ */
+ getConnectorAlert(masterRecord, slaveRecord) {
+ const { dateStartField, dateStopField } = this.model.metaData;
+ if (slaveRecord[dateStartField] < masterRecord[dateStopField]) {
+ if (slaveRecord[dateStartField] < masterRecord[dateStartField]) {
+ return "error";
+ } else {
+ return "warning";
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @param {Row} row
+ * @param {Column} column
+ * @return {Object}
+ */
+ ganttCellAttClass(row, column) {
+ return {
+ o_sample_data_disabled: this.isDisabled(row),
+ o_gantt_today: column.isToday,
+ o_gantt_group: row.isGroup,
+ o_gantt_hoverable: this.isHoverable(row),
+ o_group_open: !this.model.isClosed(row.id),
+ };
+ }
+
+ getCurrentFocusDate() {
+ const { globalStart, globalStop } = this.model.metaData;
+ const rtlFactor = localization.direction === "rtl" ? -1 : 1;
+ const cellGridMiddleX =
+ rtlFactor * this.props.contentRef.el.scrollLeft +
+ (this.contentRefWidth + this.rowHeaderWidth) / 2;
+ const factor =
+ (cellGridMiddleX - this.rowHeaderWidth) / this.cellContainerRef.el.clientWidth;
+ const totalDiff = globalStop.diff(globalStart);
+ const diff = factor * totalDiff;
+ const focusDate = globalStart.plus(diff);
+ return focusDate;
+ }
+
+ /**
+ * @param {"top"|"bottom"} vertical the vertical alignment of the connector creator
+ * @returns {{ vertical: "top"|"bottom", horizontal: "left"|"right" }}
+ */
+ getConnectorCreatorAlignment(vertical) {
+ const alignment = { vertical };
+ if (localization.direction === "rtl") {
+ alignment.horizontal = vertical === "top" ? "right" : "left";
+ } else {
+ alignment.horizontal = vertical === "top" ? "left" : "right";
+ }
+ return alignment;
+ }
+
+ /**
+ * Get schedule parameters
+ *
+ * @param {Element} pill
+ * @returns {Object} - An object containing parameters needed for scheduling the pill.
+ */
+ getScheduleParams(pill) {
+ return {};
+ }
+
+ /**
+ * This function will add a 'label' property to each
+ * non-consolidated pill included in the pills list.
+ * This new property is a string meant to replace
+ * the text displayed on a pill.
+ *
+ * @param {Pill} pill
+ */
+ getDisplayName(pill) {
+ const { computePillDisplayName, dateStartField, dateStopField, scale } =
+ this.model.metaData;
+ const { id: scaleId } = scale;
+ const { record } = pill;
+
+ if (!computePillDisplayName) {
+ return record.display_name;
+ }
+
+ const startDate = record[dateStartField];
+ const stopDate = record[dateStopField];
+ const yearlessDateFormat = omit(DateTime.DATE_SHORT, "year");
+
+ const spanAccrossDays =
+ stopDate.startOf("day") > startDate.startOf("day") &&
+ startDate.endOf("day").diff(startDate, "hours").toObject().hours >= 3 &&
+ stopDate.diff(stopDate.startOf("day"), "hours").toObject().hours >= 3;
+ const spanAccrossWeeks = getStartOfLocalWeek(stopDate) > getStartOfLocalWeek(startDate);
+ const spanAccrossMonths = stopDate.startOf("month") > startDate.startOf("month");
+
+ /** @type {string[]} */
+ const labelElements = [];
+
+ // Start & End Dates
+ if (scaleId === "year" && !spanAccrossDays) {
+ labelElements.push(startDate.toLocaleString(yearlessDateFormat));
+ } else if (
+ (scaleId === "day" && spanAccrossDays) ||
+ (scaleId === "week" && spanAccrossWeeks) ||
+ (scaleId === "month" && spanAccrossMonths) ||
+ (scaleId === "year" && spanAccrossDays)
+ ) {
+ labelElements.push(startDate.toLocaleString(yearlessDateFormat));
+ labelElements.push(stopDate.toLocaleString(yearlessDateFormat));
+ }
+
+ // Start & End Times
+ if (record.allocated_hours && !spanAccrossDays && ["week", "month"].includes(scaleId)) {
+ const durationStr = this.getDurationStr(record);
+ labelElements.push(startDate.toFormat("t"), `${stopDate.toFormat("t")}${durationStr}`);
+ }
+
+ // Original Display Name
+ if (scaleId !== "month" || !record.allocated_hours || spanAccrossDays) {
+ labelElements.push(record.display_name);
+ }
+
+ return labelElements.filter((el) => !!el).join(" - ");
+ }
+
+ /**
+ * @param {RelationalRecord} record
+ */
+ getDurationStr(record) {
+ const durationStr = formatFloatTime(record.allocated_hours, {
+ noLeadingZeroHour: true,
+ }).replace(/(:00|:)/g, "h");
+ return ` (${durationStr})`;
+ }
+
+ /**
+ * @param {Pill} pill
+ */
+ getGroupPillDisplayName(pill) {
+ return pill.aggregateValue;
+ }
+
+ /**
+ * @param {{ column?: [number, number], row?: [number, number] }} position
+ */
+ getGridPosition(position) {
+ const style = [];
+ const keys = Object.keys(pick(position, "column", "row"));
+ for (const key of keys) {
+ const prefix = key.slice(0, 1);
+ const [first, last] = position[key];
+ style.push(`grid-${key}:${prefix}${first}/${prefix}${last}`);
+ }
+ return style.join(";");
+ }
+
+ setSomeGridStyleProperties() {
+ const rowsTemplate = this.computeRowsTemplate();
+ const colsTemplate = this.computeColsTemplate();
+ this.gridRef.el.style.setProperty("--Gantt__GridRows-grid-template-rows", rowsTemplate);
+ this.gridRef.el.style.setProperty(
+ "--Gantt__GridColumns-grid-template-columns",
+ colsTemplate
+ );
+ }
+
+ getGridStyle() {
+ const rowsTemplate = this.computeRowsTemplate();
+ const colsTemplate = this.computeColsTemplate();
+ const style = {
+ "--Gantt__RowHeader-width": `${this.rowHeaderWidth}px`,
+ "--Gantt__Pill-height": "35px",
+ "--Gantt__Thumbnail-max-height": "16px",
+ "--Gantt__GridRows-grid-template-rows": rowsTemplate,
+ "--Gantt__GridColumns-grid-template-columns": colsTemplate,
+ };
+ if (this.totalWidth !== null) {
+ style.width = `${this.totalWidth}px`;
+ }
+ return Object.entries(style)
+ .map((entry) => entry.join(":"))
+ .join(";");
+ }
+
+ /**
+ * @param {RelationalRecord} record
+ * @returns {Partial}
+ */
+ getPill(record) {
+ const { canEdit, dateStartField, dateStopField, disableDrag, globalStart, globalStop } =
+ this.model.metaData;
+
+ const startOutside = record[dateStartField] < globalStart;
+
+ let recordDateStopField = record[dateStopField];
+ if (this.model.dateStopFieldIsDate()) {
+ recordDateStopField = recordDateStopField.plus({ day: 1 });
+ }
+
+ const stopOutside = recordDateStopField > globalStop;
+
+ /** @type {DateTime} */
+ const pillStartDate = startOutside ? globalStart : record[dateStartField];
+ /** @type {DateTime} */
+ const pillStopDate = stopOutside ? globalStop : recordDateStopField;
+
+ const disableStartResize = !canEdit || startOutside;
+ const disableStopResize = !canEdit || stopOutside;
+
+ /** @type {Partial} */
+ const pill = {
+ disableDrag: disableDrag || disableStartResize || disableStopResize,
+ disableStartResize,
+ disableStopResize,
+ grid: { column: this.getGridColumnFromDates(pillStartDate, pillStopDate) },
+ record,
+ };
+
+ return pill;
+ }
+
+ getGridColumnFromDates(startDate, stopDate) {
+ const { globalStart, scale } = this.model.metaData;
+ const { cellPart, interval } = scale;
+ const { column: column1, delta: delta1 } = this.getSubColumnFromDate(startDate);
+ const { column: column2, delta: delta2 } = this.getSubColumnFromDate(stopDate, false);
+ const firstCol = 1 + diffColumn(globalStart, column1, interval) * cellPart + delta1;
+ const span = diffColumn(column1, column2, interval) * cellPart + delta2 - delta1;
+ return [firstCol, firstCol + span];
+ }
+
+ getSubColumnFromDate(date, onLeft = true) {
+ const { interval, cellPart, cellTime, time } = this.model.metaData.scale;
+ const column = date.startOf(interval);
+ let delta;
+ if (onLeft) {
+ delta = 0;
+ for (let i = 1; i < cellPart; i++) {
+ const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });
+ if (subCellStart <= date) {
+ delta += 1;
+ } else {
+ break;
+ }
+ }
+ } else {
+ delta = cellPart;
+ for (let i = cellPart - 1; i >= 0; i--) {
+ const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });
+ if (subCellStart >= date) {
+ delta -= 1;
+ } else {
+ break;
+ }
+ }
+ }
+ return { column, delta };
+ }
+
+ getSubColumnFromColNumber(col) {
+ let subColumn = this.mappingColToSubColumn.get(col);
+ if (!subColumn) {
+ const { globalStart, scale } = this.model.metaData;
+ const { interval, cellPart, cellTime, time } = scale;
+ const delta = (col - 1) % cellPart;
+ const columnIndex = (col - 1 - delta) / cellPart;
+ const start = globalStart.plus({ [interval]: columnIndex });
+ subColumn = this.makeSubColumn(start, delta, cellTime, time);
+ this.mappingColToSubColumn.set(col, subColumn);
+ }
+ return subColumn;
+ }
+
+ getColumnFromColNumber(col) {
+ let column = this.mappingColToColumn.get(col);
+ if (!column) {
+ const { globalStart, scale } = this.model.metaData;
+ const { interval, cellPart } = scale;
+ const delta = (col - 1) % cellPart;
+ const columnIndex = (col - 1 - delta) / cellPart;
+ const start = globalStart.plus({ [interval]: columnIndex });
+ const stop = start.endOf(interval);
+ column = { start, stop };
+ this.mappingColToColumn.set(col, column);
+ }
+ return column;
+ }
+
+ /**
+ * @param {PillId} pillId
+ */
+ getPillEl(pillId) {
+ return this.getPillWrapperEl(pillId).querySelector(".o_gantt_pill");
+ }
+
+ /**
+ * @param {Object} group
+ * @param {number} maxAggregateValue
+ * @param {boolean} consolidate
+ */
+ getPillFromGroup(group, maxAggregateValue, consolidate) {
+ const { excludeField, field, maxValue } = this.model.metaData.consolidationParams;
+
+ const minColor = 215;
+ const maxColor = 100;
+
+ const newPill = {
+ id: `__pill__${this.nextPillId++}`,
+ level: 0,
+ aggregateValue: group.aggregateValue,
+ grid: group.grid,
+ };
+
+ // Enrich the aggregates with consolidation data
+ if (consolidate && field) {
+ newPill.consolidationValue = 0;
+ for (const pill of group.pills) {
+ if (!pill.record[excludeField]) {
+ newPill.consolidationValue += pill.record[field];
+ }
+ }
+ newPill.consolidationMaxValue = maxValue;
+ newPill.consolidationExceeded =
+ newPill.consolidationValue > newPill.consolidationMaxValue;
+ }
+
+ if (consolidate && maxValue) {
+ const status = newPill.consolidationExceeded ? "danger" : "success";
+ newPill.className = `bg-${status} border-${status}`;
+ newPill.displayName = newPill.consolidationValue;
+ } else {
+ const color =
+ minColor -
+ Math.round((newPill.aggregateValue - 1) / maxAggregateValue) *
+ (minColor - maxColor);
+ newPill.style = `background-color:rgba(${color},${color},${color},0.6)`;
+ newPill.displayName = this.getGroupPillDisplayName(newPill);
+ }
+
+ return newPill;
+ }
+
+ /**
+ * There are two forms of pills: pills comming from fetched records
+ * and pills that are some kind of aggregation of the previous.
+ *
+ * Here we create the pills of the firs type.
+ *
+ * The basic properties (independent of rows,...) of the pills of
+ * the first type should be computed here.
+ *
+ * @returns {Partial[]}
+ */
+ getPills() {
+ const { records } = this.model.data;
+ const { dateStartField } = this.model.metaData;
+ const pills = [];
+ for (const record of records) {
+ const pill = this.getPill(record);
+ pills.push(this.enrichPill(pill));
+ }
+ return pills.sort(
+ (p1, p2) =>
+ p1.grid.column[0] - p2.grid.column[0] ||
+ p1.record[dateStartField] - p2.record[dateStartField]
+ );
+ }
+
+ /**
+ * @param {PillId} pillId
+ */
+ getPillWrapperEl(pillId) {
+ const pillSelector = `:scope > [data-pill-id="${pillId}"]`;
+ return this.cellContainerRef.el?.querySelector(pillSelector);
+ }
+
+ /**
+ * Get domain of records for plan dialog in the gantt view.
+ *
+ * @param {Object} state
+ * @returns {any[][]}
+ */
+ getPlanDialogDomain() {
+ const { dateStartField, dateStopField } = this.model.metaData;
+ const newDomain = Domain.removeDomainLeaves(this.env.searchModel.globalDomain, [
+ dateStartField,
+ dateStopField,
+ ]);
+ return Domain.and([
+ newDomain,
+ ["|", [dateStartField, "=", false], [dateStopField, "=", false]],
+ ]).toList({});
+ }
+
+ /**
+ * @param {PillId} pillId
+ * @param {boolean} onRight
+ */
+ getPoint(pillId, onRight) {
+ if (localization.direction === "rtl") {
+ onRight = !onRight;
+ }
+ const pillEl = this.getPillEl(pillId);
+ const pillRect = pillEl.getBoundingClientRect();
+ return {
+ left: pillRect.left + (onRight ? pillRect.width : 0),
+ top: pillRect.top + pillRect.height / 2,
+ };
+ }
+
+ /**
+ * @param {Pill} pill
+ */
+ getPopoverProps(pill) {
+ const { record } = pill;
+ const { id: resId, display_name: displayName } = record;
+ const { canEdit, dateStartField, dateStopField, popoverArchParams, resModel } =
+ this.model.metaData;
+ const context = popoverArchParams.bodyTemplate
+ ? { ...record }
+ : /* Default context */ {
+ name: displayName,
+ start: record[dateStartField].toFormat("f"),
+ stop: record[dateStopField].toFormat("f"),
+ };
+
+ return {
+ ...popoverArchParams,
+ title: displayName,
+ context,
+ resId,
+ resModel,
+ reload: () => this.model.fetchData(),
+ buttons: [
+ {
+ id: "open_view_edit_dialog",
+ text: canEdit ? _t("Edit") : _t("View"),
+ class: "btn btn-sm btn-primary",
+ // Sync with the mutex to wait for potential changes on the view
+ onClick: () =>
+ this.model.mutex.exec(
+ () => this.props.openDialog({ resId }) // (canEdit is also considered in openDialog)
+ ),
+ },
+ ],
+ };
+ }
+
+ /**
+ * @param {Row} row
+ */
+ getProgressBarProps(row) {
+ return {
+ progressBar: row.progressBar,
+ reactive: this.progressBarsReactive,
+ rowId: row.id,
+ };
+ }
+
+ /**
+ * @param {Row} row
+ */
+ getRowCellColors(row) {
+ const { unavailabilities } = row;
+ const { cellPart } = this.model.metaData.scale;
+ // We assume that the unavailabilities have been normalized
+ // (i.e. are naturally ordered and are pairwise disjoint).
+ // A subCell is considered unavailable (and greyed) when totally covered by
+ // an unavailability.
+ let index = 0;
+ let j = 0;
+ /** @type {Record} */
+ const cellColors = {};
+ const subSlotUnavailabilities = [];
+ for (const subColumn of this.subColumns) {
+ const { isToday, start, stop, columnId } = subColumn;
+ if (index < unavailabilities.length) {
+ let subSlotUnavailable = 0;
+ for (let i = index; i < unavailabilities.length; i++) {
+ const u = unavailabilities[i];
+ if (stop > u.stop) {
+ index++;
+ continue;
+ } else if (u.start <= start) {
+ subSlotUnavailable = 1;
+ }
+ break;
+ }
+ subSlotUnavailabilities.push(subSlotUnavailable);
+ if ((j + 1) % cellPart === 0) {
+ const style = getCellColor(cellPart, subSlotUnavailabilities, isToday);
+ subSlotUnavailabilities.splice(0, cellPart);
+ if (style) {
+ cellColors[columnId] = style;
+ }
+ }
+ j++;
+ }
+ }
+ return cellColors;
+ }
+
+ getFromData(groupedByField, resId, key, defaultVal) {
+ const values = this.model.data[key];
+ if (groupedByField) {
+ return values[groupedByField]?.[resId ?? false] || defaultVal;
+ }
+ return values.__default?.false || defaultVal;
+ }
+
+ /**
+ * @param {string} [groupedByField]
+ * @param {false|number} [resId]
+ * @returns {Object}
+ */
+ getRowProgressBar(groupedByField, resId) {
+ return this.getFromData(groupedByField, resId, "progressBars", null);
+ }
+
+ /**
+ * @param {string} [groupedByField]
+ * @param {false|number} [resId]
+ * @returns {{ start: DateTime, stop: DateTime }[]}
+ */
+ getRowUnavailabilities(groupedByField, resId) {
+ return this.getFromData(groupedByField, resId, "unavailabilities", []);
+ }
+
+ /**
+ * @param {"t0" | "t1" | "t2"} type
+ * @returns {number}
+ */
+ getRowTypeHeight(type) {
+ return {
+ t0: 24,
+ t1: 36,
+ t2: 16,
+ }[type];
+ }
+
+ getRowTitleStyle(row) {
+ return `grid-column: ${row.groupLevel + 2} / -1`;
+ }
+
+ openPlanDialogCallback() {}
+
+ getSelectCreateDialogProps(params) {
+ const domain = this.getPlanDialogDomain();
+ const schedule = this.model.getDialogContext(params);
+ return {
+ title: _t("Plan"),
+ resModel: this.model.metaData.resModel,
+ context: schedule,
+ domain,
+ noCreate: !this.model.metaData.canCellCreate,
+ onSelected: (resIds) => {
+ if (resIds.length) {
+ this.model.reschedule(resIds, schedule, this.openPlanDialogCallback.bind(this));
+ }
+ },
+ };
+ }
+
+ /**
+ * @param {Pill[]} pills
+ */
+ getTotalRow(pills) {
+ const preRow = {
+ groupLevel: 0,
+ id: "[]",
+ rows: [],
+ name: _t("Total"),
+ recordIds: pills.map(({ record }) => record.id),
+ };
+
+ this.currentGridRow = 1;
+ const result = this.processRow(preRow, pills);
+ const [totalRow] = result.rows;
+ const allPills = this.rowPills[totalRow.id] || [];
+ const maxAggregateValue = Math.max(...allPills.map((p) => p.aggregateValue));
+
+ totalRow.factor = maxAggregateValue ? 90 / maxAggregateValue : 0;
+
+ return totalRow;
+ }
+
+ highlightPill(pillId, highlighted) {
+ const pill = this.pills[pillId];
+ if (!pill) {
+ return;
+ }
+ pill.highlighted = highlighted;
+ const pillWrapper = this.getPillWrapperEl(pillId);
+ pillWrapper?.classList.toggle("highlight", highlighted);
+ pillWrapper?.classList.toggle(
+ "o_connector_creator_highlight",
+ highlighted && this.connectorDragState.dragging
+ );
+ }
+
+ initializeConnectors() {
+ for (const connectorId in this.connectors) {
+ this.deleteConnector(connectorId);
+ }
+ }
+
+ isPillSmall(pill) {
+ return this.cellPartWidth * pill.grid.column[1] < pill.displayName.length * 10;
+ }
+
+ /**
+ * @param {Row} row
+ */
+ isDisabled(row = null) {
+ return this.model.useSampleModel;
+ }
+
+ /**
+ * @param {Row} row
+ */
+ isHoverable(row) {
+ return !this.model.useSampleModel;
+ }
+
+ /**
+ * @param {Group[]} groups
+ * @returns {Group[]}
+ */
+ mergeGroups(groups) {
+ if (groups.length <= 1) {
+ return groups;
+ }
+ const index = Math.floor(groups.length / 2);
+ const left = this.mergeGroups(groups.slice(0, index));
+ const right = this.mergeGroups(groups.slice(index));
+ const group = right[0];
+ if (!group.break) {
+ const previousGroup = left.pop();
+ group.break = previousGroup.break;
+ group.grid.column[0] = previousGroup.grid.column[0];
+ group.aggregateValue = this.getAggregateValue(group, previousGroup);
+ }
+ return [...left, ...right];
+ }
+
+ onWillRender() {
+ if (this.noDisplayedConnectors && this.shouldRenderConnectors()) {
+ delete this.noDisplayedConnectors;
+ this.computeDerivedParams();
+ }
+
+ if (this.shouldComputeSomeWidths) {
+ this.computeSomeWidths();
+ }
+
+ if (this.shouldComputeSomeWidths || this.shouldComputeGridColumns) {
+ this.virtualGrid.setColumnsWidths(new Array(this.columnCount).fill(this.columnWidth));
+ this.computeVisibleColumns();
+ }
+
+ if (this.shouldComputeGridRows) {
+ this.virtualGrid.setRowsHeights(this.gridRows);
+ this.computeVisibleRows();
+ }
+
+ if (
+ this.shouldComputeSomeWidths ||
+ this.shouldComputeGridColumns ||
+ this.shouldComputeGridRows
+ ) {
+ delete this.shouldComputeSomeWidths;
+ delete this.shouldComputeGridColumns;
+ delete this.shouldComputeGridRows;
+ this.computeVisiblePills();
+ if (this.shouldRenderConnectors()) {
+ this.computeVisibleConnectors();
+ } else {
+ this.noDisplayedConnectors = true;
+ }
+ }
+
+ delete this.shouldComputeSomeWidths;
+ delete this.shouldComputeGridColumns;
+ delete this.shouldComputeGridRows;
+ }
+
+ pushGridRows(gridRows) {
+ for (const key of ["t0", "t1", "t2"]) {
+ if (key in gridRows) {
+ const types = new Array(gridRows[key]).fill(this.getRowTypeHeight(key));
+ this.gridRows.push(...types);
+ }
+ }
+ }
+
+ processPillsAsRows(row, pills) {
+ const rows = [];
+ const parsedId = JSON.parse(row.id);
+ if (pills.length) {
+ for (const pill of pills) {
+ const { id: resId, display_name: name } = pill.record;
+ const subRow = {
+ id: JSON.stringify([...parsedId, { id: resId }]),
+ resId,
+ name,
+ groupLevel: row.groupLevel + 1,
+ recordIds: [resId],
+ fromServer: row.fromServer,
+ parentResId: row.resId ?? row.parentResId,
+ parentGroupedField: row.groupedByField || row.parentGroupedField,
+ };
+ const res = this.processRow(subRow, [pill], false);
+ rows.push(...res.rows);
+ }
+ } else {
+ const subRow = {
+ id: JSON.stringify([...parsedId, {}]),
+ resId: false,
+ name: "",
+ groupLevel: row.groupLevel + 1,
+ recordIds: [],
+ fromServer: row.fromServer,
+ parentResId: row.resId ?? row.parentResId,
+ parentGroupedField: row.groupedByField || row.parentGroupedField,
+ };
+ const res = this.processRow(subRow, [], false);
+ rows.push(...res.rows);
+ }
+
+ return rows;
+ }
+
+ /**
+ * @param {Row} row
+ * @param {Pill[]} pills
+ * @param {boolean} [processAsGroup=false]
+ */
+ processRow(row, pills, processAsGroup = true) {
+ const { dependencyField, displayUnavailability, fields } = this.model.metaData;
+ const { displayMode } = this.model.displayParams;
+ const {
+ consolidate,
+ fromServer,
+ groupedByField,
+ groupLevel,
+ id,
+ name,
+ parentResId,
+ parentGroupedField,
+ resId,
+ rows,
+ recordIds,
+ __extra__,
+ } = row;
+
+ // compute the subset pills at row level
+ const remainingPills = [];
+ let rowPills = [];
+ const groupPills = [];
+ const isMany2many = groupedByField && fields[groupedByField].type === "many2many";
+ for (const pill of pills) {
+ const { record } = pill;
+ const pushPill = recordIds.includes(record.id);
+ let keepPill = false;
+ if (pushPill && isMany2many) {
+ const value = record[groupedByField];
+ if (Array.isArray(value) && value.length > 1) {
+ keepPill = true;
+ }
+ }
+ if (pushPill) {
+ const rowPill = { ...pill };
+ rowPills.push(rowPill);
+ groupPills.push(pill);
+ }
+ if (!pushPill || keepPill) {
+ remainingPills.push(pill);
+ }
+ }
+
+ if (displayMode === "sparse" && __extra__) {
+ const rows = this.processPillsAsRows(row, groupPills);
+ return { rows, pillsToProcess: remainingPills };
+ }
+
+ const isGroup = displayMode === "sparse" ? processAsGroup : Boolean(rows);
+
+ const gridRowTypes = isGroup ? { t0: 1 } : { t1: 1 };
+ if (rowPills.length) {
+ if (isGroup) {
+ if (this.shouldComputeAggregateValues(row)) {
+ const groups = this.aggregatePills(rowPills, row);
+ const maxAggregateValue = Math.max(
+ ...groups.map((group) => group.aggregateValue)
+ );
+ rowPills = groups.map((group) =>
+ this.getPillFromGroup(group, maxAggregateValue, consolidate)
+ );
+ } else {
+ rowPills = [];
+ }
+ } else {
+ const level = this.calculatePillsLevel(rowPills);
+ gridRowTypes.t1 = level;
+ if (!this.isTouchDevice) {
+ gridRowTypes.t2 = 1;
+ }
+ }
+ }
+
+ const progressBar = this.getRowProgressBar(groupedByField, resId);
+ if (progressBar && this.isTouchDevice && (!gridRowTypes.t1 || gridRowTypes.t1 === 1)) {
+ // In mobile: rows span over 2 rows to alllow progressbars to properly display
+ gridRowTypes.t1 = (gridRowTypes.t1 || 0) + 1;
+ }
+ if (row.id !== "[]") {
+ this.pushGridRows(gridRowTypes);
+ }
+
+ for (const rowPill of rowPills) {
+ rowPill.id = `__pill__${this.nextPillId++}`;
+ const pillFirstRow = this.currentGridRow + rowPill.level;
+ rowPill.grid = {
+ ...rowPill.grid, // rowPill is a shallow copy of a prePill (possibly copied several times)
+ row: [pillFirstRow, pillFirstRow + 1],
+ };
+ if (!isGroup) {
+ const { record } = rowPill;
+ if (this.shouldRenderRecordConnectors(record)) {
+ if (!this.mappingRecordToPillsByRow[record.id]) {
+ this.mappingRecordToPillsByRow[record.id] = {
+ masterIds: record[dependencyField],
+ pills: {},
+ };
+ }
+ this.mappingRecordToPillsByRow[record.id].pills[id] = rowPill;
+ if (!this.mappingRowToPillsByRecord[id]) {
+ this.mappingRowToPillsByRecord[id] = {};
+ }
+ this.mappingRowToPillsByRecord[id][record.id] = rowPill;
+ }
+ }
+ rowPill.rowId = id;
+ this.pills[rowPill.id] = rowPill;
+ }
+
+ this.rowPills[id] = rowPills; // all row pills
+
+ const subRowsCount = Object.values(gridRowTypes).reduce((acc, val) => acc + val, 0);
+ /** @type {Row} */
+ const processedRow = {
+ cellColors: {},
+ fromServer,
+ groupedByField,
+ groupLevel,
+ id,
+ isGroup,
+ name,
+ progressBar,
+ resId,
+ grid: {
+ row: [this.currentGridRow, this.currentGridRow + subRowsCount],
+ },
+ };
+ if (displayUnavailability && !isGroup) {
+ processedRow.unavailabilities = this.getRowUnavailabilities(
+ parentGroupedField || groupedByField,
+ parentResId ?? resId
+ );
+ }
+
+ this.rowByIds[id] = processedRow;
+
+ this.currentGridRow += subRowsCount;
+
+ const field = this.model.metaData.thumbnails[groupedByField];
+ if (field) {
+ const model = this.model.metaData.fields[groupedByField].relation;
+ processedRow.thumbnailUrl = url("/web/image", {
+ model,
+ id: resId,
+ field,
+ });
+ }
+
+ const result = { rows: [processedRow], pillsToProcess: remainingPills };
+
+ if (!this.model.isClosed(id)) {
+ if (rows) {
+ let pillsToProcess = groupPills;
+ for (const subRow of rows) {
+ const res = this.processRow(subRow, pillsToProcess);
+ result.rows.push(...res.rows);
+ pillsToProcess = res.pillsToProcess;
+ }
+ } else if (displayMode === "sparse" && processAsGroup) {
+ const rows = this.processPillsAsRows(row, groupPills);
+ result.rows.push(...rows);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @param {string} [groupedByField]
+ * @param {false|number} [resId]
+ * @returns {{ start: DateTime, stop: DateTime }[]}
+ */
+ _getRowUnavailabilities(groupedByField, resId) {
+ const { unavailabilities } = this.model.data;
+ if (groupedByField) {
+ return unavailabilities[groupedByField]?.[resId ?? false] || [];
+ }
+ return unavailabilities.__default?.false || [];
+ }
+
+ /**
+ * @param {Object} params
+ * @param {Element} params.pill
+ * @param {number} params.diff
+ * @param {"start" | "end"} params.direction
+ */
+ async resizePillDrop({ pill, diff, direction }) {
+ const { dateStartField, dateStopField, scale } = this.model.metaData;
+ const { cellTime, time } = scale;
+ const { record } = this.pills[pill.dataset.pillId];
+ const params = this.getScheduleParams(pill);
+
+ if (direction === "start") {
+ params.start = dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });
+ } else {
+ params.stop = dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });
+ }
+ const schedule = this.model.getSchedule(params);
+
+ await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);
+ }
+
+ /**
+ * @param {Partial} params
+ * @param {PillId | null} [sourceId=null]
+ * @param {PillId | null} [targetId=null]
+ */
+ setConnector(params, sourceId = null, targetId = null) {
+ const connectorParams = { ...params };
+ const connectorId = params.id || `__connector__${this.nextConnectorId++}`;
+
+ if (sourceId) {
+ connectorParams.sourcePoint = () => this.getPoint(sourceId, true);
+ }
+
+ if (targetId) {
+ connectorParams.targetPoint = () => this.getPoint(targetId, false);
+ }
+
+ if (this.connectors[connectorId]) {
+ Object.assign(this.connectors[connectorId], connectorParams);
+ } else {
+ this.connectors[connectorId] = {
+ id: connectorId,
+ highlighted: false,
+ displayButtons: false,
+ ...connectorParams,
+ };
+ this.mappingConnectorToPills[connectorId] = {
+ sourcePillId: sourceId,
+ targetPillId: targetId,
+ };
+ }
+
+ if (sourceId) {
+ if (!this.mappingPillToConnectors[sourceId]) {
+ this.mappingPillToConnectors[sourceId] = [];
+ }
+ this.mappingPillToConnectors[sourceId].push(connectorId);
+ }
+
+ if (targetId) {
+ if (!this.mappingPillToConnectors[targetId]) {
+ this.mappingPillToConnectors[targetId] = [];
+ }
+ this.mappingPillToConnectors[targetId].push(connectorId);
+ }
+ }
+
+ /**
+ * @param {HTMLElement} [pillEl]
+ */
+ setStickyPill(pillEl) {
+ this.stickyPillId = pillEl ? pillEl.dataset.pillId : null;
+ }
+
+ /**
+ * @param {Row} row
+ */
+ shouldComputeAggregateValues(row) {
+ return true;
+ }
+
+ shouldMergeGroups() {
+ return true;
+ }
+
+ /**
+ * Returns whether connectors should be rendered or not.
+ * The connectors won't be rendered on sampleData as we can't be sure that data are coherent.
+ * The connectors won't be rendered on mobile as the usability is not guarantied.
+ *
+ * @return {boolean}
+ */
+ shouldRenderConnectors() {
+ return (
+ this.model.metaData.dependencyField && !this.model.useSampleModel && !this.env.isSmall
+ );
+ }
+
+ /**
+ * Returns whether connectors should be rendered on particular records or not.
+ * This method is intended to be overridden in particular modules in order to set particular record's condition.
+ *
+ * @param {RelationalRecord} record
+ * @return {boolean}
+ */
+ shouldRenderRecordConnectors(record) {
+ return this.shouldRenderConnectors();
+ }
+
+ /**
+ * @param {ConnectorId | null} connectorId
+ * @param {boolean} highlighted
+ */
+ toggleConnectorHighlighting(connectorId, highlighted) {
+ const connector = this.connectors[connectorId];
+ if (!connector || (!connector.highlighted && !highlighted)) {
+ return;
+ }
+
+ connector.highlighted = highlighted;
+ connector.displayButtons = highlighted;
+
+ const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];
+
+ this.highlightPill(sourcePillId, highlighted);
+ this.highlightPill(targetPillId, highlighted);
+ }
+
+ /**
+ * @param {PillId} pillId
+ * @param {boolean} highlighted
+ */
+ togglePillHighlighting(pillId, highlighted) {
+ const pill = this.pills[pillId];
+ if (!pill || pill.highlighted === highlighted) {
+ return;
+ }
+
+ const { record } = pill;
+ const pillIdsToHighlight = new Set([pillId]);
+
+ if (record && this.shouldRenderRecordConnectors(record)) {
+ // Find other related pills
+ const { pills: relatedPills } = this.mappingRecordToPillsByRow[record.id];
+ for (const pill of Object.values(relatedPills)) {
+ pillIdsToHighlight.add(pill.id);
+ }
+
+ // Highlight related connectors
+ for (const [connectorId, connector] of Object.entries(this.connectors)) {
+ const ids = Object.values(this.getRecordIds(connectorId));
+ if (ids.includes(record.id)) {
+ connector.highlighted = highlighted;
+ connector.displayButtons = false;
+ }
+ }
+ }
+
+ // Highlight pills from found IDs
+ for (const id of pillIdsToHighlight) {
+ this.highlightPill(id, highlighted);
+ }
+ }
+
+ //-------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ onCellClicked(rowId, col) {
+ if (!this.preventClick) {
+ this.preventClick = true;
+ setTimeout(() => (this.preventClick = false), 1000);
+ const { canCellCreate, canPlan } = this.model.metaData;
+ if (canPlan) {
+ this.onPlan(rowId, col, col);
+ } else if (canCellCreate) {
+ this.onCreate(rowId, col, col);
+ }
+ }
+ }
+
+ onCreate(rowId, startCol, stopCol) {
+ const { start, stop } = this.getColumnStartStop(startCol, stopCol);
+ const context = this.model.getDialogContext({
+ rowId,
+ start,
+ stop,
+ withDefault: true,
+ });
+ this.props.create(context);
+ }
+
+ onInteractionChange() {
+ let { dragAction, mode } = this.interaction;
+ if (mode === "drag") {
+ mode = dragAction;
+ }
+ if (this.gridRef.el) {
+ for (const [action, className] of INTERACTION_CLASSNAMES) {
+ this.gridRef.el.classList.toggle(className, mode === action);
+ }
+ }
+ }
+
+ onPointerLeave() {
+ this.throttledComputeHoverParams.cancel();
+
+ if (!this.isDragging) {
+ const hoveredConnectorId = this.hovered.connector?.dataset.connectorId;
+ this.toggleConnectorHighlighting(hoveredConnectorId, false);
+
+ const hoveredPillId = this.hovered.pill?.dataset.pillId;
+ this.togglePillHighlighting(hoveredPillId, false);
+ }
+
+ this.hovered.connector = null;
+ this.hovered.pill = null;
+ this.hovered.hoverable = null;
+
+ this.computeDerivedParamsFromHover();
+ }
+
+ /**
+ * Updates all hovered elements, then calls "computeDerivedParamsFromHover".
+ *
+ * @see computeDerivedParamsFromHover
+ * @param {Event} ev
+ */
+ computeHoverParams(ev) {
+ // Lazily compute elements from point as it is a costly operation
+ let els = null;
+ let position = {};
+ if (ev.type === "scroll") {
+ position = this.cursorPosition;
+ } else {
+ position.x = ev.clientX;
+ position.y = ev.clientY;
+ this.cursorPosition = position;
+ }
+ const pointedEls = () => els || (els = document.elementsFromPoint(position.x, position.y));
+
+ // To find hovered elements, also from pointed elements
+ const find = (selector) =>
+ ev.target.closest?.(selector) ||
+ pointedEls().find((el) => el.matches(selector)) ||
+ null;
+
+ this.hovered.connector = find(".o_gantt_connector");
+ this.hovered.hoverable = find(".o_gantt_hoverable");
+ this.hovered.pill = find(".o_gantt_pill_wrapper");
+
+ this.computeDerivedParamsFromHover();
+ }
+
+ /**
+ * @param {PointerEvent} ev
+ * @param {Pill} pill
+ */
+ onPillClicked(ev, pill) {
+ if (this.popover.isOpen) {
+ return;
+ }
+ this.popover.target = ev.target.closest(".o_gantt_pill_wrapper");
+ this.popover.open(this.popover.target, this.getPopoverProps(pill));
+ }
+
+ onPlan(rowId, startCol, stopCol) {
+ const { start, stop } = this.getColumnStartStop(startCol, stopCol);
+ this.dialogService.add(
+ SelectCreateDialog,
+ this.getSelectCreateDialogProps({ rowId, start, stop, withDefault: true })
+ );
+ }
+
+ getRecordIds(connectorId) {
+ const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];
+ return {
+ masterId: this.pills[sourcePillId]?.record.id,
+ slaveId: this.pills[targetPillId]?.record.id,
+ };
+ }
+
+ /**
+ *
+ * @param {Object} params
+ * @param {ConnectorId} connectorId
+ */
+ onRemoveButtonClick(connectorId) {
+ const { masterId, slaveId } = this.getRecordIds(connectorId);
+ this.model.removeDependency(masterId, slaveId);
+ }
+ rescheduleAccordingToDependencyCallback(result) {
+ if (result["type"] !== "warning" && "old_vals_per_pill_id" in result) {
+ this.model.toggleHighlightPlannedFilter(
+ Object.keys(result["old_vals_per_pill_id"]).map(Number)
+ );
+ }
+ this.notificationFn?.();
+ this.notificationFn = this.notificationService.add(
+ markup(
+ `${escape(
+ result["message"]
+ )}`
+ ),
+ {
+ type: result["type"],
+ sticky: true,
+ buttons:
+ result["type"] === "warning"
+ ? []
+ : [
+ {
+ name: "Undo",
+ icon: "fa-undo",
+ onClick: async () => {
+ const ids = Object.keys(result["old_vals_per_pill_id"]).map(
+ Number
+ );
+ await this.orm.call(
+ this.model.metaData.resModel,
+ "action_rollback_scheduling",
+ [ids, result["old_vals_per_pill_id"]]
+ );
+ this.notificationFn();
+ await this.model.fetchData();
+ },
+ },
+ ],
+ }
+ );
+ }
+
+ /**
+ *
+ * @param {"forward" | "backward"} direction
+ * @param {ConnectorId} connectorId
+ */
+ async onRescheduleButtonClick(direction, connectorId) {
+ const { masterId, slaveId } = this.getRecordIds(connectorId);
+ await this.model.rescheduleAccordingToDependency(
+ direction,
+ masterId,
+ slaveId,
+ this.rescheduleAccordingToDependencyCallback.bind(this)
+ );
+ }
+
+ /**
+ * @param {KeyboardEvent} ev
+ */
+ onWindowKeyDown(ev) {
+ if (ev.key === "Control") {
+ this.prevDragAction =
+ this.interaction.dragAction === "copy" ? "reschedule" : this.interaction.dragAction;
+ this.interaction.dragAction = "copy";
+ }
+ }
+
+ /**
+ * @param {KeyboardEvent} ev
+ */
+ onWindowKeyUp(ev) {
+ if (ev.key === "Control") {
+ this.interaction.dragAction = this.prevDragAction || "reschedule";
+ }
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_renderer.xml b/addons_extensions/web_gantt/static/src/gantt_renderer.xml
new file mode 100644
index 000000000..ebae7f61d
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_renderer.xml
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_renderer_controls.js b/addons_extensions/web_gantt/static/src/gantt_renderer_controls.js
new file mode 100644
index 000000000..048bd49cf
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_renderer_controls.js
@@ -0,0 +1,219 @@
+import { Component, useState } from "@odoo/owl";
+import { useDateTimePicker } from "@web/core/datetime/datetime_hook";
+import { Dropdown } from "@web/core/dropdown/dropdown";
+import { useDropdownState } from "@web/core/dropdown/dropdown_hooks";
+import { DropdownItem } from "@web/core/dropdown/dropdown_item";
+import { formatDate } from "@web/core/l10n/dates";
+import { _t } from "@web/core/l10n/translation";
+import { pick } from "@web/core/utils/objects";
+import { debounce } from "@web/core/utils/timing";
+import {
+ diffColumn,
+ getRangeFromDate,
+ localStartOf,
+ useGanttResponsivePopover,
+} from "./gantt_helpers";
+
+const { DateTime } = luxon;
+
+const KEYS = ["startDate", "stopDate", "rangeId", "focusDate"];
+
+export class GanttRendererControls extends Component {
+ static template = "web_gantt.GanttRendererControls";
+ static components = {
+ Dropdown,
+ DropdownItem,
+ };
+ static props = ["model", "displayExpandCollapseButtons", "focusToday", "getCurrentFocusDate"];
+ static toolbarContentTemplate = "web_gantt.GanttRendererControls.ToolbarContent";
+ static rangeMenuTemplate = "web_gantt.GanttRendererControls.RangeMenu";
+
+ setup() {
+ this.model = this.props.model;
+ this.updateMetaData = debounce(() => this.model.fetchData(this.makeParams()), 500);
+
+ const { metaData } = this.model;
+ this.state = useState({
+ scaleIndex: this.getScaleIndex(metaData.scale.id),
+ ...pick(metaData, ...KEYS),
+ });
+ this.pickerValues = useState({
+ startDate: metaData.startDate,
+ stopDate: metaData.stopDate,
+ });
+ this.scalesRange = { min: 0, max: Object.keys(metaData.scales).length - 1 };
+
+ const getPickerProps = (key) => ({ type: "date", value: this.pickerValues[key] });
+ this.startPicker = useDateTimePicker({
+ target: "start-picker",
+ onApply: (date) => {
+ this.pickerValues.startDate = date;
+ if (this.pickerValues.stopDate < date) {
+ this.pickerValues.stopDate = date;
+ } else if (date.plus({ year: 10, day: -1 }) < this.pickerValues.stopDate) {
+ this.pickerValues.stopDate = date.plus({ year: 10, day: -1 });
+ }
+ },
+ get pickerProps() {
+ return getPickerProps("startDate");
+ },
+ createPopover: (...args) => useGanttResponsivePopover(_t("Gantt start date"), ...args),
+ ensureVisibility: () => false,
+ });
+ this.stopPicker = useDateTimePicker({
+ target: "stop-picker",
+ onApply: (date) => {
+ this.pickerValues.stopDate = date;
+ if (date < this.pickerValues.startDate) {
+ this.pickerValues.startDate = date;
+ } else if (this.pickerValues.startDate.plus({ year: 10, day: -1 }) < date) {
+ this.pickerValues.startDate = date.minus({ year: 10, day: -1 });
+ }
+ },
+ get pickerProps() {
+ return getPickerProps("stopDate");
+ },
+ createPopover: (...args) => useGanttResponsivePopover(_t("Gantt stop date"), ...args),
+ ensureVisibility: () => false,
+ });
+
+ this.dropdownState = useDropdownState();
+ }
+
+ get dateDescription() {
+ const { focusDate, rangeId } = this.state;
+ switch (rangeId) {
+ case "quarter":
+ return focusDate.toFormat(`Qq yyyy`);
+ case "day":
+ return formatDate(focusDate);
+ default:
+ return this.model.metaData.scales[rangeId].groupHeaderFormatter(
+ focusDate,
+ this.env
+ );
+ }
+ }
+
+ getFormattedDate(date) {
+ return formatDate(date);
+ }
+
+ getScaleIdFromIndex(index) {
+ const keys = Object.keys(this.model.metaData.scales);
+ return keys[keys.length - 1 - index];
+ }
+
+ getScaleIndex(scaleId) {
+ const keys = Object.keys(this.model.metaData.scales);
+ return keys.length - 1 - keys.findIndex((id) => id === scaleId);
+ }
+
+ getScaleIndexFromRangeId(rangeId) {
+ const { ranges } = this.model.metaData;
+ const scaleId = ranges[rangeId].scaleId;
+ return this.getScaleIndex(scaleId);
+ }
+
+ /**
+ * @param {1|-1} inc
+ */
+ incrementScale(inc) {
+ if (
+ inc === 1
+ ? this.state.scaleIndex < this.scalesRange.max
+ : this.scalesRange.min < this.state.scaleIndex
+ ) {
+ this.state.scaleIndex += inc;
+ this.updateMetaData();
+ }
+ }
+
+ isSelected(rangeId) {
+ if (rangeId === "custom") {
+ return (
+ this.state.rangeId === rangeId ||
+ !localStartOf(this.state.focusDate, this.state.rangeId).equals(
+ localStartOf(DateTime.now(), this.state.rangeId)
+ )
+ );
+ }
+ return (
+ this.state.rangeId === rangeId &&
+ localStartOf(this.state.focusDate, rangeId).equals(
+ localStartOf(DateTime.now(), rangeId)
+ )
+ );
+ }
+
+ makeParams() {
+ return {
+ currentFocusDate: this.props.getCurrentFocusDate(),
+ scaleId: this.getScaleIdFromIndex(this.state.scaleIndex),
+ ...pick(this.state, ...KEYS),
+ };
+ }
+
+ onApply() {
+ this.state.startDate = this.pickerValues.startDate;
+ this.state.stopDate = this.pickerValues.stopDate;
+ this.state.rangeId = "custom";
+ this.updateMetaData();
+ this.dropdownState.close();
+ }
+
+ onTodayClicked() {
+ const success = this.props.focusToday();
+ if (success) {
+ return;
+ }
+ this.state.focusDate = DateTime.local().startOf("day");
+ if (this.state.rangeId === "custom") {
+ const diff = diffColumn(this.state.startDate, this.state.stopDate, "day");
+ const n = Math.floor(diff / 2);
+ const m = diff - n;
+ this.state.startDate = this.state.focusDate.minus({ day: n });
+ this.state.stopDate = this.state.focusDate.plus({ day: m - 1 });
+ } else {
+ this.state.startDate = this.state.focusDate.startOf(this.state.rangeId);
+ this.state.stopDate = this.state.focusDate.endOf(this.state.rangeId).startOf("day");
+ }
+ this.updatePickerValues();
+ this.updateMetaData();
+ }
+
+ selectRange(direction) {
+ const sign = direction === "next" ? 1 : -1;
+ const { focusDate, rangeId, startDate, stopDate } = this.state;
+ if (rangeId === "custom") {
+ const diff = diffColumn(startDate, stopDate, "day") + 1;
+ this.state.focusDate = focusDate.plus({ day: sign * diff });
+ this.state.startDate = startDate.plus({ day: sign * diff });
+ this.state.stopDate = stopDate.plus({ day: sign * diff });
+ } else {
+ Object.assign(
+ this.state,
+ getRangeFromDate(rangeId, focusDate.plus({ [rangeId]: sign }))
+ );
+ }
+ this.updatePickerValues();
+ this.updateMetaData();
+ }
+
+ selectRangeId(rangeId) {
+ Object.assign(this.state, getRangeFromDate(rangeId, DateTime.now().startOf("day")));
+ this.state.scaleIndex = this.getScaleIndexFromRangeId(rangeId);
+ this.updatePickerValues();
+ this.updateMetaData();
+ }
+
+ selectScale(index) {
+ this.state.scaleIndex = Number(index);
+ this.updateMetaData();
+ }
+
+ updatePickerValues() {
+ this.pickerValues.startDate = this.state.startDate;
+ this.pickerValues.stopDate = this.state.stopDate;
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_renderer_controls.xml b/addons_extensions/web_gantt/static/src/gantt_renderer_controls.xml
new file mode 100644
index 000000000..fca93fdee
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_renderer_controls.xml
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Activate sparse mode
+
+
+
+ Activate dense mode
+
+
+
+
+
+
+ Expand rows
+
+
+
+ Collapse rows
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_resize_badge.js b/addons_extensions/web_gantt/static/src/gantt_resize_badge.js
new file mode 100644
index 000000000..08bc16b5c
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_resize_badge.js
@@ -0,0 +1,44 @@
+import { Component } from "@odoo/owl";
+
+export class GanttResizeBadge extends Component {
+ static props = {
+ reactive: {
+ type: Object,
+ shape: {
+ position: {
+ type: Object,
+ shape: {
+ top: Number,
+ right: { type: Number, optional: true },
+ left: { type: Number, optional: true },
+ },
+ optional: true,
+ },
+ diff: { type: Number, optional: true },
+ scale: { type: String, optional: true },
+ },
+ },
+ };
+ static template = "web_gantt.GanttResizeBadge";
+
+ get diff() {
+ return this.props.reactive.diff || 0;
+ }
+
+ get diffText() {
+ const { diff, props } = this;
+ const prefix = this.diff > 0 ? "+" : "";
+ return `${prefix}${diff} ${props.reactive.scale}`;
+ }
+
+ get positionStyle() {
+ const { position } = this.props.reactive;
+ const style = [`top:${position.top}px`];
+ if ("left" in position) {
+ style.push(`left:${position.left}px`);
+ } else {
+ style.push(`right:${position.right}px`);
+ }
+ return style.join(";");
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_resize_badge.xml b/addons_extensions/web_gantt/static/src/gantt_resize_badge.xml
new file mode 100644
index 000000000..d4076d21f
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_resize_badge.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.js b/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.js
new file mode 100644
index 000000000..cfbc81fc0
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.js
@@ -0,0 +1,36 @@
+import { Component } from "@odoo/owl";
+import { hasTouch, isMobileOS } from "@web/core/browser/feature_detection";
+
+export class GanttRowProgressBar extends Component {
+ static props = {
+ reactive: {
+ type: Object,
+ shape: {
+ hoveredRowId: [String, { value: null }],
+ },
+ },
+ rowId: String,
+ progressBar: {
+ type: Object,
+ shape: {
+ max_value: Number,
+ max_value_formatted: String,
+ ratio: Number,
+ value_formatted: String,
+ warning: { type: String, optional: true },
+ "*": true,
+ },
+ },
+ };
+ static template = "web_gantt.GanttRowProgressBar";
+
+ get show() {
+ const { reactive, rowId } = this.props;
+ return reactive.hoveredRowId === rowId || isMobileOS() || hasTouch();
+ }
+
+ get status() {
+ const { ratio } = this.props.progressBar;
+ return ratio > 100 ? "danger" : ratio > 0 ? "success" : null;
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.xml b/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.xml
new file mode 100644
index 000000000..d31ed7b68
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_row_progress_bar.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/addons_extensions/web_gantt/static/src/gantt_sample_server.js b/addons_extensions/web_gantt/static/src/gantt_sample_server.js
new file mode 100644
index 000000000..bec225e6f
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_sample_server.js
@@ -0,0 +1,43 @@
+import { registry } from "@web/core/registry";
+
+function _mockGetGanttData(params) {
+ const lazy = !params.limit && !params.offset && params.groupby.length === 1;
+ let { groups, length } = this._mockWebReadGroup({
+ ...params,
+ lazy,
+ fields: ["__record_ids:array_agg(id)"],
+ });
+ if (params.limit) {
+ // we don't care about pager feature in sample mode
+ // but we want to present something coherent
+ groups = groups.slice(0, params.limit);
+ length = groups.length;
+ }
+ groups.forEach((g) => (g.__record_ids = g.id)); // the sample server does not use the key __record_ids
+
+ const recordIds = [];
+ for (const group of groups) {
+ recordIds.push(...(group.__record_ids || []));
+ }
+
+ const { records } = this._mockWebSearchReadUnity({
+ model: params.model,
+ domain: [["id", "in", recordIds]],
+ context: params.context,
+ specification: params.read_specification,
+ });
+
+ const unavailabilities = {};
+ for (const fieldName of params.unavailability_fields || []) {
+ unavailabilities[fieldName] = {};
+ }
+
+ const progress_bars = {};
+ for (const fieldName of params.progress_bar_fields || []) {
+ progress_bars[fieldName] = {};
+ }
+
+ return { groups, length, records, unavailabilities, progress_bars };
+}
+
+registry.category("sample_server").add("get_gantt_data", _mockGetGanttData);
diff --git a/addons_extensions/web_gantt/static/src/gantt_view.dark.scss b/addons_extensions/web_gantt/static/src/gantt_view.dark.scss
new file mode 100644
index 000000000..391a52458
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_view.dark.scss
@@ -0,0 +1,21 @@
+// = Gantt View
+// ============================================================================
+// No CSS hacks, variables overrides only
+
+.o_web_client .o_gantt_view {
+ --Gantt__DayOff-background-color: rgba(255, 255, 255, .05);
+ // Mix between $gantt-highlight-today-bg and $o-view-background-color
+ // to simulate the superposition of these two colors
+ --Gantt__DayOffToday-background-color: #553F3A;
+
+ .o_gantt_connector {
+ --Connector__ButtonBackground-color: #{$o-view-background-color};
+ --Connector__ButtonReschedule-color: #{darken($o-component-active-border, 10%)};
+ --Connector__ButtonBorder-color: #{$o-gray-500};
+ --Connector__ButtonAccent-color: #{$o-black};
+ }
+
+ .o_gantt_renderer {
+ --Gantt__DayOff-background-color: #{$o-gray-300};
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_view.js b/addons_extensions/web_gantt/static/src/gantt_view.js
new file mode 100644
index 000000000..64978218b
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_view.js
@@ -0,0 +1,62 @@
+import { registry } from "@web/core/registry";
+import { scrollSymbol } from "@web/search/action_hook";
+import { GanttArchParser } from "./gantt_arch_parser";
+import { GanttController } from "./gantt_controller";
+import { GanttModel } from "./gantt_model";
+import { GanttRenderer } from "./gantt_renderer";
+import { omit } from "@web/core/utils/objects";
+
+const viewRegistry = registry.category("views");
+
+export const ganttView = {
+ type: "gantt",
+ Controller: GanttController,
+ Renderer: GanttRenderer,
+ Model: GanttModel,
+ ArchParser: GanttArchParser,
+ searchMenuTypes: ["filter", "groupBy", "favorite"],
+ buttonTemplate: "web_gantt.GanttView.Buttons",
+
+ props: (genericProps, view, config) => {
+ const modelParams = {};
+ let scrollPosition;
+ if (genericProps.state) {
+ scrollPosition = genericProps.state[scrollSymbol];
+ modelParams.metaData = genericProps.state.metaData;
+ modelParams.displayParams = genericProps.state.displayParams;
+ } else {
+ const { arch, fields, resModel } = genericProps;
+ const parser = new view.ArchParser();
+ const archInfo = parser.parse(arch);
+
+ let formViewId = archInfo.formViewId;
+ if (!formViewId) {
+ const formView = config.views.find((v) => v[1] === "form");
+ if (formView) {
+ formViewId = formView[0];
+ }
+ }
+
+ modelParams.metaData = {
+ ...omit(archInfo, "displayMode"),
+ fields,
+ resModel,
+ formViewId,
+ };
+ modelParams.displayParams = {
+ displayMode: archInfo.displayMode,
+ };
+ }
+
+ return {
+ ...genericProps,
+ modelParams,
+ Model: view.Model,
+ Renderer: view.Renderer,
+ buttonTemplate: view.buttonTemplate,
+ scrollPosition,
+ };
+ },
+};
+
+viewRegistry.add("gantt", ganttView);
diff --git a/addons_extensions/web_gantt/static/src/gantt_view.scss b/addons_extensions/web_gantt/static/src/gantt_view.scss
new file mode 100644
index 000000000..0c3a7a18c
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_view.scss
@@ -0,0 +1,682 @@
+.o_gantt_view {
+ --Gantt__Buttons-height: 45px;
+
+ @media (max-width: 767px) {
+ --Gantt__Buttons-height: 91px; // = 2 times the height in normal display + 1px for the
element
+ }
+
+ user-select: none;
+
+ .o_view_nocontent {
+ @include o-gantt-zindex(view-nocontent);
+ position: fixed;
+ top: 225px;
+ }
+
+ .o_view_sample_data .o_sample_data_disabled {
+ @include o-sample-data-disabled;
+ }
+
+ .o_gantt_renderer_controls {
+ @include o-gantt-zindex(controls);
+ height: var(--Gantt__Buttons-height) !important;
+ input[type="range"] {
+ min-width: 80px;
+ max-width: 80px;
+ }
+ }
+
+ @include media-only(print) {
+ > .o_content {
+ overflow: auto;
+ }
+ }
+}
+
+.o_gantt_range_menu {
+ .o_gantt_range_custom_item.dropdown-item:not(.disabled):not(:disabled) {
+ background-color: inherit !important;
+ cursor: default !important;
+ label {
+ cursor: default !important;
+ }
+ &.selected {
+ font-weight: 400;
+ }
+ }
+}
+
+.o_gantt_view .o_gantt_renderer {
+
+ // Renderer grid
+ display: grid;
+
+ grid-template-columns:
+ [row-headers] var(--Gantt__RowHeader-width) [content] 1fr;
+
+ --Gantt__RowHeader-template-column: 16px;
+
+ // Allows to use color variables in js
+ --Gantt__Day-background-color: #{$o-view-background-color};
+ --Gantt__DayOff-background-color: #e9ecef;
+ --Gantt__DayOffToday-background-color: #fffaeb;
+
+ // Group colors
+ --Gantt__Group-background: linear-gradient(#{darken($gantt-row-open-bg, 5%)},
+ #{$gantt-row-open-bg});
+ --Gantt__GroupOpen-background: linear-gradient(#{$gantt-row-open-bg},
+ #{darken($gantt-row-open-bg, 5%)});
+ --Gantt__GroupToday-background: #{mix($gantt-row-open-bg, $gantt-highlight-today-bg)};
+
+ // ============================= Main Layout ==============================
+ // ========================================================================
+
+ // Utilities to use the grid-template-rows or grid-template-columns
+ .o_gantt_grid_rows {
+ display: grid;
+ grid-template-rows: var(--Gantt__GridRows-grid-template-rows);
+ }
+
+ .o_gantt_grid_columns {
+ display: grid;
+ grid-column: 2 / span 2;
+ grid-template-columns: var(--Gantt__GridColumns-grid-template-columns);
+ }
+
+ // Row headers
+ .o_gantt_row_header {
+ cursor: pointer;
+ display: grid;
+ grid-column: 1 / -1;
+ grid-row: 3;
+ grid-template-columns: repeat(auto-fill,
+ minmax(var(--Gantt__RowHeader-template-column), 1fr));
+ line-height: var(--Gantt__Pill-height);
+
+ .o_gantt_progress_bar {
+ grid-row: 1;
+ grid-column: 1 / -1;
+ }
+
+ .o_gantt_row_title {
+ grid-row: 1;
+ }
+
+ &.o_gantt_group {
+ line-height: initial;
+ }
+
+ &.o_mobile_progress_bar {
+ grid-template-rows: 1fr var(--Gantt__Pill-height);
+
+ .o_gantt_progress_bar {
+ grid-row: 2;
+ }
+ }
+ }
+
+ // =============================== Buttons ================================
+ // ========================================================================
+
+ .o_gantt_buttons_container {
+ gap: 0.25rem 1rem;
+ }
+
+ // All rows (Regular, Group Header and Total)
+ // ==========================================
+ .o_gantt_row_thumbnail_wrapper .o_gantt_row_thumbnail {
+ width: auto;
+ max-height: var(--Gantt__Thumbnail-max-height);
+ }
+
+ // =============== Cursors while dragging ==============
+ // =======================================================
+
+ &.o_grabbing,
+ &.o_grabbing .o_gantt_pill {
+ cursor: move !important;
+ }
+
+ &.o_copying,
+ &.o_copying .o_gantt_pill {
+ cursor: copy !important;
+ }
+
+ &.o_grabbing_locked,
+ &.o_grabbing_locked .o_gantt_pill {
+ cursor: not-allowed !important;
+ }
+
+ @include media-breakpoint-down(md) {
+ & {
+ width: max-content;
+ }
+ }
+
+ .o_dragged_pill_ghost {
+ opacity: 0.5;
+ }
+
+ // ================ Header ===============
+ // =======================================
+
+ .o_gantt_title {
+ top: var(--Gantt__Buttons-height);
+ @include o-gantt-zindex(title);
+ border-bottom: 1px solid $gantt-border-color;
+ grid-row: 1 / span 2;
+ }
+
+ .o_gantt_header_groups {
+ top: var(--Gantt__Buttons-height);
+ @include o-gantt-zindex(column-header-groups);
+ grid-row: 1;
+ }
+
+ .o_gantt_header_columns {
+ top: var(--Gantt__Buttons-height);
+ @include o-gantt-zindex(headers);
+ grid-row: 2;
+ margin-top: calc(var(--Gantt__Pill-height) * -1);
+ padding-top: var(--Gantt__Pill-height);
+ }
+
+ .o_gantt_header_cell {
+ border: 1px solid transparent;
+ border-bottom-color: $gantt-border-color;
+ border-right-color: $gantt-border-color;
+ height: var(--Gantt__Pill-height);
+ color: $headings-color;
+ position: relative;
+ @include o-gantt-cell;
+
+ @include media-breakpoint-down(md) {
+ min-width: 0;
+ }
+ }
+
+ .o_gantt_header_title {
+ height: var(--Gantt__Pill-height);
+ left: calc(var(--Gantt__RowHeader-width) - 1px);
+ border: solid $gantt-border-color;
+ border-width: 0 0 1px 1px;
+ margin-left: -1px;
+ }
+
+ // ======= All sidebar headers (Header, Regular, Groups and Total) ========
+ // ========================================================================
+ .o_gantt_row_sidebar {
+ @include o-gantt-zindex(headers);
+ grid-column: 1;
+ color: $headings-color;
+
+ &:not(.o_gantt_row_headers) {
+ border-bottom: 1px solid $gantt-border-color;
+ }
+
+ .o_gantt_progressbar,
+ .o_gantt_text_hoverable {
+ right: 0;
+ height: 100%;
+ background-color: inherit;
+ }
+ }
+
+ // =================== "Regular" & "Group Header" cells ===================
+ // ========================================================================
+ .o_gantt_cell {
+ border: solid $gantt-border-color;
+ border-width: 0 1px 1px 0;
+ @include o-gantt-cell;
+ @include o-gantt-zindex(grid);
+
+ &.o_drag_hover {
+ background: $gantt-highlight-cell-color !important;
+ @include o-gantt-zindex(grid-interact);
+ }
+ }
+
+ // ================================ Pills =================================
+ // ========================================================================
+ .o_gantt_pill_wrapper {
+ padding: 2px 2px 3px 3px;
+ min-height: var(--Gantt__Pill-height);
+ @include o-gantt-zindex(pill);
+
+ // Group pills
+ &.o_gantt_group_pill {
+ pointer-events: none;
+ min-height: auto;
+ display: grid;
+
+ .o_gantt_pill {
+ grid-area: 1 / 1;
+ background-color: $primary;
+ border-color: $primary;
+ height: 2px;
+
+ &:before,
+ &:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 5px solid transparent;
+ }
+
+ &:before {
+ border-left: 5px solid;
+ border-left-color: inherit;
+ @include o-position-absolute($top: -3px, $left: 0);
+ }
+
+ &:after {
+ border-right: 5px solid;
+ border-right-color: inherit;
+ @include o-position-absolute($top: -3px, $right: 0);
+ }
+ }
+
+ &.o_group_open .o_gantt_pill {
+
+ &:before,
+ &:after {
+ top: 2px;
+ border: 2px solid transparent;
+ border-top-color: inherit;
+ }
+
+ &:before {
+ border-left-color: inherit;
+ }
+
+ &:after {
+ border-right-color: inherit;
+ }
+ }
+
+ .o_gantt_pill_title {
+ grid-area: 1 / 1;
+ width: fit-content;
+ }
+ }
+
+ &.o_resizable {
+ .o_resize_handle {
+ width: 0.5rem
+ /* 6px */
+ ;
+ pointer-events: auto;
+ position: absolute;
+ top: 0;
+ @include o-gantt-zindex(interact);
+
+ &.o_handle_start {
+ left: 0;
+ }
+
+ &.o_handle_end {
+ right: 0;
+ }
+ }
+
+ @include o-gantt-hover() {
+ &:not(.o_resized) .o_resize_handle {
+ background-color: rgba(230, 230, 230, 0.5);
+
+ &:hover {
+ background-color: rgba(230, 230, 230, 0.8);
+ }
+ }
+ }
+ }
+
+ &.o_draggable,
+ &.o_undraggable {
+ transition: transform 0.6s, box-shadow 0.3s;
+
+ &.o_dragged {
+ opacity: 0.8;
+ transform: rotate(-3deg);
+ box-shadow: 0 5px 25px -10px black;
+ @include o-gantt-zindex(interact);
+
+ .o_gantt_pill {
+ box-shadow: 0 5px 25px -10px black;
+ }
+
+ .o_resize_handle {
+ visibility: hidden;
+ }
+ }
+ }
+
+ &.o_undraggable:not(.o_dragged) .o_gantt_lock {
+ display: none;
+ }
+
+ &.o_resizable.o_resized {
+ .o_gantt_pill {
+ cursor: inherit;
+ }
+
+ .o_resize_handle {
+ background-color: rgba(black, 0.5);
+ @include o-gantt-zindex(interact);
+ }
+ }
+
+ .o_gantt_consolidated_pill_title {
+ background: none !important;
+ color: $headings-color;
+ position: absolute;
+ top: 21px;
+ font-size: 0.7em;
+
+ &.o_gantt_consolidated_pill_small {
+ transform: rotate(75deg);
+ }
+ }
+
+ &:not(.o_connector_creator_lock):not(.o_connector_creator_highlight) .o_connector_creator_wrapper {
+ display: none;
+ }
+
+ @include o-gantt-hover() {
+ .o_connector_creator_wrapper {
+ display: inline;
+ }
+ }
+
+ // used for `color` attribute on
+ @for $index from 1 through length($o-colors-complete) {
+ $color: nth($o-colors-complete, $index);
+
+ .o_gantt_pill.o_gantt_color_#{$index - 1} .o_gantt_progress {
+ opacity: 0.2;
+ background-color: darken($color, 30%);
+ }
+
+ &.highlight .o_gantt_pill.o_gantt_color_#{$index - 1} {
+ color: color-contrast($color);
+ background-color: $color;
+ }
+ }
+ }
+
+ // ========================== Main pill content ===========================
+ // ========================================================================
+
+ .o_gantt_cells {
+ grid-row: 3;
+ }
+
+ .o_gantt_cells .o_gantt_pill {
+ overflow: hidden;
+ user-select: none;
+ box-sizing: content-box;
+ cursor: pointer;
+ @include o-gantt-hoverable-colors(nth($o-colors-complete, 1));
+
+ .o_gantt_pill_title {
+ // Prevent displaying pill's description when size is smaller than 50px
+ max-width: calc((100% - 50px) * 9999);
+ }
+
+ .o_gantt_pill_avatar {
+ // Prevent displaying pill's avatar when size is smaller than 100px
+ max-width: calc((100% - 100px) * 9999);
+ }
+
+ &.decoration-info {
+ @include o-gantt-gradient-decorations(nth($o-colors-complete, 1));
+ }
+
+ // used for `color` attribute on
+ @for $index from 1 through length($o-colors-complete) {
+ &.o_gantt_color_#{$index - 1} {
+ $gantt-color: nth($o-colors-complete, $index);
+
+ @include o-gantt-hoverable-colors($gantt-color);
+
+ &.decoration-info {
+ @include o-gantt-gradient-decorations($gantt-color);
+ }
+
+ .o_gantt_progress {
+ opacity: 0.2;
+ background-color: darken($gantt-color, 30%);
+ }
+ }
+ }
+
+ @each $color, $value in $theme-colors {
+ &.decoration-#{$color}:before {
+ @include o-gantt-ribbon-decoration($value);
+ }
+ }
+ }
+
+ // ========================= "Group Header" rows ==========================
+ // ========================================================================
+ .o_gantt_group {
+ background: var(--Gantt__Group-background);
+
+ &.o_gantt_today {
+ background: var(--Gantt__GroupToday-background);
+ }
+
+ &.o_gantt_group_hovered:not(.o_gantt_today) {
+ background: var(--Gantt__GroupOpen-background);
+ }
+
+ &.o_group_open {
+ border-left-width: 0;
+ background: var(--Gantt__GroupOpen-background);
+
+ &.o_gantt_group_hovered {
+ background: var(--Gantt__Group-background);
+ }
+ }
+
+ &.o_gantt_row_header b {
+ font-weight: bold;
+ }
+ }
+
+ // ========================== Connector creators ==========================
+ // ========================================================================
+
+ .o_connector_creator_wrapper {
+ height: $o-connector-wrapper-height;
+ @include o-gantt-zindex(interact);
+
+ // used for `color` attribute on
+ @for $index from 1 through length($o-colors-complete) {
+ &.o_gantt_color_#{$index - 1} {
+ $color: nth($o-colors-complete, $index);
+
+ .o_connector_creator_bullet {
+ background-color: $color;
+ color: color-contrast($color);
+ @include o-grab-cursor;
+ }
+
+ .o_connector_creator_top {
+ border-top: solid 1px $color;
+ }
+
+ .o_connector_creator_right {
+ /*rtl:ignore*/
+ border-left: solid 1px $color;
+ }
+
+ .o_connector_creator_bottom {
+ border-bottom: solid 1px $color;
+ }
+
+ .o_connector_creator_left {
+ /*rtl:ignore*/
+ border-right: solid 1px $color;
+ }
+ }
+ }
+ }
+
+ .o_connector_creator_wrapper_top {
+ top: -1 * $o-connector-wrapper-height;
+ }
+
+ .o_connector_creator_wrapper_bottom {
+ bottom: -1 * $o-connector-wrapper-height;
+ }
+
+ .o_connector_creator {
+ height: $o-connector-creator-size;
+ width: $o-connector-creator-size;
+ }
+
+ .o_connector_creator_bullet {
+ height: $o-connector-creator-bullet-diameter;
+ width: $o-connector-creator-bullet-diameter;
+ }
+
+ .o_connector_creator_top {
+ bottom: 0;
+
+ .o_connector_creator_bullet {
+ top: -0.5 * $o-connector-creator-bullet-diameter;
+ }
+ }
+
+ .o_connector_creator_right {
+ /*rtl:ignore*/
+ right: $o-connector-creator-size;
+
+ .o_connector_creator_bullet {
+ /*rtl:ignore*/
+ right: -0.5 * $o-connector-creator-bullet-diameter;
+ }
+ }
+
+ .o_connector_creator_bottom {
+ top: 0;
+
+ .o_connector_creator_bullet {
+ bottom: -0.5 * $o-connector-creator-bullet-diameter;
+ }
+ }
+
+ .o_connector_creator_left {
+ /*rtl:ignore*/
+ left: $o-connector-creator-size;
+
+ .o_connector_creator_bullet {
+ /*rtl:ignore*/
+ left: -0.5 * $o-connector-creator-bullet-diameter;
+ }
+ }
+
+ // ============================= "TOTAL" row ==============================
+ // ========================================================================
+ .o_gantt_row_total {
+ grid-row: 4;
+
+ .o_gantt_cell,
+ .o_gantt_row_title,
+ .o_gantt_pill_wrapper {
+ min-height: calc(var(--Gantt__Pill-height) * 1.6);
+ }
+
+ .o_gantt_pill {
+ color: inherit;
+ margin-left: 1px;
+ background-color: rgba($o-brand-odoo, 0.5);
+ }
+
+ .o_gantt_pill_wrapper:hover {
+ overflow: visible;
+
+ .o_gantt_pill {
+ color: inherit;
+ background-color: rgba($o-brand-odoo, 0.8);
+ }
+
+ &:before {
+ content: "";
+ border: 1px solid $o-brand-odoo;
+ border-width: 0 1px;
+ background: rgba($o-brand-odoo, 0.1);
+ height: 100vh;
+ pointer-events: none;
+ @include o-gantt-zindex(interact);
+ @include o-position-absolute(auto, -1px, 0, 0);
+ }
+ }
+
+ .o_gantt_cell:last-child .o_gantt_pill_wrapper:hover:before {
+ border-right: 0px;
+ right: 0;
+ }
+ }
+
+ .o_gantt_pill_resize_badge {
+ transition: all 0.15s ease-in-out;
+ box-shadow: 0 1px 2px 0 rgba(black, 0.28);
+ @include o-gantt-zindex(badge);
+ }
+
+ .o_gantt_connector {
+ --Connector__ButtonBorder-color: #091124;
+ --Connector__ButtonBackground-color: #ffffff;
+ --Connector__ButtonReschedule-color: #00a09d;
+ --Connector__ButtonRemove-color: #dd3c4f;
+ --Connector__ButtonAccent-color: #ffffff;
+
+ &.o_connector_highlighted {
+ @include o-gantt-zindex(interact);
+ }
+
+ .o_connector_stroke_button {
+ >rect {
+ cursor: pointer;
+ fill: var(--Connector__ButtonBackground-color);
+ stroke: var(--Connector__ButtonBorder-color);
+ stroke-width: 24px;
+ transition: fill 0.15s;
+ }
+
+ &.o_connector_stroke_reschedule_button {
+ line {
+ stroke: var(--Connector__ButtonReschedule-color);
+ transition: stroke 0.15s;
+ }
+
+ &:hover {
+ >rect {
+ fill: var(--Connector__ButtonReschedule-color);
+ }
+
+ line {
+ stroke: var(--Connector__ButtonAccent-color);
+ }
+ }
+ }
+
+ &.o_connector_stroke_remove_button {
+ g rect {
+ fill: var(--Connector__ButtonRemove-color);
+ transition: fill 0.15s;
+ }
+
+ &:hover {
+ >rect {
+ fill: var(--Connector__ButtonRemove-color);
+ }
+
+ g rect {
+ fill: var(--Connector__ButtonAccent-color);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/addons_extensions/web_gantt/static/src/gantt_view.variables.dark.scss b/addons_extensions/web_gantt/static/src/gantt_view.variables.dark.scss
new file mode 100644
index 000000000..fc39ad93c
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_view.variables.dark.scss
@@ -0,0 +1,9 @@
+// = Gantt View Variables
+// ============================================================================
+// No CSS hacks, variables overrides only
+
+$gantt-highlight-today-border: rgba($o-warning, 0.5) !default;
+$gantt-highlight-today-bg: rgba($o-warning, 0.15)!default;
+$gantt-highlight-hover-row: rgba($o-brand-primary, .1) !default;
+$gantt-row-open-bg: $o-gray-100 !default;
+$gantt-unavailability-bg: $o-gray-200 !default;
diff --git a/addons_extensions/web_gantt/static/src/gantt_view.variables.scss b/addons_extensions/web_gantt/static/src/gantt_view.variables.scss
new file mode 100644
index 000000000..0cbb10e78
--- /dev/null
+++ b/addons_extensions/web_gantt/static/src/gantt_view.variables.scss
@@ -0,0 +1,104 @@
+// = Gantt View Variables
+// ============================================================================
+
+// Define the necessary conditions to provide visual feedback on hover,
+// focus, drag, clone and resize.
+@mixin o-gantt-hover() {
+ &:hover,
+ &:focus,
+ &.o_dragged_pill,
+ &.ui-resizable-resize {
+ // Avoid visual feedback if 'o_gantt_renderer' has class 'o_grabbing', 'o_copying', 'o_no_dragging' or 'o_connect'
+ @at-root #{selector-replace(&, ".o_gantt_renderer", ".o_gantt_renderer:not(.o_grabbing):not(.o_copying):not(.o_no_dragging):not(.o_connect)")} {
+ @content;
+ }
+ }
+}
+
+// Generate background and text for each color.
+@mixin o-gantt-hoverable-colors($color) {
+ $color-subdle: mix($color, white, 60%);
+ color: color-contrast($color-subdle);
+ background-color: $color-subdle;
+
+ @include o-gantt-hover() {
+ background-color: $color;
+ color: color-contrast($color);
+ }
+}
+
+// Generate stripes decorations for each color.
+@mixin o-gantt-gradient-decorations($color) {
+ $color-subdle: mix($color, white, 60%);
+ background-image: repeating-linear-gradient(
+ -45deg,
+ $color-subdle 0 10px,
+ lighten($color-subdle, 6%) 10px 20px
+ );
+
+ @include o-gantt-hover() {
+ background-image: repeating-linear-gradient(
+ -45deg,
+ $color 0 10px,
+ lighten($color, 6%) 10px 20px
+ );
+ }
+}
+
+@mixin o-gantt-ribbon-decoration($color) {
+ content: "";
+ width: 20px;
+ height: 16px;
+ @include o-position-absolute(-11px, $left: -13px);
+ box-shadow: 1px 1px 0 white;
+ background: $color;
+ transform: rotate(45deg);
+}
+
+@mixin o-gantt-cell {
+ &.o_gantt_today {
+ background-color: $gantt-highlight-today-bg;
+ border-left-color: $gantt-highlight-today-border;
+ border-top-color: $gantt-highlight-today-border;
+
+ + .o_gantt_header_cell,
+ + .o_gantt_cell {
+ border-left-color: $gantt-highlight-today-border;
+ }
+ }
+}
+
+@mixin o-gantt-zindex($level) {
+ z-index: map-get(
+ (
+ // Grid and grid interactions level
+ grid: 0,
+ grid-interact: 1,
+ // Pills level
+ pill: 10,
+ // Interactions => over pills
+ interact: 20,
+ // No-content helper
+ view-nocontent: 25,
+ headers: 30,
+ // Top-most elements
+ column-header-groups: 35,
+ title: 40,
+ controls: 50,
+ badge: 50,
+ ),
+ $level
+ );
+}
+
+$gantt-border-color: $o-gray-300 !default;
+$gantt-highlight-cell-color: rgba(0, 160, 157, 0.3) !default;
+$gantt-highlight-today-border: #dca665 !default;
+$gantt-highlight-today-bg: #fffaeb !default;
+$gantt-highlight-hover-row: rgba($o-brand-primary, 0.1) !default;
+$gantt-row-open-bg: $o-gray-100 !default;
+$gantt-unavailability-bg: $o-gray-200 !default;
+$o-connector-creator-bullet-radius: 3px !default;
+$o-connector-creator-size: 8px !default;
+$o-connector-creator-bullet-diameter: 2 * $o-connector-creator-bullet-radius !default;
+$o-connector-wrapper-height: $o-connector-creator-size + $o-connector-creator-bullet-radius !default;
diff --git a/addons_extensions/web_gantt/static/tests/gantt_dependency.test.js b/addons_extensions/web_gantt/static/tests/gantt_dependency.test.js
new file mode 100644
index 000000000..96fd0f5a7
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_dependency.test.js
@@ -0,0 +1,921 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import {
+ hover,
+ pointerDown,
+ queryAll,
+ queryFirst,
+ queryOne,
+ queryRect,
+ resize,
+} from "@odoo/hoot-dom";
+import { advanceFrame, animationFrame, mockDate, runAllTimers } from "@odoo/hoot-mock";
+import {
+ contains,
+ defineModels,
+ fields,
+ findComponent,
+ models,
+ onRpc,
+ patchWithCleanup,
+} from "@web/../tests/web_test_helpers";
+import {
+ clickConnectorButton,
+ getConnector,
+ getConnectorMap,
+} from "@web_gantt/../tests/gantt_dependency_helpers";
+import { COLORS } from "@web_gantt/gantt_connector";
+import {
+ CLASSES,
+ SELECTORS,
+ getPill,
+ getPillWrapper,
+ mountGanttView,
+} from "./web_gantt_test_helpers";
+
+import { GanttRenderer } from "@web_gantt/gantt_renderer";
+
+/** @typedef {import("@web_gantt/gantt_renderer").ConnectorProps} ConnectorProps */
+/** @typedef {import("@web_gantt/gantt_renderer").PillId} PillId */
+
+/**
+ * @typedef {`[${ResId},${ResId},${ResId},${ResId}]`} ConnectorTaskIds
+ * In the following order: [masterTaskId, masterTaskUserId, taskId, taskUserId]
+ */
+
+/** @typedef {number | false} ResId */
+
+const ganttViewParams = {
+ resModel: "project.task",
+ arch: /* xml */ ``,
+ groupBy: ["user_ids"],
+};
+
+let nextColor = 1;
+class ProjectTask extends models.Model {
+ _name = "project.task";
+
+ name = fields.Char();
+ planned_date_begin = fields.Datetime({ string: "Start Date" });
+ date_deadline = fields.Datetime({ string: "Stop Date" });
+ user_ids = fields.Many2many({ string: "Assignees", relation: "res.users" });
+ allow_task_dependencies = fields.Boolean({ default: true });
+ depend_on_ids = fields.One2many({ string: "Depends on", relation: "project.task" });
+ display_warning_dependency_in_gantt = fields.Boolean({ default: true });
+ color = fields.Integer({ default: () => nextColor++ });
+
+ _records = [
+ {
+ id: 1,
+ name: "Task 1",
+ planned_date_begin: "2021-10-11 18:30:00",
+ date_deadline: "2021-10-11 19:29:59",
+ user_ids: [1],
+ depend_on_ids: [],
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ planned_date_begin: "2021-10-12 11:30:00",
+ date_deadline: "2021-10-12 12:29:59",
+ user_ids: [1, 3],
+ depend_on_ids: [1],
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ planned_date_begin: "2021-10-13 06:30:00",
+ date_deadline: "2021-10-13 07:29:59",
+ user_ids: [],
+ depend_on_ids: [2],
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ planned_date_begin: "2021-10-14 22:30:00",
+ date_deadline: "2021-10-14 23:29:59",
+ user_ids: [2, 3],
+ depend_on_ids: [2],
+ },
+ {
+ id: 5,
+ name: "Task 5",
+ planned_date_begin: "2021-10-15 01:53:10",
+ date_deadline: "2021-10-15 02:34:34",
+ user_ids: [],
+ depend_on_ids: [],
+ },
+ {
+ id: 6,
+ name: "Task 6",
+ planned_date_begin: "2021-10-16 23:00:00",
+ date_deadline: "2021-10-16 23:21:01",
+ user_ids: [1, 3],
+ depend_on_ids: [4, 5],
+ },
+ {
+ id: 7,
+ name: "Task 7",
+ planned_date_begin: "2021-10-17 10:30:12",
+ date_deadline: "2021-10-17 11:29:59",
+ user_ids: [1, 2, 3],
+ depend_on_ids: [6],
+ },
+ {
+ id: 8,
+ name: "Task 8",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-18 07:29:59",
+ user_ids: [1, 3],
+ depend_on_ids: [7],
+ },
+ {
+ id: 9,
+ name: "Task 9",
+ planned_date_begin: "2021-10-19 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [8],
+ },
+ {
+ id: 10,
+ name: "Task 10",
+ planned_date_begin: "2021-10-19 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [],
+ },
+ {
+ id: 11,
+ name: "Task 11",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-18 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [10],
+ },
+ {
+ id: 12,
+ name: "Task 12",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [],
+ },
+ {
+ id: 13,
+ name: "Task 13",
+ planned_date_begin: "2021-10-18 07:29:59",
+ date_deadline: "2021-10-20 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [12],
+ },
+ ];
+}
+
+class ResUsers extends models.Model {
+ _name = "res.users";
+
+ name = fields.Char();
+
+ _records = [
+ { id: 1, name: "User 1" },
+ { id: 2, name: "User 2" },
+ { id: 3, name: "User 3" },
+ { id: 4, name: "User 4" },
+ ];
+}
+
+defineModels([ProjectTask, ResUsers]);
+
+describe.current.tags("desktop");
+
+beforeEach(() => mockDate("2021-10-10T08:00:00", +1));
+
+test("Connectors are correctly computed and rendered.", async () => {
+ /**
+ * @type {Map}
+ * => Check that there is a connector between masterTaskId from group masterTaskUserId and taskId from group taskUserId with normal|error color.
+ */
+ const testMap = new Map([
+ ["[1,1,2,1]", "default"],
+ ["[1,1,2,3]", "default"],
+ ["[2,1,3,false]", "default"],
+ ["[2,3,3,false]", "default"],
+ ["[2,1,4,2]", "default"],
+ ["[2,3,4,3]", "default"],
+ ["[4,2,6,1]", "default"],
+ ["[4,3,6,3]", "default"],
+ ["[5,false,6,1]", "default"],
+ ["[5,false,6,3]", "default"],
+ ["[6,1,7,1]", "default"],
+ ["[6,1,7,2]", "default"],
+ ["[6,3,7,2]", "default"],
+ ["[6,3,7,3]", "default"],
+ ["[7,1,8,1]", "default"],
+ ["[7,2,8,1]", "default"],
+ ["[7,2,8,3]", "default"],
+ ["[7,3,8,3]", "default"],
+ ["[8,1,9,2]", "default"],
+ ["[8,3,9,2]", "default"],
+ ["[10,2,11,2]", "error"],
+ ["[12,2,13,2]", "warning"],
+ ]);
+
+ const view = await mountGanttView(ganttViewParams);
+ const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
+
+ const connectorMap = getConnectorMap(renderer);
+
+ for (const [testKey, colorCode] of testMap.entries()) {
+ const [masterTaskId, masterTaskUserId, taskId, taskUserId] = JSON.parse(testKey);
+
+ expect(connectorMap.has(testKey)).toBe(true, {
+ message: `There should be a connector between task ${masterTaskId} from group user ${masterTaskUserId} and task ${taskId} from group user ${taskUserId}.`,
+ });
+
+ const connector = connectorMap.get(testKey);
+ const connectorEl = getConnector(connector.id);
+ expect(connectorEl).not.toBe(null);
+ const connectorStroke = queryFirst(SELECTORS.connectorStroke, { root: connectorEl });
+ expect(connectorStroke).toHaveAttribute("stroke", COLORS[colorCode].color);
+ }
+
+ expect(testMap).toHaveLength(connectorMap.size);
+ expect(SELECTORS.connector).toHaveCount(testMap.size);
+});
+
+test("Connectors are correctly rendered.", async () => {
+ patchWithCleanup(GanttRenderer.prototype, {
+ shouldRenderRecordConnectors(record) {
+ return record.id !== 1;
+ },
+ });
+
+ ProjectTask._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ planned_date_begin: "2021-10-11 18:30:00",
+ date_deadline: "2021-10-11 19:29:59",
+ user_ids: [1],
+ depend_on_ids: [],
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ planned_date_begin: "2021-10-12 11:30:00",
+ date_deadline: "2021-10-12 12:29:59",
+ user_ids: [1],
+ depend_on_ids: [1],
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ planned_date_begin: "2021-10-13 06:30:00",
+ date_deadline: "2021-10-13 07:29:59",
+ user_ids: [],
+ depend_on_ids: [1, 2],
+ },
+ ];
+
+ const view = await mountGanttView(ganttViewParams);
+ const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
+ const connectorMap = getConnectorMap(renderer);
+ expect([...connectorMap.keys()]).toEqual(["[2,1,3,false]"], {
+ message: "The only rendered connector should be the one from task_id 2 to task_id 3",
+ });
+});
+
+test("Connectors are correctly computed and rendered when consolidation is active.", async () => {
+ ProjectTask._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ planned_date_begin: "2021-10-11 18:30:00",
+ date_deadline: "2021-10-11 19:29:59",
+ user_ids: [1],
+ depend_on_ids: [],
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ planned_date_begin: "2021-10-12 11:30:00",
+ date_deadline: "2021-10-12 12:29:59",
+ user_ids: [1, 3],
+ depend_on_ids: [1],
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ planned_date_begin: "2021-10-13 06:30:00",
+ date_deadline: "2021-10-13 07:29:59",
+ user_ids: [],
+ depend_on_ids: [2],
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ planned_date_begin: "2021-10-14 22:30:00",
+ date_deadline: "2021-10-14 23:29:59",
+ user_ids: [2, 3],
+ depend_on_ids: [2],
+ },
+ {
+ id: 5,
+ name: "Task 5",
+ planned_date_begin: "2021-10-15 01:53:10",
+ date_deadline: "2021-10-15 02:34:34",
+ user_ids: [],
+ depend_on_ids: [],
+ },
+ {
+ id: 6,
+ name: "Task 6",
+ planned_date_begin: "2021-10-16 23:00:00",
+ date_deadline: "2021-10-16 23:21:01",
+ user_ids: [1, 3],
+ depend_on_ids: [4, 5],
+ },
+ {
+ id: 7,
+ name: "Task 7",
+ planned_date_begin: "2021-10-17 10:30:12",
+ date_deadline: "2021-10-17 11:29:59",
+ user_ids: [1, 2, 3],
+ depend_on_ids: [6],
+ },
+ {
+ id: 8,
+ name: "Task 8",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-18 07:29:59",
+ user_ids: [1, 3],
+ depend_on_ids: [7],
+ },
+ {
+ id: 9,
+ name: "Task 9",
+ planned_date_begin: "2021-10-19 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [8],
+ },
+ {
+ id: 10,
+ name: "Task 10",
+ planned_date_begin: "2021-10-19 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [],
+ },
+ {
+ id: 11,
+ name: "Task 11",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-18 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [10],
+ },
+ {
+ id: 12,
+ name: "Task 12",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [],
+ },
+ {
+ id: 13,
+ name: "Task 13",
+ planned_date_begin: "2021-10-18 07:29:59",
+ date_deadline: "2021-10-20 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [12],
+ },
+ ];
+
+ await mountGanttView({
+ ...ganttViewParams,
+ arch: /* xml */ ``,
+ });
+
+ // groups have been created of r
+ expect(".o_gantt_row_header.o_gantt_group.o_group_open").toHaveCount(4);
+
+ function getGroupRow(index) {
+ return queryAll(".o_gantt_row_header.o_gantt_group")[index];
+ }
+
+ expect(SELECTORS.connector).toHaveCount(22);
+
+ await contains(getGroupRow(1)).click();
+ expect(getGroupRow(1)).not.toHaveClass("o_group_open");
+ expect(SELECTORS.connector).toHaveCount(13);
+
+ await contains(getGroupRow(1)).click();
+ expect(SELECTORS.connector).toHaveCount(22);
+
+ await contains(getGroupRow(1)).click();
+ expect(SELECTORS.connector).toHaveCount(13);
+
+ await contains(getGroupRow(2)).click();
+ expect(SELECTORS.connector).toHaveCount(6);
+
+ await contains(getGroupRow(0)).click();
+ expect(SELECTORS.connector).toHaveCount(4);
+
+ await contains(getGroupRow(3)).click();
+ expect(SELECTORS.connector).toHaveCount(0);
+});
+
+test("Connector hovered state is triggered and color is set accordingly.", async () => {
+ await mountGanttView(ganttViewParams);
+
+ expect(getConnector(1)).not.toHaveClass(CLASSES.highlightedConnector);
+ expect(queryFirst(SELECTORS.connectorStroke, { root: getConnector(1) })).toHaveAttribute(
+ "stroke",
+ COLORS.default.color
+ );
+
+ await hover(getConnector(1));
+ await animationFrame();
+
+ expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
+ expect(queryFirst(SELECTORS.connectorStroke, { root: getConnector(1) })).toHaveAttribute(
+ "stroke",
+ COLORS.default.highlightedColor
+ );
+});
+
+test("Buttons are displayed when hovering a connector.", async () => {
+ await mountGanttView(ganttViewParams);
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
+
+ await hover(getConnector(1));
+ await animationFrame();
+
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(3);
+});
+
+test("Buttons are displayed when hovering a connector after a pill has been hovered.", async () => {
+ await mountGanttView(ganttViewParams);
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
+
+ await hover(getPill("Task 1"));
+ await animationFrame();
+
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(0);
+ expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
+
+ await hover(getConnector(1));
+ await animationFrame();
+
+ expect(getConnector(1)).toHaveClass(CLASSES.highlightedConnector);
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: getConnector(1) })).toHaveCount(3);
+});
+
+test("Connector buttons: remove a dependency", async () => {
+ onRpc(({ method, model, args }) => {
+ if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
+ expect.step([method, args]);
+ return true;
+ }
+ });
+ await mountGanttView(ganttViewParams);
+
+ await clickConnectorButton(getConnector(1), "remove");
+ expect.verifySteps([["write", [[2], { depend_on_ids: [[3, 1, false]] }]]]);
+});
+
+test("Connector buttons: reschedule task backward date.", async () => {
+ onRpc(({ method, model, args }) => {
+ if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
+ expect.step([method, args]);
+ return {};
+ }
+ });
+ await mountGanttView(ganttViewParams);
+
+ await clickConnectorButton(getConnector(1), "reschedule-backward");
+ expect.verifySteps([
+ [
+ "web_gantt_reschedule",
+ ["backward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
+ ],
+ ]);
+});
+
+test("Connector buttons: reschedule task forward date.", async () => {
+ onRpc(({ args, method, model }) => {
+ if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
+ expect.step([method, args]);
+ return {};
+ }
+ });
+ await mountGanttView(ganttViewParams);
+
+ await clickConnectorButton(getConnector(1), "reschedule-forward");
+ expect.verifySteps([
+ [
+ "web_gantt_reschedule",
+ ["forward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
+ ],
+ ]);
+});
+
+test("Connector buttons: reschedule task start backward, different data.", async () => {
+ onRpc(({ method, model, args }) => {
+ if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
+ expect.step([method, args]);
+ return {};
+ }
+ });
+ await mountGanttView(ganttViewParams);
+
+ await clickConnectorButton(getConnector(1), "reschedule-backward");
+ expect.verifySteps([
+ [
+ "web_gantt_reschedule",
+ ["backward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
+ ],
+ ]);
+});
+
+test("Connector buttons: reschedule task forward, different data.", async () => {
+ onRpc(({ method, model, args }) => {
+ if (model === "project.task" && ["web_gantt_reschedule", "write"].includes(method)) {
+ expect.step([method, args]);
+ return {};
+ }
+ });
+ await mountGanttView(ganttViewParams);
+
+ await clickConnectorButton(getConnector(1), "reschedule-forward");
+ expect.verifySteps([
+ [
+ "web_gantt_reschedule",
+ ["forward", 1, 2, "depend_on_ids", null, "planned_date_begin", "date_deadline"],
+ ],
+ ]);
+});
+
+test("Hovering a task pill should highlight related tasks and dependencies", async () => {
+ /** @type {Map} */
+ const testMap = new Map([
+ ["[1,1,2,1]", true],
+ ["[1,1,2,3]", true],
+ ["[2,1,3,false]", true],
+ ["[2,3,3,false]", true],
+ ["[2,1,4,2]", true],
+ ["[2,3,4,3]", true],
+ ["[10,2,11,2]", false],
+ ]);
+
+ ProjectTask._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ planned_date_begin: "2021-10-10 18:30:00",
+ date_deadline: "2021-10-11 19:29:59",
+ user_ids: [1],
+ depend_on_ids: [],
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ planned_date_begin: "2021-10-12 11:30:00",
+ date_deadline: "2021-10-12 12:29:59",
+ user_ids: [1, 3],
+ depend_on_ids: [1],
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ planned_date_begin: "2021-10-13 06:30:00",
+ date_deadline: "2021-10-13 07:29:59",
+ user_ids: [],
+ depend_on_ids: [2],
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ planned_date_begin: "2021-10-14 22:30:00",
+ date_deadline: "2021-10-14 23:29:59",
+ user_ids: [2, 3],
+ depend_on_ids: [2],
+ },
+ {
+ id: 10,
+ name: "Task 10",
+ planned_date_begin: "2021-10-19 06:30:12",
+ date_deadline: "2021-10-19 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [],
+ display_warning_dependency_in_gantt: false,
+ },
+ {
+ id: 11,
+ name: "Task 11",
+ planned_date_begin: "2021-10-18 06:30:12",
+ date_deadline: "2021-10-18 07:29:59",
+ user_ids: [2],
+ depend_on_ids: [10],
+ },
+ ];
+
+ const view = await mountGanttView(ganttViewParams);
+ const renderer = findComponent(view, (c) => c instanceof GanttRenderer);
+
+ const connectorMap = getConnectorMap(renderer);
+ const pills = [];
+ for (const wrapper of queryAll(SELECTORS.pillWrapper)) {
+ const pillId = wrapper.dataset.pillId;
+ pills.push({
+ el: queryFirst(SELECTORS.pill, { root: wrapper }),
+ recordId: renderer.pills[pillId].record.id,
+ });
+ }
+
+ const task2Pills = pills.filter((p) => p.recordId === 2);
+
+ expect(task2Pills).toHaveLength(2);
+ expect(CLASSES.highlightedPill).toHaveCount(0);
+
+ // Check that all connectors are not in hover state.
+ for (const testKey of testMap.keys()) {
+ expect(getConnector(connectorMap.get(testKey).id)).not.toHaveClass(
+ CLASSES.highlightedConnector
+ );
+ }
+
+ await contains(getPill("Task 2", { nth: 1 })).hover();
+ // Both pills should be highlighted
+ expect(getPillWrapper("Task 2", { nth: 1 })).toHaveClass(CLASSES.highlightedPill);
+ expect(getPillWrapper("Task 2", { nth: 2 })).toHaveClass(CLASSES.highlightedPill);
+
+ // The rest of the pills should not be highlighted nor display connector creators
+ for (const { el, recordId } of pills) {
+ if (recordId !== 2) {
+ expect(el).not.toHaveClass(CLASSES.highlightedPill);
+ }
+ }
+
+ // Check that all connectors are in the expected hover state.
+ for (const [testKey, shouldBeHighlighted] of testMap.entries()) {
+ const connector = getConnector(connectorMap.get(testKey).id);
+ if (shouldBeHighlighted) {
+ expect(connector).toHaveClass(CLASSES.highlightedConnector);
+ } else {
+ expect(connector).not.toHaveClass(CLASSES.highlightedConnector);
+ }
+ expect(queryAll(SELECTORS.connectorStrokeButton, { root: connector })).toHaveCount(0);
+ }
+});
+
+test("Hovering a connector should cause the connected pills to get highlighted.", async () => {
+ await mountGanttView(ganttViewParams);
+ expect(SELECTORS.highlightedConnector).toHaveCount(0);
+ expect(SELECTORS.highlightedPill).toHaveCount(0);
+
+ await contains(getConnector(1)).hover();
+ expect(SELECTORS.highlightedConnector).toHaveCount(1);
+ expect(SELECTORS.highlightedPill).toHaveCount(2);
+});
+
+test("Connectors are displayed behind pills, except on hover.", async () => {
+ const getZIndex = (el) => Number(getComputedStyle(el).zIndex) || 0;
+ await mountGanttView(ganttViewParams);
+ expect(getZIndex(getPillWrapper("Task 2"))).toBeGreaterThan(getZIndex(getConnector(1)));
+
+ await contains(getConnector(1)).hover();
+ expect(getZIndex(getPillWrapper("Task 2"))).toBeLessThan(getZIndex(getConnector(1)));
+});
+
+test("Create a connector from the gantt view.", async () => {
+ onRpc("write", ({ args, method }) => expect.step([method, args]));
+ await mountGanttView(ganttViewParams);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
+ rightWrapper.classList.add("d-block");
+
+ await contains(
+ `${SELECTORS.connectorCreatorWrapper} ${SELECTORS.connectorCreatorBullet}:first`
+ ).dragAndDrop(getPill("Task 2"));
+ expect.verifySteps([["write", [[2], { depend_on_ids: [[4, 3, false]] }]]]);
+});
+
+test("Create a connector from the gantt view: going fast", async () => {
+ await mountGanttView({
+ ...ganttViewParams,
+ domain: [["id", "in", [1, 3]]],
+ });
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper, {
+ root: getPillWrapper("Task 1"),
+ });
+ rightWrapper.classList.add("d-block");
+
+ const connectorBullet = queryFirst(SELECTORS.connectorCreatorBullet, { root: rightWrapper });
+ const bulletRect = queryRect(connectorBullet);
+ const initialPosition = {
+ x: Math.floor(bulletRect.left + bulletRect.width / 2), // floor to avoid sub-pixel positioning
+ y: Math.floor(bulletRect.top + bulletRect.height / 2), // floor to avoid sub-pixel positioning
+ };
+ await pointerDown(connectorBullet, {
+ position: { clientX: initialPosition.x, clientY: initialPosition.y },
+ });
+
+ // Here we simulate a fast move, using arbitrary values.
+ const currentPosition = {
+ x: Math.floor(initialPosition.x + 123), // floor to avoid sub-pixel positioning
+ y: Math.floor(initialPosition.y + 12), // floor to avoid sub-pixel positioning
+ };
+ await hover(SELECTORS.cellContainer, {
+ position: { clientX: currentPosition.x, clientY: currentPosition.y },
+ });
+ await animationFrame();
+
+ // Then we check that the connector stroke is correctly positioned.
+ const connectorStroke = queryOne(SELECTORS.connectorStroke, { root: getConnector("new") });
+ expect(connectorStroke).toHaveRect({
+ top: initialPosition.y,
+ right: currentPosition.x,
+ bottom: currentPosition.y,
+ left: initialPosition.x,
+ });
+});
+
+test("Connectors should be rendered if connected pill is not visible", async () => {
+ // We first need to bump all the ids for users 2, 3 and 4 to make them disappear.
+ for (const record of ResUsers._records.slice(1)) {
+ record.id += 1000;
+ }
+ for (const record of ProjectTask._records) {
+ record.user_ids = record.user_ids.map((id) => (id > 1 ? id + 1000 : id));
+ }
+ // Generate a lot of users so that the connectors are far beyond the visible
+ // viewport, hence generating fake extra pills to render the connectors.
+ for (let i = 0; i < 100; i++) {
+ const id = 100 + i;
+ ResUsers._records.push({ id, name: `User ${id}` });
+ ProjectTask._records.push({
+ id,
+ name: `Task ${id}`,
+ planned_date_begin: "2021-10-11 18:30:00",
+ date_deadline: "2021-10-11 19:29:59",
+ user_ids: [id],
+ depend_on_ids: [],
+ });
+ }
+ ProjectTask._records[12].user_ids = [199];
+
+ await mountGanttView(ganttViewParams);
+ expect(queryAll(SELECTORS.connector, { visible: true })).toHaveCount(13);
+});
+
+test("No display of resize handles when creating a connector", async () => {
+ await mountGanttView(ganttViewParams);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
+ rightWrapper.classList.add("d-block");
+
+ // Creating a connector and hover another pill while dragging it
+ const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
+ root: rightWrapper,
+ }).drag();
+ await moveTo(getPill("Task 2"));
+
+ expect(SELECTORS.resizeHandle).toHaveCount(0);
+
+ await cancel();
+});
+
+test("Renderer in connect mode when creating a connector", async () => {
+ await mountGanttView(ganttViewParams);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
+ rightWrapper.classList.add("d-block");
+
+ // Creating a connector and hover another pill while dragging it
+ const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
+ root: rightWrapper,
+ }).drag();
+ await moveTo(getPill("Task 2"));
+
+ expect(SELECTORS.renderer).toHaveClass("o_connect");
+
+ await cancel();
+});
+
+test("Connector creators of initial pill are highlighted when creating a connector", async () => {
+ await mountGanttView(ganttViewParams);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst`${SELECTORS.pillWrapper} ${SELECTORS.connectorCreatorWrapper}`;
+ rightWrapper.classList.add("d-block");
+
+ // Creating a connector and hover another pill while dragging it
+ const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
+ root: rightWrapper,
+ }).drag();
+ await moveTo(getPill("Task 2"));
+
+ expect(`${SELECTORS.pillWrapper}:first`).toHaveClass(CLASSES.lockedConnectorCreator);
+
+ await cancel();
+});
+
+test("Connector creators of hovered pill are highlighted when creating a connector", async () => {
+ await mountGanttView(ganttViewParams);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
+ rightWrapper.classList.add("d-block");
+
+ // Creating a connector and hover another pill while dragging it
+ const { cancel, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
+ root: rightWrapper,
+ }).drag();
+
+ const destinationWrapper = getPillWrapper("Task 2");
+ const destinationPill = queryFirst(SELECTORS.pill, { root: destinationWrapper });
+ await moveTo(destinationPill);
+
+ // moveTo only triggers a pointerenter event on destination pill,
+ // a pointermove event is still needed to highlight it
+ await contains(destinationPill).hover();
+ expect(destinationWrapper).toHaveClass(CLASSES.highlightedConnectorCreator);
+
+ await cancel();
+});
+
+test("Switch to full-size browser: the connections between pills should be diplayed", async () => {
+ await resize({ width: 375, height: 667 });
+
+ await mountGanttView(ganttViewParams);
+
+ // Mobile view
+ expect("svg.o_gantt_connector").toHaveCount(0, {
+ message: "Gantt connectors should not be visible in small/mobile view",
+ });
+
+ // Resizing browser to leave mobile view
+ await resize({ width: 1366, height: 768 });
+ await runAllTimers();
+
+ expect("svg.o_gantt_connector").toHaveCount(22, {
+ message: "Gantt connectors should be visible when switching to desktop view",
+ });
+});
+
+test("Connect two very distant pills", async () => {
+ ProjectTask._records = [
+ ProjectTask._records[0],
+ {
+ id: 2,
+ name: "Task 2",
+ planned_date_begin: "2021-11-18 08:00:00",
+ date_deadline: "2021-11-18 16:00:00",
+ user_ids: [2],
+ depend_on_ids: [],
+ },
+ ];
+ onRpc("write", ({ args }) => {
+ expect.step(JSON.stringify(args));
+ });
+ await mountGanttView({
+ ...ganttViewParams,
+ context: {
+ default_start_date: "2021-10-01",
+ default_stop_date: "2021-11-30",
+ },
+ });
+ expect(SELECTORS.connector).toHaveCount(0);
+
+ // Explicitly shows the connector creator wrapper since its "display: none"
+ // disappears on native CSS hover, which cannot be programatically emulated.
+ const rightWrapper = queryFirst(SELECTORS.connectorCreatorWrapper);
+ rightWrapper.classList.add("d-block");
+
+ // Creating a connector and hover another pill while dragging it
+ const { drop, moveTo } = await contains(SELECTORS.connectorCreatorBullet, {
+ root: rightWrapper,
+ }).drag();
+
+ const selector = `${SELECTORS.pill}:contains('Task 2')`;
+ expect(selector).toHaveCount(0);
+ await moveTo(SELECTORS.pill, { relative: true, position: { x: 1500 } });
+ await advanceFrame(200);
+ await drop(selector);
+ expect.verifySteps([`[[2],{"depend_on_ids":[[4,1,false]]}]`]);
+ expect(SELECTORS.connector).toHaveCount(1);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_dependency_helpers.js b/addons_extensions/web_gantt/static/tests/gantt_dependency_helpers.js
new file mode 100644
index 000000000..e835770c3
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_dependency_helpers.js
@@ -0,0 +1,83 @@
+import { hover, queryFirst } from "@odoo/hoot-dom";
+import { runAllTimers } from "@odoo/hoot-mock";
+import { contains } from "@web/../tests/web_test_helpers";
+import { SELECTORS } from "./web_gantt_test_helpers";
+
+/**
+ * @param {import("@odoo/hoot-dom").Target} target
+ * @param {"remove" | "reschedule-forward" | "reschedule-backward"} button
+ */
+export async function clickConnectorButton(target, button) {
+ await hover(target);
+ await runAllTimers();
+ let element = null;
+ switch (button) {
+ case "remove": {
+ element = queryFirst(SELECTORS.connectorRemoveButton, { root: target });
+ break;
+ }
+ case "reschedule-backward": {
+ element = queryFirst(`${SELECTORS.connectorRescheduleButton}:first-of-type`, {
+ root: target,
+ });
+ break;
+ }
+ case "reschedule-forward": {
+ element = queryFirst(`${SELECTORS.connectorRescheduleButton}:last-of-type`, {
+ root: target,
+ });
+ break;
+ }
+ }
+ return contains(element).click();
+}
+
+/**
+ * @param {number | "new"} id
+ */
+export function getConnector(id) {
+ if (!/^__connector__/.test(id)) {
+ id = `__connector__${id}`;
+ }
+ return queryFirst(
+ `${SELECTORS.cellContainer} ${SELECTORS.connector}[data-connector-id='${id}']`
+ );
+}
+
+export function getConnectorMap(renderer) {
+ /**
+ * @param {PillId} pillId
+ */
+ const getIdAndUserIdFromPill = (pillId) => {
+ /** @type {[ResId, ResId]} */
+ const result = [renderer.pills[pillId]?.record.id || false, false];
+ if (result[0]) {
+ const pills = renderer.mappingRecordToPillsByRow[result[0]]?.pills;
+ if (pills) {
+ const pillEntry = Object.entries(pills).find((e) => e[1].id === pillId);
+ if (pillEntry) {
+ const [firstGroup] = JSON.parse(pillEntry[0]);
+ if (firstGroup.user_ids?.length) {
+ result[1] = firstGroup.user_ids[0] || false;
+ }
+ }
+ }
+ }
+ return result;
+ };
+
+ /** @type {Map} */
+ const connectorMap = new Map();
+ for (const connector of Object.values(renderer.connectors)) {
+ const { sourcePillId, targetPillId } = renderer.mappingConnectorToPills[connector.id];
+ if (!sourcePillId || !targetPillId) {
+ continue;
+ }
+ const key = JSON.stringify([
+ ...getIdAndUserIdFromPill(sourcePillId),
+ ...getIdAndUserIdFromPill(targetPillId),
+ ]);
+ connectorMap.set(key, connector);
+ }
+ return connectorMap;
+}
diff --git a/addons_extensions/web_gantt/static/tests/gantt_mock_models.js b/addons_extensions/web_gantt/static/tests/gantt_mock_models.js
new file mode 100644
index 000000000..302bf4d9c
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_mock_models.js
@@ -0,0 +1,182 @@
+import { defineModels, fields, models } from "@web/../tests/web_test_helpers";
+
+export const TASKS_STAGE_SELECTION = [
+ ["todo", "To Do"],
+ ["in_progress", "In Progress"],
+ ["done", "Done"],
+ ["cancel", "Cancelled"],
+];
+
+export class Project extends models.Model {
+ name = fields.Char();
+
+ _records = [
+ { id: 1, name: "Project 1" },
+ { id: 2, name: "Project 2" },
+ ];
+}
+
+export class ResUsers extends models.Model {
+ _name = "res.users";
+
+ name = fields.Char();
+
+ has_group() {
+ return true;
+ }
+
+ _records = [
+ { id: 1, name: "User 1" },
+ { id: 2, name: "User 2" },
+ ];
+}
+
+export class Stage extends models.Model {
+ name = fields.Char();
+ sequence = fields.Integer();
+
+ _records = [
+ {
+ id: 1,
+ name: "in_progress",
+ sequence: 2,
+ },
+ {
+ id: 2,
+ name: "todo",
+ sequence: 1,
+ },
+ {
+ id: 3,
+ name: "cancel",
+ sequence: 4,
+ },
+ {
+ id: 4,
+ name: "done",
+ sequence: 3,
+ },
+ ];
+}
+
+export class Tasks extends models.Model {
+ name = fields.Char();
+ start = fields.Datetime({ string: "Start Date" });
+ stop = fields.Datetime({ string: "Stop Date" });
+ allocated_hours = fields.Float({ string: "Allocated Hours" });
+ stage = fields.Selection({
+ selection: TASKS_STAGE_SELECTION,
+ });
+ color = fields.Integer();
+ progress = fields.Integer();
+ exclude = fields.Boolean({ string: "Excluded from Consolidation" });
+ project_id = fields.Many2one({ relation: "project" });
+ stage_id = fields.Many2one({ relation: "stage" });
+ user_id = fields.Many2one({ string: "Assign To", relation: "res.users" });
+
+ _records = [
+ {
+ id: 1,
+ name: "Task 1",
+ start: "2018-11-30 18:30:00",
+ stop: "2018-12-31 18:29:59",
+ stage: "todo",
+ stage_id: 1,
+ project_id: 1,
+ user_id: 1,
+ color: 0,
+ progress: 0,
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ start: "2018-12-17 11:30:00",
+ stop: "2018-12-22 06:29:59",
+ stage: "done",
+ stage_id: 4,
+ project_id: 1,
+ user_id: 2,
+ color: 2,
+ progress: 30,
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ start: "2018-12-27 06:30:00",
+ stop: "2019-01-03 06:29:59",
+ stage: "cancel",
+ stage_id: 3,
+ project_id: 1,
+ user_id: 2,
+ color: 10,
+ progress: 60,
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ start: "2018-12-20 02:30:00",
+ stop: "2018-12-20 06:29:59",
+ stage: "in_progress",
+ stage_id: 3,
+ project_id: 1,
+ user_id: 1,
+ color: 1,
+ exclude: false,
+ },
+ {
+ id: 5,
+ name: "Task 5",
+ start: "2018-11-08 01:53:10",
+ stop: "2018-12-04 01:34:34",
+ stage: "done",
+ stage_id: 2,
+ project_id: 2,
+ user_id: 1,
+ color: 2,
+ progress: 100,
+ exclude: true,
+ },
+ {
+ id: 6,
+ name: "Task 6",
+ start: "2018-11-19 23:00:00",
+ stop: "2018-11-20 04:21:01",
+ stage: "in_progress",
+ stage_id: 4,
+ project_id: 2,
+ user_id: 1,
+ color: 1,
+ },
+ {
+ id: 7,
+ name: "Task 7",
+ start: "2018-12-20 12:30:12",
+ stop: "2018-12-20 18:29:59",
+ stage: "cancel",
+ stage_id: 1,
+ project_id: 2,
+ user_id: 2,
+ color: 10,
+ progress: 80,
+ },
+ {
+ id: 8,
+ name: "Task 8",
+ start: "2020-03-28 06:30:12",
+ stop: "2020-03-28 18:29:59",
+ stage: "in_progress",
+ stage_id: 1,
+ project_id: 2,
+ user_id: 2,
+ color: 10,
+ progress: 80,
+ },
+ ];
+ _views = {
+ search: /* xml */ ``,
+ };
+}
+
+export function defineGanttModels() {
+ defineModels([Stage, Project, ResUsers, Tasks]);
+}
diff --git a/addons_extensions/web_gantt/static/tests/gantt_mock_server.js b/addons_extensions/web_gantt/static/tests/gantt_mock_server.js
new file mode 100644
index 000000000..326775118
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_mock_server.js
@@ -0,0 +1,37 @@
+import { makeKwArgs } from "@web/../tests/web_test_helpers";
+import { registry } from "@web/core/registry";
+
+function _mockGetGanttData({ kwargs, model }) {
+ kwargs = makeKwArgs(kwargs);
+ const lazy = !kwargs.limit && !kwargs.offset && kwargs.groupby.length === 1;
+ const { groups, length } = this.env[model].web_read_group({
+ ...kwargs,
+ lazy,
+ fields: ["__record_ids:array_agg(id)"],
+ });
+
+ const recordIds = [];
+ for (const group of groups) {
+ recordIds.push(...(group.__record_ids || []));
+ }
+
+ const { records } = this.env[model].web_search_read(
+ [["id", "in", recordIds]],
+ kwargs.read_specification,
+ makeKwArgs({ context: kwargs.context })
+ );
+
+ const unavailabilities = {};
+ for (const fieldName of kwargs.unavailability_fields || []) {
+ unavailabilities[fieldName] = {};
+ }
+
+ const progress_bars = {};
+ for (const fieldName of kwargs.progress_bar_fields || []) {
+ progress_bars[fieldName] = {};
+ }
+
+ return { groups, length, records, unavailabilities, progress_bars };
+}
+
+registry.category("mock_rpc").add("get_gantt_data", _mockGetGanttData);
diff --git a/addons_extensions/web_gantt/static/tests/gantt_sparse.test.js b/addons_extensions/web_gantt/static/tests/gantt_sparse.test.js
new file mode 100644
index 000000000..90dff616f
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_sparse.test.js
@@ -0,0 +1,527 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { mockDate } from "@odoo/hoot-mock";
+import { onRpc } from "@web/../tests/web_test_helpers";
+import { defineGanttModels } from "./gantt_mock_models";
+import {
+ SELECTORS,
+ getCellColorProperties,
+ getGridContent,
+ mountGanttView,
+} from "./web_gantt_test_helpers";
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => mockDate("2018-12-20T08:00:00", +1));
+
+test("empty sparse gantt", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 0]],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([{ title: "" }]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("sparse gantt", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 1]],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Task 1",
+ },
+ ]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("sparse grouped gantt", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["stage"],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ title: "1",
+ },
+ ],
+ title: "To Do",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Task 1",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "In Progress",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ ],
+ title: "Task 4",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "Done",
+ },
+ {
+ title: "Task 5",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Task 2",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "1",
+ },
+ ],
+ title: "Cancelled",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "Task 7",
+ },
+ ],
+ title: "Task 7",
+ },
+ {
+ pills: [
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ title: "Task 3",
+ },
+ ]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("sparse gantt with consolidation", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["stage"],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ title: "1",
+ },
+ ],
+ title: "To Do",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Task 1",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "In Progress",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ ],
+ title: "Task 4",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "Done",
+ },
+ {
+ title: "Task 5",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Task 2",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "1",
+ },
+ ],
+ title: "Cancelled",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "Task 7",
+ },
+ ],
+ title: "Task 7",
+ },
+ {
+ pills: [
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ title: "Task 3",
+ },
+ ]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("sparse gantt with a group expand", async () => {
+ onRpc("get_gantt_data", () => {
+ return {
+ groups: [
+ {
+ stage: "todo",
+ __record_ids: [],
+ },
+ {
+ stage: "in_progress",
+ __record_ids: [4],
+ },
+ ],
+ length: 2,
+ records: [
+ {
+ display_name: "Task 4",
+ id: 4,
+ progress: 0,
+ stage: "in_progress",
+ start: "2018-12-20 02:30:00",
+ stop: "2018-12-20 06:29:59",
+ },
+ ],
+ };
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["stage"],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ title: "To Do",
+ },
+ {
+ title: "",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "In Progress",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ ],
+ title: "Task 4",
+ },
+ ]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("empty sparse gantt with unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-19 23:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", async ({ parent, kwargs }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.unavailability_fields).toEqual([]);
+ result.unavailabilities.__default = { false: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 0]],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ // Full unavailability
+ expect(getCellColorProperties("19 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+});
+
+test("sparse gantt with unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-19 23:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", async ({ parent, kwargs }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.unavailability_fields).toEqual([]);
+ result.unavailabilities.__default = { false: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 1]],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ // Full unavailability
+ expect(getCellColorProperties("19 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+});
+
+test("sparse grouped gantt with unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-19 23:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", async ({ parent, kwargs }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.unavailability_fields).toEqual(["user_id"]);
+ result.unavailabilities.user_id = { 1: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id"],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ // Full unavailability
+ expect(getCellColorProperties("19 December 2018", "Task 5")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+});
+
+test("sparse gantt with consolidation with unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-19 23:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", async ({ parent, kwargs }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.unavailability_fields).toEqual(["user_id"]);
+ result.unavailabilities.user_id = { 1: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["user_id"],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ // Full unavailability
+ expect(getCellColorProperties("19 December 2018", "", { num: 2 })).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+});
+
+test("sparse gantt with a group expand and unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-19 23:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step("get_gantt_data");
+ expect(kwargs.unavailability_fields).toEqual(["user_id"]);
+ return {
+ groups: [
+ {
+ user_id: [1, "Charles"],
+ __record_ids: [],
+ },
+ {
+ user_id: [2, "Louis"],
+ __record_ids: [4],
+ },
+ ],
+ length: 2,
+ records: [
+ {
+ display_name: "Task 4",
+ id: 4,
+ progress: 0,
+ user_id: 1,
+ start: "2018-12-20 02:30:00",
+ stop: "2018-12-20 06:29:59",
+ },
+ ],
+ unavailabilities: {
+ user_id: { 1: unavailabilities, 2: [] },
+ },
+ };
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["user_id"],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(getCellColorProperties("19 December 2018", "", { num: 2 })).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_attributes.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_attributes.test.js
new file mode 100644
index 000000000..25322cbdd
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_attributes.test.js
@@ -0,0 +1,986 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { click, leave, queryAll, queryOne, queryFirst } from "@odoo/hoot-dom";
+import { animationFrame, mockDate } from "@odoo/hoot-mock";
+import { contains, defineParams, onRpc } from "@web/../tests/web_test_helpers";
+import { Tasks, defineGanttModels } from "./gantt_mock_models";
+import {
+ SELECTORS,
+ clickCell,
+ getActiveScale,
+ getCell,
+ getCellColorProperties,
+ getGridContent,
+ getPill,
+ getPillWrapper,
+ hoverGridCell,
+ mountGanttView,
+ resizePill,
+} from "./web_gantt_test_helpers";
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => {
+ mockDate("2018-12-20T07:00:00", +1);
+ defineParams({
+ lang_parameters: {
+ time_format: "%I:%M:%S",
+ },
+ });
+});
+
+test("create attribute", async () => {
+ Tasks._views.list = `
`;
+ Tasks._views.search = ``;
+ onRpc("has_group", () => true);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(".o_dialog").toHaveCount(0);
+ await hoverGridCell("06 December 2018");
+ await clickCell("06 December 2018");
+ expect(".o_dialog").toHaveCount(1);
+ expect(".modal-title").toHaveText("Plan");
+ expect(".o_create_button").toHaveCount(0);
+});
+
+test("plan attribute", async () => {
+ Tasks._views.form = ``;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(".o_dialog").toHaveCount(0);
+ await hoverGridCell("06 December 2018");
+ await clickCell("06 December 2018");
+ expect(".o_dialog").toHaveCount(1);
+ expect(".modal-title").toHaveText("Create");
+});
+
+test("edit attribute", async () => {
+ Tasks._views.form = ``;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(SELECTORS.resizable).toHaveCount(0);
+ expect(SELECTORS.draggable).toHaveCount(0);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 5",
+ level: 0,
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ },
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+
+ await contains(getPill("Task 1")).click();
+ expect(`.o_popover button.btn-primary`).toHaveText(/view/i);
+ await contains(`.o_popover button.btn-primary`).click();
+ expect(".modal .o_form_readonly").toHaveCount(1);
+});
+
+test("total_row attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 5",
+ level: 0,
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ },
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ isTotalRow: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ level: 0,
+ title: "2",
+ },
+ {
+ colSpan: "04 (1/2) December 2018 -> 17 (1/2) December 2018",
+ level: 0,
+ title: "1",
+ },
+ {
+ colSpan: "17 (1/2) December 2018 -> 19 December 2018",
+ level: 0,
+ title: "2",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "3",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "3",
+ },
+ {
+ colSpan: "21 December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "2",
+ },
+ {
+ colSpan: "22 (1/2) December 2018 -> 26 December 2018",
+ level: 0,
+ title: "1",
+ },
+ {
+ colSpan: "27 December 2018 -> 31 December 2018",
+ level: 0,
+ title: "2",
+ },
+ {
+ colSpan: "01 January 2019 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "1",
+ },
+ ],
+ },
+ ]);
+});
+
+test("default_scale attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getActiveScale()).toBe(5); // day scale
+ const { columnHeaders, range } = getGridContent();
+ expect(range).toBe("From: 12/20/2018 to: 12/22/2018");
+ expect(columnHeaders).toHaveLength(38);
+});
+
+test("scales attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(queryOne(".o_gantt_renderer_controls input").max).toBe("1", {
+ message: "there are only 2 valid scales (starting from 0)",
+ });
+ expect(getActiveScale()).toBe(1);
+});
+
+test("precision attribute", async () => {
+ onRpc("write", ({ args }) => expect.step(args));
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ domain: [["id", "=", 7]],
+ });
+
+ // resize of a quarter
+ const drop = await resizePill(getPillWrapper("Task 7"), "end", 0.25, false);
+ await animationFrame();
+ expect(SELECTORS.resizeBadge).toHaveText("+15 minutes");
+
+ // manually trigger the drop to trigger a write
+ await drop();
+ await animationFrame();
+ expect(SELECTORS.resizeBadge).toHaveCount(0);
+ expect.verifySteps([[[7], { stop: "2018-12-20 18:44:59" }]]);
+});
+
+test("progress attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ expect(`${SELECTORS.pill} .o_gantt_progress`).toHaveCount(3);
+ expect(
+ queryAll(SELECTORS.pill).map((el) => ({
+ text: el.innerText,
+ progress: el.querySelector(".o_gantt_progress")?.style?.width || null,
+ }))
+ ).toEqual([
+ { text: "Task 1", progress: null },
+ { text: "Task 2", progress: "30%" },
+ { text: "Task 4", progress: null },
+ { text: "Task 3", progress: "60%" },
+ { text: "Task 7", progress: "80%" },
+ ]);
+});
+
+test("form_view_id attribute", async () => {
+ Tasks._views[["form", 42]] = ``;
+ onRpc("get_views", ({ kwargs }) => expect.step(["get_views", kwargs.views]));
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ await contains(queryFirst(SELECTORS.addButton + ":visible")).click();
+ expect(".modal .o_form_view").toHaveCount(1);
+ expect.verifySteps([
+ [
+ "get_views",
+ [
+ [123456789, "gantt"],
+ [987654321, "search"],
+ ],
+ ], // initial get_views
+ ["get_views", [[42, "form"]]], // get_views when form view dialog opens
+ ]);
+});
+
+test("decoration attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+ '
+ `,
+ });
+ expect(getPill("Task 1")).toHaveClass("decoration-info");
+ expect(getPill("Task 2")).not.toHaveClass("decoration-info");
+});
+
+test("decoration attribute with date", async () => {
+ mockDate("2018-12-19T12:00:00");
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getPill("Task 1")).toHaveClass("decoration-danger");
+ expect(getPill("Task 2")).toHaveClass("decoration-danger");
+ expect(getPill("Task 5")).toHaveClass("decoration-danger");
+ expect(getPill("Task 3")).not.toHaveClass("decoration-danger");
+ expect(getPill("Task 4")).not.toHaveClass("decoration-danger");
+ expect(getPill("Task 7")).not.toHaveClass("decoration-danger");
+});
+
+test("consolidation feature", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+
+ const { rows } = getGridContent();
+ expect(rows).toHaveLength(18);
+ expect(rows.filter((r) => r.isGroup)).toHaveLength(12);
+ expect(".o_gantt_row_headers").toHaveCount(1);
+
+ // Check grouped rows
+ expect(rows[0].isGroup).toBe(true);
+ expect(rows[0].title).toBe("User 1");
+ expect(rows[9].isGroup).toBe(true);
+ expect(rows[9].title).toBe("User 2");
+
+ // Consolidation
+ // 0 over the size of Task 5 (Task 5 is 100 but is excluded!) then 0 over the rest of Task 1, cut by Task 4 which has progress 0
+ expect(rows[0].pills).toEqual([
+ { colSpan: "Out of bounds (8) -> 19 December 2018", title: "0" },
+ { colSpan: "20 December 2018 -> 20 (1/2) December 2018", title: "0" },
+ { colSpan: "20 (1/2) December 2018 -> 31 December 2018", title: "0" },
+ ]);
+
+ // 30 over Task 2 until Task 7 then 110 (Task 2 (30) + Task 7 (80)) then 30 again until end of task 2 then 60 over Task 3
+ expect(rows[9].pills).toEqual([
+ { colSpan: "17 (1/2) December 2018 -> 20 (1/2) December 2018", title: "30" },
+ { colSpan: "20 (1/2) December 2018 -> 20 December 2018", title: "110" },
+ { colSpan: "21 December 2018 -> 22 (1/2) December 2018", title: "30" },
+ { colSpan: "27 December 2018 -> 03 (1/2) January 2019", title: "60" },
+ ]);
+
+ const withStatus = [];
+ for (const el of queryAll(".o_gantt_consolidated_pill")) {
+ if (el.classList.contains("bg-success") || el.classList.contains("bg-danger")) {
+ withStatus.push({
+ title: el.title,
+ danger: el.classList.contains("border-danger"),
+ });
+ }
+ }
+
+ expect(withStatus).toEqual([
+ { title: "0", danger: false },
+ { title: "0", danger: false },
+ { title: "0", danger: false },
+ { title: "30", danger: false },
+ { title: "110", danger: true },
+ { title: "30", danger: false },
+ { title: "60", danger: false },
+ ]);
+});
+
+test("consolidation feature (single level)", async () => {
+ Tasks._views.form = `
+
+ `;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id"],
+ });
+
+ const { rows, range } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(".o_gantt_button_expand_rows").toHaveCount(1);
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (8) -> 19 December 2018",
+ title: "0",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "0",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 31 December 2018",
+ title: "0",
+ },
+ ],
+ title: "User 1",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 1,
+ title: "Task 1",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ ],
+ title: "",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 20 (1/2) December 2018",
+ title: "30",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "110",
+ },
+ {
+ colSpan: "21 December 2018 -> 22 (1/2) December 2018",
+ title: "30",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "60",
+ },
+ ],
+ title: "User 2",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 1,
+ title: "Task 7",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ title: "",
+ },
+ ]);
+});
+
+test("color attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getPill("Task 1")).toHaveClass("o_gantt_color_0");
+ expect(getPill("Task 2")).toHaveClass("o_gantt_color_2");
+});
+
+test("color attribute in multi-level grouped", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id", "project_id"],
+ domain: [["id", "=", 1]],
+ });
+ expect(`${SELECTORS.pill}.o_gantt_consolidated_pill`).not.toHaveClass("o_gantt_color_0");
+ expect(`${SELECTORS.pill}:not(.o_gantt_consolidated_pill)`).toHaveClass("o_gantt_color_0");
+});
+
+test("color attribute on a many2one", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getPill("Task 1")).toHaveClass("o_gantt_color_1");
+ expect(`${SELECTORS.pill}.o_gantt_color_1`).toHaveCount(4);
+ expect(`${SELECTORS.pill}.o_gantt_color_2`).toHaveCount(2);
+});
+
+test(`Today style with unavailabilities ("week": "day:half")`, async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 10:00:00",
+ stop: "2018-12-20 14:00:00",
+ },
+ ];
+
+ onRpc("get_gantt_data", async ({ parent }) => {
+ const result = await parent();
+ result.unavailabilities.__default = { false: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ // Normal day / unavailability
+ expect(getCellColorProperties("18 W51 2018")).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ ]);
+
+ // Full unavailability
+ expect(getCellColorProperties("19 W51 2018")).toEqual(["--Gantt__DayOff-background-color"]);
+
+ // Unavailability / today
+ expect(getCell("20 W51 2018")).toHaveClass("o_gantt_today");
+ expect(getCellColorProperties("20 W51 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOffToday-background-color",
+ ]);
+});
+
+test("Today style of group rows", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 10:00:00",
+ stop: "2018-12-20 14:00:00",
+ },
+ ];
+ Tasks._records = [Tasks._records[3]]; // id: 4
+
+ onRpc("get_gantt_data", async ({ parent }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ result.unavailabilities.project_id = { 1: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id", "project_id"],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+
+ // Normal group cell: open
+ let cell4 = getCell("19 W51 2018");
+ expect(cell4).not.toHaveClass("o_gantt_today");
+ expect(cell4).toHaveClass("o_group_open");
+ expect(cell4).toHaveStyle({
+ backgroundImage: "linear-gradient(rgb(249, 250, 251), rgb(234, 237, 241))",
+ });
+
+ // Today group cell: open
+ let cell5 = getCell("20 W51 2018");
+ expect(cell5).toHaveClass("o_gantt_today");
+ expect(cell5).toHaveClass("o_group_open");
+ expect(cell5).toHaveStyle({
+ backgroundImage: "linear-gradient(rgb(249, 250, 251), rgb(234, 237, 241))",
+ });
+ await contains(SELECTORS.group).click(); // fold group
+ await leave();
+ // Normal group cell: closed
+ cell4 = getCell("19 W51 2018");
+ expect(cell4).not.toHaveClass("o_gantt_today");
+ expect(cell4).not.toHaveClass("o_group_open");
+ expect(cell4).toHaveStyle({
+ backgroundImage: "linear-gradient(rgb(234, 237, 241), rgb(249, 250, 251))",
+ });
+
+ // Today group cell: closed
+ cell5 = getCell("20 W51 2018");
+ expect(cell5).toHaveClass("o_gantt_today");
+ expect(cell5).not.toHaveClass("o_group_open");
+ expect(cell5).toHaveStyle({ backgroundImage: "none" });
+ expect(cell5).toHaveStyle({ backgroundColor: "rgb(252, 250, 243)" });
+});
+
+test("style without unavailabilities", async () => {
+ mockDate("2018-12-05T02:00:00");
+
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step("get_gantt_data");
+ expect(kwargs.unavailability_fields).toEqual([]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ const cell5 = getCell("05 December 2018");
+ expect(cell5).toHaveClass("o_gantt_today");
+ expect(cell5).toHaveAttribute("style", "grid-column:c9/c11;grid-row:r1/r5;");
+ const cell6 = getCell("06 December 2018");
+ expect(cell6).toHaveAttribute("style", "grid-column:c11/c13;grid-row:r1/r5;");
+});
+
+test(`Unavailabilities ("month": "day:half")`, async () => {
+ mockDate("2018-12-05T02:00:00");
+
+ const unavailabilities = [
+ {
+ start: "2018-12-05 09:30:00",
+ stop: "2018-12-07 08:00:00",
+ },
+ {
+ start: "2018-12-16 09:00:00",
+ stop: "2018-12-18 13:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", ({ model, kwargs, parent }) => {
+ expect.step("get_gantt_data");
+ expect(model).toBe("tasks");
+ expect(kwargs.unavailability_fields).toEqual([]);
+ expect(kwargs.start_date).toBe("2018-11-30 23:00:00");
+ expect(kwargs.stop_date).toBe("2019-02-28 23:00:00");
+ const result = parent();
+ result.unavailabilities = { __default: { false: unavailabilities } };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(getCell("05 December 2018")).toHaveClass("o_gantt_today");
+ expect(getCellColorProperties("05 December 2018")).toEqual([
+ "--Gantt__DayOffToday-background-color",
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("06 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("07 December 2018")).toEqual([]);
+ expect(getCellColorProperties("16 December 2018")).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("17 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("18 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ "--Gantt__Day-background-color",
+ ]);
+});
+
+test(`Unavailabilities ("day": "hours:quarter")`, async () => {
+ Tasks._records = [];
+ const unavailabilities = [
+ // in utc
+ {
+ start: "2018-12-20 08:15:00",
+ stop: "2018-12-20 08:30:00",
+ },
+ {
+ start: "2018-12-20 10:35:00",
+ stop: "2018-12-20 12:29:00",
+ },
+ {
+ start: "2018-12-20 20:15:00",
+ stop: "2018-12-20 20:50:00",
+ },
+ ];
+ onRpc("get_gantt_data", ({ kwargs, parent }) => {
+ expect(kwargs.unavailability_fields).toEqual([]);
+ const result = parent();
+ result.unavailabilities = { __default: { false: unavailabilities } };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getCellColorProperties("9am 20 December 2018")).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ ]);
+ expect(getCellColorProperties("11am 20 December 2018")).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("12pm 20 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+ expect(getCellColorProperties("1pm 20 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ "--Gantt__Day-background-color",
+ ]);
+ expect(getCellColorProperties("9pm 20 December 2018")).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOff-background-color",
+ "--Gantt__Day-background-color",
+ ]);
+});
+
+test("offset attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ const { range } = getGridContent();
+ expect(range).toBe("From: 12/16/2018 to: 12/18/2018", {
+ message: "gantt view should be set to 4 days before initial date",
+ });
+});
+
+test("default_group_by attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ title: "User 1",
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 1,
+ title: "Task 1",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ ],
+ },
+ {
+ title: "User 2",
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 1,
+ title: "Task 7",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ },
+ ]);
+});
+
+test("default_group_by attribute with groupBy", async () => {
+ // The default_group_by attribute should be ignored if a groupBy is given.
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 1,
+ title: "Task 2",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 2,
+ title: "Task 4",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 1,
+ title: "Task 3",
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "Task 7",
+ },
+ ],
+ },
+ ]);
+});
+
+test("default_group_by attribute with 2 fields", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ title: "User 1",
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (8) -> 19 December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "2",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 31 December 2018",
+ title: "1",
+ },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 1,
+ title: "Task 4",
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ },
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 20 (1/2) December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "2",
+ },
+ {
+ colSpan: "21 December 2018 -> 22 (1/2) December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "1",
+ },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "Task 7",
+ },
+ ],
+ },
+ ]);
+});
+
+test("default_range attribute", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getActiveScale()).toBe(2); // month scale
+ const { columnHeaders, range } = getGridContent();
+ expect(range).toBe("12/20/2018");
+ expect(columnHeaders).toHaveLength(1);
+ await click(SELECTORS.rangeMenuToggler);
+ await animationFrame();
+ const firstRangeMenuItem = queryFirst(`${SELECTORS.rangeMenu} .dropdown-item`);
+ expect(firstRangeMenuItem).toHaveClass("selected");
+ expect(firstRangeMenuItem).toHaveText("Today");
+});
+
+test("consolidation and unavailabilities", async () => {
+ const unavailabilities = [
+ {
+ start: "2018-12-18 10:00:00",
+ stop: "2018-12-20 14:00:00",
+ },
+ ];
+ onRpc("get_gantt_data", async ({ parent, kwargs }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.unavailability_fields).toEqual(["user_id"]);
+ result.unavailabilities.user_id = { 1: unavailabilities };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ groupBy: ["user_id"],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ // Normal day / unavailability
+ expect(getCellColorProperties("18 December 2018", "", { num: 2 })).toEqual([
+ "--Gantt__Day-background-color",
+ "--Gantt__DayOff-background-color",
+ ]);
+
+ // Full unavailability
+ expect(getCellColorProperties("19 December 2018", "", { num: 2 })).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+
+ // Unavailability / today
+ expect(getCell("20 December 2018")).toHaveClass("o_gantt_today");
+ expect(getCellColorProperties("20 December 2018", "", { num: 2 })).toEqual([
+ "--Gantt__DayOff-background-color",
+ "--Gantt__DayOffToday-background-color",
+ ]);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_basics.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_basics.test.js
new file mode 100644
index 000000000..ba7626685
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_basics.test.js
@@ -0,0 +1,1254 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { click, queryAll, queryAllTexts } from "@odoo/hoot-dom";
+import { animationFrame, mockDate } from "@odoo/hoot-mock";
+import {
+ contains,
+ defineParams,
+ fields,
+ getService,
+ mountWithCleanup,
+ onRpc,
+ patchWithCleanup,
+} from "@web/../tests/web_test_helpers";
+import { Tasks, Project, defineGanttModels } from "./gantt_mock_models";
+import {
+ SELECTORS,
+ focusToday,
+ ganttControlsChanges,
+ getActiveScale,
+ getGridContent,
+ mountGanttView,
+ selectGanttRange,
+ setScale,
+} from "./web_gantt_test_helpers";
+
+import { browser } from "@web/core/browser/browser";
+import { Domain } from "@web/core/domain";
+import { deserializeDateTime } from "@web/core/l10n/dates";
+import { WebClient } from "@web/webclient/webclient";
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => {
+ mockDate("2018-12-20T08:00:00", +1);
+ defineParams({
+ lang_parameters: {
+ time_format: "%I:%M:%S",
+ },
+ });
+});
+
+test("empty ungrouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 0]],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe(null);
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([{}]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("ungrouped gantt rendering", async () => {
+ const task2 = Tasks._records[1];
+ const startDateLocalString = deserializeDateTime(task2.start).toFormat("f");
+ const stopDateLocalString = deserializeDateTime(task2.stop).toFormat("f");
+ Tasks._views.gantt = ``;
+ Tasks._views.search = ``;
+
+ onRpc("get_gantt_data", ({ model }) => expect.step(model));
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [[false, "gantt"]],
+ });
+ expect.verifySteps(["tasks"]);
+ await animationFrame();
+
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe(null);
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 5",
+ level: 0,
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ },
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+
+ // test popover and local timezone
+ expect(`.o_popover`).toHaveCount(0);
+ const task2Pill = queryAll(SELECTORS.pill)[2];
+ expect(task2Pill).toHaveText("Task 2");
+
+ await contains(task2Pill).click();
+ expect(`.o_popover`).toHaveCount(1);
+ expect(queryAllTexts`.o_popover .popover-body span`).toEqual([
+ "Task 2",
+ startDateLocalString,
+ stopDateLocalString,
+ ]);
+
+ await contains(`.o_popover .popover-header i.fa.fa-close`).click();
+ expect(`.o_popover`).toHaveCount(0);
+});
+
+test("ordered gantt view", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["stage_id"],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+ expect(rows).toEqual([
+ {
+ title: "todo",
+ },
+ {
+ title: "in_progress",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> 31 December 2018", title: "Task 1" },
+ {
+ level: 1,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "Task 7",
+ },
+ ],
+ },
+ {
+ title: "done",
+ pills: [
+ {
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ title: "Task 2",
+ },
+ ],
+ },
+ {
+ title: "cancel",
+ pills: [
+ {
+ level: 0,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "Task 4",
+ },
+ { level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019", title: "Task 3" },
+ ],
+ },
+ ]);
+});
+
+test("empty single-level grouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ domain: Domain.FALSE.toList(),
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([{ title: "" }]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("single-level grouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ {
+ title: "Task 1",
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ },
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 1,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 2,
+ },
+ {
+ title: "Task 3",
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 1,
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+});
+
+test("single-level grouped gantt rendering with group_expand", async () => {
+ const groups = [
+ { project_id: [20, "Unused Project 1"], __record_ids: [] },
+ { project_id: [50, "Unused Project 2"], __record_ids: [] },
+ { project_id: [2, "Project 2"], __record_ids: [5, 7] },
+ { project_id: [30, "Unused Project 3"], __record_ids: [] },
+ { project_id: [1, "Project 1"], __record_ids: [1, 2, 3, 4] },
+ ];
+ patchWithCleanup(Tasks.prototype, {
+ web_read_group: () => ({ groups, length: groups.length }),
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ { title: "Unused Project 1" },
+ { title: "Unused Project 2" },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ },
+ ],
+ },
+ { title: "Unused Project 3" },
+ {
+ title: "Project 1",
+ pills: [
+ {
+ title: "Task 1",
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ },
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 1,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 2,
+ },
+ {
+ title: "Task 3",
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 1,
+ },
+ ],
+ },
+ ]);
+});
+
+test("multi-level grouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).toHaveCount(2);
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ title: "User 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "Out of bounds (8) -> 19 December 2018" },
+ { title: "2", colSpan: "20 December 2018 -> 20 (1/2) December 2018" },
+ { title: "1", colSpan: "20 (1/2) December 2018 -> 31 December 2018" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "Out of bounds (1) -> 19 December 2018" },
+ { title: "2", colSpan: "20 December 2018 -> 20 (1/2) December 2018" },
+ { title: "1", colSpan: "20 (1/2) December 2018 -> 31 December 2018" },
+ ],
+ },
+ {
+ title: "To Do",
+ pills: [
+ { title: "Task 1", colSpan: "Out of bounds (1) -> 31 December 2018", level: 0 },
+ ],
+ },
+ {
+ title: "In Progress",
+ pills: [
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ isGroup: true,
+ },
+ {
+ title: "Done",
+ },
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 20 (1/2) December 2018" },
+ { title: "2", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "21 December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Done",
+ pills: [
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ },
+ ],
+ },
+ {
+ title: "Cancelled",
+ pills: [
+ { title: "Task 3", colSpan: "27 December 2018 -> 03 (1/2) January 2019", level: 0 },
+ ],
+ },
+ {
+ title: "Project 2",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "20 (1/2) December 2018 -> 20 December 2018" }],
+ },
+ {
+ title: "Cancelled",
+ pills: [
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+ expect(`.o_gantt_group_pill .o_gantt_consolidated_pill`).toHaveStyle({
+ backgroundColor: "rgb(113, 75, 103)",
+ });
+});
+
+test("many2many grouped gantt rendering", async () => {
+ Tasks._fields.user_ids = fields.Many2many({ string: "Assignees", relation: "res.users" });
+ Tasks._records[0].user_ids = [1, 2];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_ids"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ title: "Undefined Assignees",
+ pills: [
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 1,
+ },
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 1,
+ },
+ { title: "Task 3", colSpan: "27 December 2018 -> 03 (1/2) January 2019", level: 0 },
+ ],
+ },
+ {
+ title: "User 1",
+ pills: [
+ { title: "Task 1", colSpan: "Out of bounds (1) -> 31 December 2018", level: 0 },
+ ],
+ },
+ {
+ title: "User 2",
+ pills: [
+ { title: "Task 1", colSpan: "Out of bounds (1) -> 31 December 2018", level: 0 },
+ ],
+ },
+ ]);
+});
+
+test("multi-level grouped with many2many field in gantt view", async () => {
+ Tasks._fields.user_ids = fields.Many2many({ string: "Assignees", relation: "res.users" });
+ Tasks._records[0].user_ids = [1, 2];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_ids", "project_id"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).toHaveCount(2);
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(34);
+ expect(rows).toEqual([
+ {
+ title: "Undefined Assignees",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 19 December 2018" },
+ { title: "2", colSpan: "20 December 2018 -> 20 (1/2) December 2018" },
+ { title: "2", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "21 December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 1,
+ },
+ { title: "Task 3", colSpan: "27 December 2018 -> 03 (1/2) January 2019", level: 0 },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ },
+ ],
+ },
+ {
+ title: "User 1",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "Out of bounds (1) -> 31 December 2018" }],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", colSpan: "Out of bounds (1) -> 31 December 2018", level: 0 },
+ ],
+ },
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "Out of bounds (1) -> 31 December 2018" }],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", colSpan: "Out of bounds (1) -> 31 December 2018", level: 0 },
+ ],
+ },
+ ]);
+});
+
+test("full precision gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id", "project_id"],
+ });
+ expect(getActiveScale()).toBe(4);
+ expect(SELECTORS.expandCollapseButtons).toHaveCount(2);
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/16/2018 to: 01/05/2019");
+ expect(viewTitle).toBe("Gantt View");
+ expect(columnHeaders).toHaveLength(9);
+ expect(rows).toEqual([
+ {
+ title: "User 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "16 W51 2018 -> 19 W51 2018" },
+ { title: "2", colSpan: "20 W51 2018 -> 20 W51 2018" },
+ { title: "1", colSpan: "21 W51 2018 -> Out of bounds (17) " },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ { level: 0, colSpan: "16 W51 2018 -> Out of bounds (17) ", title: "Task 1" },
+ { level: 1, colSpan: "20 W51 2018 -> 20 W51 2018", title: "Task 4" },
+ ],
+ },
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 W51 2018 -> 19 W51 2018" },
+ { title: "2", colSpan: "20 W51 2018 -> 20 W51 2018" },
+ { title: "1", colSpan: "21 W51 2018 -> 22 W51 2018" },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [{ level: 0, colSpan: "17 W51 2018 -> 22 W51 2018", title: "Task 2" }],
+ },
+ {
+ title: "Project 2",
+ pills: [{ level: 0, colSpan: "20 W51 2018 -> 20 W51 2018", title: "Task 7" }],
+ },
+ ]);
+});
+
+test("gantt rendering, thumbnails", async () => {
+ onRpc("get_gantt_data", () => ({
+ groups: [
+ {
+ user_id: [1, "User 1"],
+ __record_ids: [1],
+ },
+ {
+ user_id: false,
+ __record_ids: [2],
+ },
+ ],
+ length: 2,
+ records: [
+ {
+ display_name: "Task 1",
+ id: 1,
+ start: "2018-11-30 18:30:00",
+ stop: "2018-12-31 18:29:59",
+ },
+ {
+ display_name: "Task 2",
+ id: 2,
+ start: "2018-12-01 18:30:00",
+ stop: "2018-12-02 18:29:59",
+ },
+ ],
+ }));
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id"],
+ });
+ expect(SELECTORS.thumbnail).toHaveCount(1);
+ expect(SELECTORS.thumbnail).toHaveAttribute(
+ "data-src",
+ /web\/image\?model=res\.users&id=1&field=image/
+ );
+});
+
+test("gantt rendering, pills must be chronologically ordered", async () => {
+ onRpc("get_gantt_data", () => ({
+ groups: [
+ {
+ user_id: [1, "User 1"],
+ __record_ids: [1],
+ },
+ {
+ user_id: false,
+ __record_ids: [2],
+ },
+ ],
+ length: 2,
+ records: [
+ {
+ display_name: "Task 14:30:00",
+ id: 1,
+ start: "2018-12-17 14:30:00",
+ stop: "2018-12-17 18:29:59",
+ },
+ {
+ display_name: "Task 08:30:00",
+ id: 2,
+ start: "2018-12-17 08:30:00",
+ stop: "2018-12-17 13:29:59",
+ },
+ ],
+ }));
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ pills: [
+ { title: "Task 08:30:00", level: 0, colSpan: "17 W51 2018 -> 17 W51 2018" },
+ { title: "Task 14:30:00", level: 1, colSpan: "17 (1/2) W51 2018 -> 17 W51 2018" },
+ ],
+ },
+ ]);
+});
+
+test("scale switching", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ // default (month)
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ let gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(34);
+ expect(gridContent.rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 5",
+ level: 0,
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ },
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+
+ // switch to day view
+ await setScale(5);
+ await focusToday();
+ await ganttControlsChanges();
+ expect(getActiveScale()).toBe(5);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(42);
+ expect(gridContent.rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 1",
+ level: 1,
+ colSpan: "Out of bounds (1) -> Out of bounds (741) ",
+ },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "Out of bounds (397) -> Out of bounds (513) ",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "3am 20 December 2018 -> 7am 20 December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "1pm 20 December 2018 -> 7pm 20 December 2018",
+ },
+ ],
+ },
+ ]);
+
+ // switch to week view
+ await setScale(4);
+ await focusToday();
+ await ganttControlsChanges();
+
+ expect(getActiveScale()).toBe(4);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(10);
+ expect(gridContent.rows).toEqual([
+ {
+ pills: [
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> Out of bounds (63) " },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) W51 2018 -> 22 (1/2) W51 2018",
+ },
+ { title: "Task 4", level: 2, colSpan: "20 W51 2018 -> 20 (1/2) W51 2018" },
+ { title: "Task 7", level: 2, colSpan: "20 (1/2) W51 2018 -> 20 W51 2018" },
+ ],
+ },
+ ]);
+
+ // switch to month view
+ await setScale(2);
+ await focusToday();
+ await ganttControlsChanges();
+
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(34);
+ expect(gridContent.rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 5",
+ level: 0,
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ },
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+
+ // switch to year view
+ await setScale(0);
+ await focusToday();
+ await ganttControlsChanges();
+
+ expect(getActiveScale()).toBe(0);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(3);
+ expect(gridContent.rows).toEqual([
+ {
+ pills: [
+ { title: "Task 5", level: 0, colSpan: "December 2018 -> December 2018" },
+ { title: "Task 1", level: 1, colSpan: "December 2018 -> December 2018" },
+ { title: "Task 2", level: 2, colSpan: "December 2018 -> December 2018" },
+ { title: "Task 4", level: 3, colSpan: "December 2018 -> December 2018" },
+ { title: "Task 7", level: 4, colSpan: "December 2018 -> December 2018" },
+ { title: "Task 3", level: 5, colSpan: "December 2018 -> January 2019" },
+ ],
+ },
+ ]);
+});
+
+test("today is highlighted", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveCount(1);
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveText("20");
+});
+
+test("current month is highlighted'", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveCount(1);
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveText("December");
+});
+
+test("current hour is highlighted'", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveCount(1);
+ expect(`.o_gantt_header_cell.o_gantt_today`).toHaveText("9am");
+});
+
+test("Day scale with 12-hours format", async () => {
+ defineParams({
+ lang_parameters: {
+ time_format: "%I:%M:%S",
+ },
+ });
+
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ expect(getActiveScale()).toBe(5);
+ const headers = getGridContent().columnHeaders;
+ expect(headers.slice(0, 4).map((h) => h.title)).toEqual(["12am", "1am", "2am", "3am"]);
+ expect(headers.slice(12, 16).map((h) => h.title)).toEqual(["12pm", "1pm", "2pm", "3pm"]);
+});
+
+test("Day scale with 24-hours format", async () => {
+ defineParams({
+ lang_parameters: {
+ time_format: "%H:%M:%S",
+ },
+ });
+
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ expect(getActiveScale()).toBe(5);
+ const headers = getGridContent().columnHeaders;
+ expect(headers.slice(0, 4).map((h) => h.title)).toEqual(["0", "1", "2", "3"]);
+ expect(headers.slice(12, 16).map((h) => h.title)).toEqual(["12", "13", "14", "15"]);
+});
+
+test("group tasks by task_properties", async () => {
+ Project._fields.properties_definitions = fields.PropertiesDefinition();
+ Project._records[0].properties_definitions = [
+ {
+ name: "bd6404492c244cff",
+ type: "char",
+ },
+ ];
+ Tasks._fields.task_properties = fields.Properties({
+ definition_record: "project_id",
+ definition_record_field: "properties_definitions",
+ });
+ Tasks._records = [
+ {
+ id: 1,
+ name: "Blop",
+ start: "2018-12-14 08:00:00",
+ stop: "2018-12-24 08:00:00",
+ user_id: 1,
+ project_id: 1,
+ task_properties: {
+ bd6404492c244cff: "test value 1",
+ },
+ },
+ {
+ id: 2,
+ name: "Yop",
+ start: "2018-12-02 08:00:00",
+ stop: "2018-12-12 08:00:00",
+ user_id: 2,
+ project_id: 1,
+ task_properties: {
+ bd6404492c244cff: "test value 1",
+ },
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["task_properties.bd6404492c244cff"],
+ });
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Yop",
+ colSpan: "Out of bounds (3) -> 12 (1/2) December 2018",
+ level: 0,
+ },
+ {
+ title: "Blop",
+ colSpan: "14 December 2018 -> 24 (1/2) December 2018",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+});
+
+test("group tasks by date", async () => {
+ Tasks._fields.my_date = fields.Date({ string: "My date" });
+ Tasks._records = [
+ {
+ id: 1,
+ name: "Blop",
+ start: "2018-12-14 08:00:00",
+ stop: "2018-12-24 08:00:00",
+ user_id: 1,
+ project_id: 1,
+ },
+ {
+ id: 2,
+ name: "Yop",
+ start: "2018-12-02 08:00:00",
+ stop: "2018-12-12 08:00:00",
+ user_id: 2,
+ project_id: 1,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["my_date:month"],
+ });
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Yop",
+ colSpan: "Out of bounds (3) -> 12 (1/2) December 2018",
+ level: 0,
+ },
+ {
+ title: "Blop",
+ colSpan: "14 December 2018 -> 24 (1/2) December 2018",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+});
+
+test("Scale: scale default is fetched from localStorage", async () => {
+ let view;
+ patchWithCleanup(browser.localStorage, {
+ getItem(key) {
+ if (String(key).startsWith("scaleOf-viewId")) {
+ expect.step(`get_scale_week`);
+ return "week";
+ }
+ },
+ setItem(key, value) {
+ if (view && key === `scaleOf-viewId-${view.env?.config?.viewId}`) {
+ expect.step(`set_scale_${value}`);
+ }
+ },
+ });
+ view = await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: '',
+ });
+ expect(getActiveScale()).toBe(4);
+ await setScale(0);
+ await ganttControlsChanges();
+ expect(getActiveScale()).toBe(0);
+ expect.verifySteps(["get_scale_week", "set_scale_year"]);
+});
+
+test("initialization with default_start_date only", async (assert) => {
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: '',
+ context: { default_start_date: "2028-04-25" },
+ });
+ const { range, columnHeaders, groupHeaders } = getGridContent();
+ expect(range).toBe("From: 04/25/2028 to: 06/30/2028");
+ expect(columnHeaders.slice(0, 7).map((h) => h.title)).toEqual([
+ "25",
+ "26",
+ "27",
+ "28",
+ "29",
+ "30",
+ "01",
+ ]);
+ expect(groupHeaders.map((h) => h.title)).toEqual(["April 2028", "May 2028"]);
+});
+
+test("initialization with default_stop_date only", async (assert) => {
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: '',
+ context: { default_stop_date: "2028-04-25" },
+ });
+ const { range, columnHeaders, groupHeaders } = getGridContent();
+ expect(range).toBe("From: 02/01/2028 to: 04/25/2028");
+ expect(
+ columnHeaders.slice(columnHeaders.length - 7, columnHeaders.length).map((h) => h.title)
+ ).toEqual(["19", "20", "21", "22", "23", "24", "25"]);
+ expect(groupHeaders.map((h) => h.title)).toEqual(["March 2028", "April 2028"]);
+});
+
+test("initialization with default_start_date and default_stop_date", async (assert) => {
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: '',
+ context: {
+ default_start_date: "2017-01-29",
+ default_stop_date: "2019-05-26",
+ },
+ });
+ const { range, groupHeaders } = getGridContent();
+ expect(range).toBe("From: 01/29/2017 to: 05/26/2019");
+ expect(groupHeaders.map((h) => h.title)).toEqual(["December 2018", "January 2019"]);
+ expect(`${SELECTORS.columnHeader}.o_gantt_today`).toHaveCount(1);
+});
+
+test("data fetched with right domain", async () => {
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step(kwargs.domain);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ });
+ expect.verifySteps([
+ ["&", ["start", "<", "2018-12-22 23:00:00"], ["stop", ">", "2018-12-19 23:00:00"]],
+ ]);
+ await setScale(0);
+ await ganttControlsChanges();
+ expect.verifySteps([
+ ["&", ["start", "<", "2018-12-31 23:00:00"], ["stop", ">", "2018-11-30 23:00:00"]],
+ ]);
+ await selectGanttRange({ startDate: "2018-12-31", stopDate: "2019-06-15" });
+ expect.verifySteps([
+ ["&", ["start", "<", "2019-06-30 23:00:00"], ["stop", ">", "2018-11-30 23:00:00"]],
+ ]);
+});
+
+test("switch startDate and stopDate if not in <= relation", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+ await selectGanttRange({ startDate: "2019-03-01" });
+ expect(getGridContent().range).toBe("From: 03/01/2019 to: 03/01/2019");
+ await selectGanttRange({ stopDate: "2019-02-28" });
+ expect(getGridContent().range).toBe("From: 02/28/2019 to: 02/28/2019");
+});
+
+test("range will not exceed 10 years", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ });
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+ await selectGanttRange({ startDate: "2006-02-28" });
+ expect(getGridContent().range).toBe("From: 02/28/2006 to: 02/27/2016");
+ await selectGanttRange({ stopDate: "2020-02-28" });
+ expect(getGridContent().range).toBe("From: 03/01/2010 to: 02/28/2020");
+});
+
+test("popover-template with an added footer", async () => {
+ expect.assertions(9);
+ onRpc("unlink", ({ model, method, args }) => {
+ expect(model).toBe("tasks");
+ expect(method).toBe("unlink");
+ expect(args).toEqual([[2]]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ Content
+
+
+
+
+ `,
+ domain: [["id", "=", 2]],
+ });
+ expect(SELECTORS.pill).toHaveCount(1);
+ expect(".o_popover").toHaveCount(0);
+
+ await click(SELECTORS.pill);
+ await animationFrame();
+ expect(".o_popover").toHaveCount(1);
+ expect(".o_popover .popover-footer button").toHaveCount(2);
+ expect(queryAllTexts(".o_popover .popover-footer button")).toEqual(["Edit", "Delete"]);
+
+ await click(".o_popover .popover-footer button:last-child");
+ await animationFrame();
+ expect(SELECTORS.pill).toHaveCount(0);
+});
+
+test("popover-template with a replaced footer", async () => {
+ expect.assertions(9);
+ onRpc("unlink", ({ model, method, args }) => {
+ expect(model).toBe("tasks");
+ expect(method).toBe("unlink");
+ expect(args).toEqual([[2]]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ Content
+
+
+
+
+ `,
+ domain: [["id", "=", 2]],
+ });
+ expect(SELECTORS.pill).toHaveCount(1);
+ expect(".o_popover").toHaveCount(0);
+
+ await click(SELECTORS.pill);
+ await animationFrame();
+ expect(".o_popover").toHaveCount(1);
+ expect(".o_popover .popover-footer button").toHaveCount(1);
+ expect(".o_popover .popover-footer button").toHaveText("Delete");
+
+ await click(".o_popover .popover-footer button");
+ await animationFrame();
+ expect(SELECTORS.pill).toHaveCount(0);
+});
+
+test("popover-template with a button in the body", async () => {
+ expect.assertions(11);
+ onRpc("unlink", ({ model, method, args }) => {
+ expect(model).toBe("tasks");
+ expect(method).toBe("unlink");
+ expect(args).toEqual([[2]]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+
+
+
+
+ `,
+ domain: [["id", "=", 2]],
+ });
+ expect(SELECTORS.pill).toHaveCount(1);
+ expect(".o_popover").toHaveCount(0);
+
+ await click(SELECTORS.pill);
+ await animationFrame();
+ expect(".o_popover").toHaveCount(1);
+ expect(".o_popover .popover-body button").toHaveCount(1);
+ expect(".o_popover .popover-footer button").toHaveCount(1);
+ expect(".o_popover .popover-body button").toHaveText("Delete");
+ expect(".o_popover .popover-footer button").toHaveText("Edit");
+
+ await click(".o_popover .popover-body button");
+ await animationFrame();
+ expect(SELECTORS.pill).toHaveCount(0);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_behavioral.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_behavioral.test.js
new file mode 100644
index 000000000..852f949b5
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_behavioral.test.js
@@ -0,0 +1,2301 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import {
+ click,
+ hover,
+ keyDown,
+ keyUp,
+ pointerDown,
+ press,
+ queryAllTexts,
+ queryOne,
+ scroll,
+} from "@odoo/hoot-dom";
+import { Deferred, advanceTime, animationFrame, mockDate, runAllTimers } from "@odoo/hoot-mock";
+import {
+ contains,
+ defineParams,
+ fields,
+ mockService,
+ onRpc,
+ patchWithCleanup,
+ validateSearch,
+} from "@web/../tests/web_test_helpers";
+import { ResUsers, Tasks, defineGanttModels } from "./gantt_mock_models";
+import {
+ CLASSES,
+ SELECTORS,
+ clickCell,
+ dragPill,
+ editPill,
+ focusToday,
+ ganttControlsChanges,
+ getActiveScale,
+ getCell,
+ getGridContent,
+ getPill,
+ getPillWrapper,
+ hoverGridCell,
+ mountGanttView,
+ resizePill,
+ selectGanttRange,
+ selectRange,
+ setScale,
+} from "./web_gantt_test_helpers";
+
+import { omit, pick } from "@web/core/utils/objects";
+import { deserializeDate } from "@web/core/l10n/dates";
+
+// Hard-coded daylight saving dates from 2019
+const DST_DATES = {
+ winterToSummer: {
+ before: "2019-03-30",
+ after: "2019-03-31",
+ },
+ summerToWinter: {
+ before: "2019-10-26",
+ after: "2019-10-27",
+ },
+};
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => {
+ mockDate("2018-12-20T08:00:00", +1);
+ defineParams({
+ lang_parameters: {
+ time_format: "%I:%M:%S",
+ },
+ });
+});
+
+test("date navigation with timezone (1h)", async () => {
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step(kwargs.domain.toString());
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+
+ // switch to day view and check day navigation
+ await setScale(5);
+ await ganttControlsChanges();
+
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+
+ // switch to week view and check week navigation
+ await setScale(1);
+ await ganttControlsChanges();
+
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+
+ // switch to year view and check year navigation
+ await setScale(5);
+ await ganttControlsChanges();
+
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+ expect(getGridContent().range).toBe("From: 12/01/2018 to: 02/28/2019");
+});
+
+test("if a on_create is specified, execute the action rather than opening a dialog. And reloads after the action", async () => {
+ mockService("action", {
+ doAction(action, options) {
+ expect.step(`[action] ${action}`);
+ expect(options.additionalContext).toEqual({
+ default_start: "2018-11-30 23:00:00",
+ default_stop: "2018-12-31 23:00:00",
+ lang: "en",
+ allowed_company_ids: [1],
+ start: "2018-11-30 23:00:00",
+ stop: "2018-12-31 23:00:00",
+ tz: "taht",
+ uid: 7,
+ });
+ options.onClose();
+ },
+ });
+
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect.verifySteps(["get_gantt_data"]);
+
+ await contains(SELECTORS.addButton + ":visible").click();
+ expect.verifySteps(["[action] this_is_create_action", "get_gantt_data"]);
+});
+
+test("select cells to plan a task", async () => {
+ mockService("dialog", {
+ add(_, props) {
+ expect.step(`[dialog] ${props.title}`);
+ expect(props.context).toEqual({
+ default_start: "2018-12-18 23:00:00",
+ default_stop: "2018-12-21 23:00:00",
+ lang: "en",
+ allowed_company_ids: [1],
+ start: "2018-12-18 23:00:00",
+ stop: "2018-12-21 23:00:00",
+ tz: "taht",
+ uid: 7,
+ });
+ },
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ await contains(getCell("19 December 2018")).dragAndDrop(getCell("21 December 2018"));
+
+ expect.verifySteps(["[dialog] Plan"]);
+});
+
+test("drag and drop on the same cell to plan a task", async () => {
+ mockService("dialog", {
+ add(_, props) {
+ expect.step(`[dialog] ${props.title}`);
+ expect(props.context).toEqual({
+ default_start: "2018-12-14 23:00:00",
+ default_stop: "2018-12-15 23:00:00",
+ lang: "en",
+ allowed_company_ids: [1],
+ start: "2018-12-14 23:00:00",
+ stop: "2018-12-15 23:00:00",
+ tz: "taht",
+ uid: 7,
+ });
+ },
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ await contains(getCell("15 December 2018")).dragAndDrop(getCell("15 December 2018"));
+
+ expect.verifySteps(["[dialog] Plan"]);
+});
+
+test("row id is properly escaped to avoid name issues in selection", async () => {
+ mockService("dialog", {
+ add() {
+ expect.step("[dialog]");
+ },
+ });
+
+ ResUsers._records[0].name = "O'Reilly";
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ await hoverGridCell("11 December 2018");
+ await clickCell("11 December 2018");
+
+ expect.verifySteps(["[dialog]"]);
+});
+
+test("select cells to plan a task: 1-level grouped", async () => {
+ mockService("dialog", {
+ add(_, props) {
+ expect.step(`[dialog] ${props.title}`);
+ expect(props.context).toEqual({
+ default_start: "2018-12-10 23:00:00",
+ default_stop: "2018-12-12 23:00:00",
+ default_user_id: 1,
+ lang: "en",
+ allowed_company_ids: [1],
+ start: "2018-12-10 23:00:00",
+ stop: "2018-12-12 23:00:00",
+ tz: "taht",
+ uid: 7,
+ user_id: 1,
+ });
+ },
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id"],
+ });
+
+ await hoverGridCell("11 December 2018");
+ const { moveTo, drop } = await contains(getCell("11 December 2018")).drag();
+ moveTo(getCell("12 December 2018"));
+ await runAllTimers(); // Pointer move is subjected to throttleForAnimation in gantt
+ drop();
+
+ expect.verifySteps(["[dialog] Plan"]);
+});
+
+test("select cells to plan a task: 2-level grouped", async () => {
+ mockService("dialog", {
+ add(_, props) {
+ expect.step(`[dialog] ${props.title}`);
+ expect(props.context).toEqual({
+ default_project_id: 1,
+ default_start: "2018-12-10 23:00:00",
+ default_stop: "2018-12-12 23:00:00",
+ default_user_id: 1,
+ allowed_company_ids: [1],
+ lang: "en",
+ project_id: 1,
+ start: "2018-12-10 23:00:00",
+ stop: "2018-12-12 23:00:00",
+ tz: "taht",
+ uid: 7,
+ user_id: 1,
+ });
+ },
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id"],
+ });
+ await hoverGridCell("11 December 2018");
+ const dragAndDrop1 = await contains(getCell("11 December 2018")).drag();
+ dragAndDrop1.moveTo(getCell("12 December 2018"));
+ await advanceTime(20); // Pointer move is subjected to throttleForAnimation in gantt
+ dragAndDrop1.drop();
+ // nothing happens
+ await hoverGridCell("11 December 2018", "Project 1");
+ await advanceTime(20);
+ const dragAndDrop2 = await contains(getCell("11 December 2018", "Project 1")).drag();
+ dragAndDrop2.moveTo(getCell("12 December 2018", "Project 1"));
+ await advanceTime(20);
+ dragAndDrop2.drop();
+
+ expect.verifySteps(["[dialog] Plan"]);
+});
+
+test("hovering a cell with special character", async () => {
+ expect.assertions(1);
+
+ // add special character to data
+ ResUsers._records[0].name = "User' 1";
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id"],
+ });
+
+ // hover on first header "User' 1" with data-row-id equal to [{"user_id":[1,"User' 1"]}]
+ // the "'" must be escaped with "\\'" in findSiblings to prevent the selector to crash
+ await contains(".o_gantt_row_header").hover();
+
+ expect(".o_gantt_row_header:first").toHaveClass("o_gantt_group_hovered", {
+ message: "hover style is applied to the element",
+ });
+});
+
+test("open a dialog to add a new task", async () => {
+ defineParams({
+ lang_parameters: {
+ time_format: "%H:%M:%S",
+ },
+ });
+
+ Tasks._views = {
+ form: `
+
+ `,
+ };
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect(".modal").toHaveCount(0);
+
+ await contains(SELECTORS.addButton + ":visible").click();
+
+ // check that the dialog is opened with prefilled fields
+ expect(".modal").toHaveCount(1);
+ expect(".o_field_widget[name=start] input").toHaveValue("12/01/2018 00:00:00");
+ expect(".o_field_widget[name=stop] input").toHaveValue("01/01/2019 00:00:00");
+});
+
+test("open a dialog to create/edit a task", async () => {
+ defineParams({
+ lang_parameters: {
+ time_format: "%H:%M:%S",
+ },
+ });
+
+ Tasks._views = {
+ form: `
+
+ `,
+ };
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+
+ // open dialog to create a task
+ expect(".modal").toHaveCount(0);
+ await hoverGridCell("10 December 2018", "In Progress");
+ await clickCell("10 December 2018", "In Progress");
+
+ // check that the dialog is opened with prefilled fields
+ expect(".modal").toHaveCount(1);
+ expect(".modal-title").toHaveText("Create");
+ await contains(".o_field_widget[name=name] input").edit("Task 8");
+ expect(".o_field_widget[name=start] input").toHaveValue("12/10/2018 00:00:00");
+ expect(".o_field_widget[name=stop] input").toHaveValue("12/11/2018 00:00:00");
+ expect(".o_field_widget[name=project_id] input").toHaveValue("Project 1");
+ expect(".o_field_widget[name=user_id] input").toHaveValue("User 1");
+ expect(".o_field_widget[name=stage] select").toHaveValue('"in_progress"');
+
+ // create the task
+ await contains(".o_form_button_save").click();
+ expect(".modal").toHaveCount(0);
+
+ // open dialog to view a task
+ await editPill("Task 8");
+ expect(".modal").toHaveCount(1);
+ expect(".modal-title").toHaveText("Open");
+ expect(".o_field_widget[name=name] input").toHaveValue("Task 8");
+ expect(".o_field_widget[name=start] input").toHaveValue("12/10/2018 00:00:00");
+ expect(".o_field_widget[name=stop] input").toHaveValue("12/11/2018 00:00:00");
+ expect(".o_field_widget[name=project_id] input").toHaveValue("Project 1");
+ expect(".o_field_widget[name=user_id] input").toHaveValue("User 1");
+ expect(".o_field_widget[name=stage] select").toHaveValue('"in_progress"');
+});
+
+test("open a dialog to create a task when grouped by many2many field", async () => {
+ Tasks._fields.user_ids = fields.Many2many({
+ string: "Assignees",
+ relation: "res.users",
+ });
+ Tasks._records[0].user_ids = [1, 2];
+ Tasks._views = {
+ form: `
+
+ `,
+ };
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_ids", "project_id"],
+ });
+
+ // Check grouped rows
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Undefined Assignees",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 19 December 2018" },
+ { title: "2", colSpan: "20 December 2018 -> 20 (1/2) December 2018" },
+ { title: "2", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "21 December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ {
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ title: "Task 2",
+ },
+ {
+ level: 1,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "Task 4",
+ },
+ {
+ level: 0,
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "Task 3",
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "Task 7",
+ },
+ ],
+ },
+ {
+ title: "User 1",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "Out of bounds (1) -> 31 December 2018" }],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> 31 December 2018", title: "Task 1" },
+ ],
+ },
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "Out of bounds (1) -> 31 December 2018" }],
+ },
+ {
+ title: "Project 1",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> 31 December 2018", title: "Task 1" },
+ ],
+ },
+ ]);
+
+ // open dialog to create a task with two many2many values
+ await hoverGridCell("10 December 2018", "Project 1", { num: 2 });
+ await clickCell("10 December 2018", "Project 1", { num: 2 });
+ await contains(".o_field_widget[name=name] input").edit("NEW TASK 0");
+ await contains(".o_field_widget[name=user_ids] input").fill("User 2", { confirm: false });
+ await runAllTimers();
+ await contains(".o-autocomplete--dropdown-menu li:first-child a").click();
+ await contains(".o_form_button_save").click();
+ expect(".modal").toHaveCount(0);
+ const [, , , , fifthRow, , seventhRow] = getGridContent().rows;
+ expect(fifthRow).toEqual({
+ title: "Project 1",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> 31 December 2018", title: "Task 1" },
+ { level: 1, colSpan: "10 December 2018 -> 10 December 2018", title: "NEW TASK 0" },
+ ],
+ });
+ expect(seventhRow).toEqual({
+ title: "Project 1",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> 31 December 2018", title: "Task 1" },
+ { level: 1, colSpan: "10 December 2018 -> 10 December 2018", title: "NEW TASK 0" },
+ ],
+ });
+
+ // open dialog to create a task with no many2many values
+ await hoverGridCell("24 December 2018", "Project 2");
+ await clickCell("24 December 2018", "Project 2");
+ await contains(".o_field_widget[name=name] input").edit("NEW TASK 1");
+ await contains(".o_form_button_save").click();
+ expect(".modal").toHaveCount(0);
+ const [, , thirdRow] = getGridContent().rows;
+ expect(thirdRow).toEqual({
+ title: "Project 2",
+ pills: [
+ {
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "Task 7",
+ },
+ { level: 0, colSpan: "24 December 2018 -> 24 December 2018", title: "NEW TASK 1" },
+ ],
+ });
+});
+
+test("open a dialog to create a task, does not have a delete button", async () => {
+ Tasks._views = {
+ form: ``,
+ };
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: [],
+ });
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect(".modal").toHaveCount(1);
+ expect(".modal .o_btn_remove").toHaveCount(0);
+});
+
+test("open a dialog to edit a task, has a delete buttton", async () => {
+ Tasks._views = {
+ form: ``,
+ };
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: [],
+ });
+ await editPill("Task 1");
+ expect(".modal").toHaveCount(1);
+ expect(".modal .o_form_button_remove").toHaveCount(1);
+});
+
+test("clicking on delete button in edit dialog triggers a confirmation dialog, clicking discard does not call unlink on the model", async () => {
+ Tasks._views = {
+ form: ``,
+ };
+ onRpc(({ method }) => {
+ if (method === "unlink") {
+ expect.step(method);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: [],
+ });
+ expect(".o_dialog").toHaveCount(0);
+ await editPill("Task 1");
+ expect(".o_dialog").toHaveCount(1);
+ // trigger the delete button
+ await contains(".o_dialog .o_form_button_remove").click();
+ expect(".o_dialog").toHaveCount(2);
+
+ const button = queryOne(".o_dialog:not(.o_inactive_modal) footer .btn-secondary");
+ expect(button).toHaveText("Cancel");
+ await contains(button).click();
+ expect(".o_dialog").toHaveCount(1);
+ expect.verifySteps([]);
+});
+
+test("clicking on delete button in edit dialog triggers a confirmation dialog, clicking ok calls unlink on the model", async () => {
+ Tasks._views = {
+ form: ``,
+ };
+ onRpc("unlink", () => {
+ expect.step("unlink");
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: [],
+ });
+ expect(".o_dialog").toHaveCount(0);
+ await editPill("Task 1");
+ expect(".o_dialog").toHaveCount(1);
+ // trigger the delete button
+ await contains(".o_dialog .o_form_button_remove").click();
+ expect(".o_dialog").toHaveCount(2);
+ const button = queryOne(".o_dialog:not(.o_inactive_modal) footer .btn-primary");
+ expect(button).toHaveText("Ok");
+ await contains(button).click();
+ expect(".o_dialog").toHaveCount(0);
+ expect.verifySteps(["unlink"]);
+ // Check that the pill has disappeared
+ await expect(editPill("Task 1")).rejects.toThrow();
+});
+
+test("create dialog with timezone", async () => {
+ defineParams({
+ lang_parameters: {
+ time_format: "%H:%M:%S",
+ },
+ });
+
+ expect.assertions(3);
+
+ Tasks._views = {
+ form: ``,
+ };
+
+ onRpc(({ method, args }) => {
+ if (method === "web_save") {
+ expect(args[1]).toEqual({
+ start: "2018-12-09 23:00:00",
+ stop: "2018-12-10 23:00:00",
+ });
+ }
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect(".o_field_widget[name=start] input").toHaveValue("12/10/2018 00:00:00");
+ expect(".o_field_widget[name=stop] input").toHaveValue("12/11/2018 00:00:00");
+ await contains(".o_form_button_save").click();
+});
+
+test("open a dialog to plan a task", async () => {
+ Tasks._views = {
+ list: '
',
+ search: '',
+ };
+ Tasks._records.push(
+ { id: 41, name: "Task 41" },
+ { id: 42, name: "Task 42", stop: "2018-12-31 18:29:59" },
+ { id: 43, name: "Task 43", start: "2018-11-30 18:30:00" }
+ );
+ onRpc(({ method, args, model }) => {
+ if (method === "write") {
+ expect.step(model);
+ expect(args[0]).toEqual([41, 42], { message: "should write on the selected ids" });
+ expect(args[1]).toEqual({
+ start: "2018-12-09 23:00:00",
+ stop: "2018-12-10 23:00:00",
+ });
+ }
+ });
+ onRpc("has_group", () => true);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ // click on the plan button
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect(".modal .o_list_view").toHaveCount(1);
+ expect(queryAllTexts(".modal .o_list_view .o_data_cell")).toEqual([
+ "Task 41",
+ "Task 42",
+ "Task 43",
+ ]);
+
+ // Select the first two tasks
+ await contains(".modal .o_list_view tbody tr:nth-child(1) input").click();
+ await contains(".modal .o_list_view tbody tr:nth-child(2) input").click();
+ await contains(".modal footer .o_select_button").click();
+ expect.verifySteps(["tasks"]);
+});
+
+test("open a dialog to plan a task (multi-level)", async () => {
+ Tasks._views = {
+ list: '
',
+ search: '',
+ };
+ Tasks._records.push({ id: 41, name: "Task 41" });
+
+ onRpc(({ args, method, model }) => {
+ if (method === "write") {
+ expect.step(model);
+ expect(args[0]).toEqual([41], { message: "should write on the selected id" });
+ expect(args[1]).toEqual(
+ {
+ project_id: 1,
+ stage: "todo",
+ start: "2018-12-09 23:00:00",
+ stop: "2018-12-10 23:00:00",
+ user_id: 1,
+ },
+ { message: "should write on all the correct fields" }
+ );
+ }
+ });
+ onRpc("has_group", () => true);
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+
+ // click on the plan button
+ await hoverGridCell("10 December 2018", "To Do");
+ await clickCell("10 December 2018", "To Do");
+ expect(".modal .o_list_view").toHaveCount(1);
+ expect(".modal .o_list_view .o_data_cell").toHaveText("Task 41");
+
+ // Select the first task
+ await contains(".modal .o_list_view tbody tr:nth-child(1) input").click();
+ await animationFrame();
+ await contains(".modal-footer .o_select_button").click();
+ expect.verifySteps(["tasks"]);
+});
+
+test("expand/collapse rows", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "To Do" },
+ { title: "In Progress" },
+ { title: "Project 2", isGroup: true },
+ { title: "Done" },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+
+ // collapse all groups
+ await contains(SELECTORS.collapseButton).click();
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "User 2", isGroup: true },
+ ]);
+
+ // expand all groups
+ await contains(SELECTORS.expandButton).click();
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "To Do" },
+ { title: "In Progress" },
+ { title: "Project 2", isGroup: true },
+ { title: "Done" },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+
+ // collapse the first group
+ await contains(`${SELECTORS.rowHeader}${SELECTORS.group}:nth-child(1)`).click();
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+});
+
+test("collapsed rows remain collapsed at reload", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "stage"],
+ });
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "To Do" },
+ { title: "In Progress" },
+ { title: "Project 2", isGroup: true },
+ { title: "Done" },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+
+ // collapse the first group
+ await contains(`${SELECTORS.rowHeader}${SELECTORS.group}:nth-child(1)`).click();
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+
+ // reload
+ await validateSearch();
+ expect(getGridContent().rows.map((r) => omit(r, "pills"))).toEqual([
+ { title: "User 1", isGroup: true },
+ { title: "User 2", isGroup: true },
+ { title: "Project 1", isGroup: true },
+ { title: "Done" },
+ { title: "Cancelled" },
+ { title: "Project 2", isGroup: true },
+ { title: "Cancelled" },
+ ]);
+});
+
+test("resize a pill", async () => {
+ expect.assertions(10);
+
+ onRpc("write", ({ args }) => {
+ // initial dates -- start: '2018-11-30 18:30:00', stop: '2018-12-31 18:29:59'
+ expect.step(args);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "=", 1]],
+ context: { initialDate: "2018-12-25" },
+ });
+
+ expect(SELECTORS.pill).toHaveCount(1, { message: "there should be one pill (Task 1)" });
+ expect(SELECTORS.resizable).toHaveCount(1);
+ expect(SELECTORS.resizeHandle).toHaveCount(0);
+
+ await contains(getPillWrapper("Task 1")).hover();
+
+ // No start resizer because the start date overflows
+ expect(SELECTORS.resizeStartHandle).toHaveCount(0);
+ expect(SELECTORS.resizeEndHandle).toHaveCount(1);
+
+ // resize to one cell smaller at end (-1 day)
+ await resizePill(getPillWrapper("Task 1"), "end", -1);
+
+ await selectGanttRange({ startDate: "2018-11-10", stopDate: "2018-11-30" });
+
+ expect(".o_gantt_pill").toHaveCount(1, { message: "there should still be one pill (Task 1)" });
+ expect(SELECTORS.resizable).toHaveCount(1);
+
+ await contains(getPillWrapper("Task 1")).hover();
+
+ // No end resizer because the end date overflows
+ expect(SELECTORS.resizeStartHandle).toHaveCount(1);
+ expect(SELECTORS.resizeEndHandle).toHaveCount(0);
+
+ // resize to one cell smaller at start (-1 day)
+ await resizePill(getPillWrapper("Task 1"), "start", -1);
+
+ expect.verifySteps([
+ [[1], { stop: "2018-12-30 18:29:59" }],
+ [[1], { start: "2018-11-29 18:30:00" }],
+ ]);
+});
+
+test("resize pill in year mode", async () => {
+ expect.assertions(2);
+
+ onRpc(({ method }) => {
+ if (method === "write") {
+ throw new Error("Should not call write");
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ const initialPillWidth = getPillWrapper("Task 5").getBoundingClientRect().width;
+
+ expect(getPillWrapper("Task 5")).toHaveClass(CLASSES.resizable);
+
+ // Resize way over the limit
+ await resizePill(getPillWrapper("Task 5"), "end", 0, { x: 200 });
+
+ expect(initialPillWidth).toBe(getPillWrapper("Task 5").getBoundingClientRect().width, {
+ message: "the pill should have the same width as before the resize",
+ });
+});
+
+test("resize a pill (2)", async () => {
+ expect.assertions(5);
+ onRpc("write", ({ args }) => expect.step(args));
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "=", 2]],
+ });
+
+ expect(SELECTORS.pill).toHaveCount(1);
+
+ await contains(getPillWrapper("Task 2")).hover();
+
+ expect(getPillWrapper("Task 2")).toHaveClass(CLASSES.resizable);
+ expect(SELECTORS.resizeHandle).toHaveCount(2);
+
+ // resize to one cell larger
+ await resizePill(getPillWrapper("Task 2"), "end", +1);
+
+ expect(".modal").toHaveCount(0);
+ expect.verifySteps([[[2], { stop: "2018-12-23 06:29:59" }]]);
+});
+
+test.tags("desktop");
+test("resize a pill: quickly enter the neighbour pill when resize start", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "in", [4, 7]]],
+ });
+ expect(SELECTORS.pill).toHaveCount(2);
+ await contains(getPillWrapper("Task 4")).hover();
+ expect(getPillWrapper("Task 4")).toHaveClass(CLASSES.resizable);
+ expect(SELECTORS.resizeHandle).toHaveCount(2);
+
+ // Here we simulate a resize start on Task 4 and quickly enter Task 7
+ // The resize handle should not be added to Task 7
+ await pointerDown(SELECTORS.resizeEndHandle);
+ await hover(getPillWrapper("Task 7"));
+
+ expect(getPillWrapper("Task 4").querySelectorAll(SELECTORS.resizeHandle)).toHaveCount(2);
+ expect(getPillWrapper("Task 7").querySelectorAll(SELECTORS.resizeHandle)).toHaveCount(0);
+});
+
+test("create a task maintains the domain", async () => {
+ Tasks._views = { form: '' };
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["user_id", "=", 2]], // I am an important line
+ });
+ expect(SELECTORS.pill).toHaveCount(3);
+ await hoverGridCell("06 December 2018");
+ await clickCell("06 December 2018");
+
+ await contains(".modal [name=name] input").edit("new task");
+ await contains(".modal .o_form_button_save").click();
+ expect(SELECTORS.pill).toHaveCount(3);
+});
+
+test("pill is updated after failed resized", async () => {
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+ onRpc("write", () => {
+ expect.step("write");
+ return true;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "=", 7]],
+ });
+
+ const initialPillWidth = getPillWrapper("Task 7").getBoundingClientRect().width;
+
+ // resize to one cell larger (1 day)
+ await resizePill(getPillWrapper("Task 7"), "end", +1);
+
+ expect(initialPillWidth).toBe(getPillWrapper("Task 7").getBoundingClientRect().width);
+
+ expect.verifySteps(["get_gantt_data", "write", "get_gantt_data"]);
+});
+
+test("move a pill in the same row", async () => {
+ expect.assertions(5);
+
+ onRpc("write", ({ args }) => {
+ expect(args[0]).toEqual([7], { message: "should write on the correct record" });
+ expect(args[1]).toEqual(
+ {
+ start: "2018-12-21 12:30:12",
+ stop: "2018-12-21 18:29:59",
+ },
+ { message: "both start and stop date should be correctly set (+1 day)" }
+ );
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "=", 7]],
+ });
+
+ expect(getPillWrapper("Task 7")).toHaveClass(CLASSES.draggable);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ ]);
+
+ // move a pill in the next cell (+1 day)
+ const { drop } = await dragPill("Task 7");
+ await drop({ column: "21 December 2018", part: 2 });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "21 (1/2) December 2018 -> 21 December 2018",
+ },
+ ],
+ },
+ ]);
+});
+
+test("move a pill in the same row (with different timezone)", async () => {
+ expect.assertions(4);
+
+ patchWithCleanup(luxon.Settings, {
+ defaultZone: luxon.IANAZone.create("Europe/Brussels"),
+ });
+
+ Tasks._records[7].start = `${DST_DATES.winterToSummer.before} 05:00:00`;
+ Tasks._records[7].stop = `${DST_DATES.winterToSummer.before} 06:30:00`;
+
+ onRpc(({ args, method }) => {
+ if (method === "write") {
+ expect.step("write");
+ expect(args).toEqual([
+ [8],
+ {
+ start: `${DST_DATES.winterToSummer.after} 04:00:00`,
+ stop: `${DST_DATES.winterToSummer.after} 05:30:00`,
+ },
+ ]);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ domain: [["id", "=", 8]],
+ context: {
+ initialDate: `${DST_DATES.winterToSummer.before} 08:00:00`,
+ },
+ });
+
+ await contains(".o_content").scroll({ x: 300 });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [{ title: "Task 8", level: 0, colSpan: "30 March 2019 -> 30 (1/2) March 2019" }],
+ },
+ ]);
+
+ // +1 day -> move beyond the DST switch
+ const { drop } = await dragPill("Task 8");
+ await drop({ column: "31 March 2019", part: 1 });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [{ title: "Task 8", level: 0, colSpan: "31 March 2019 -> 31 (1/2) March 2019" }],
+ },
+ ]);
+ expect.verifySteps(["write"]);
+});
+
+test("move a pill in another row", async () => {
+ expect.assertions(4);
+
+ onRpc("write", ({ args }) => {
+ expect(args[0]).toEqual([7], { message: "should write on the correct record" });
+ expect(args[1]).toEqual(
+ {
+ project_id: 1,
+ start: "2018-12-21 12:30:12",
+ stop: "2018-12-21 18:29:59",
+ },
+ { message: "all modified fields should be correctly set" }
+ );
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["project_id"],
+ domain: [["id", "in", [1, 7]]],
+ });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ ]);
+
+ // move a pill (task 7) in the other row and in the the next cell (+1 day)
+ const { drop } = await dragPill("Task 7");
+ await drop({ column: "21 December 2018", part: 2 });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 7",
+ level: 1,
+ colSpan: "21 (1/2) December 2018 -> 21 December 2018",
+ },
+ ],
+ },
+ ]);
+});
+
+test("copy a pill in another row", async () => {
+ expect.assertions(6);
+ onRpc("copy", ({ args, kwargs }) => {
+ expect(args[0]).toEqual([7], { message: "should copy the correct record" });
+ expect(kwargs.default).toEqual(
+ {
+ start: "2018-12-21 12:30:12",
+ stop: "2018-12-21 18:29:59",
+ project_id: 1,
+ },
+ { message: "should use the correct default values when copying" }
+ );
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["project_id"],
+ domain: [["id", "in", [1, 7, 9]]], // 9 will be the newly created record
+ });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ ]);
+
+ await keyDown("Control");
+
+ // move a pill (task 7) in the other row and in the the next cell (+1 day)
+ const { drop, moveTo } = await dragPill("Task 7");
+ await moveTo({ column: "21 December 2018", part: 2 });
+
+ expect(SELECTORS.renderer).toHaveClass("o_copying");
+
+ await keyUp("Control");
+
+ expect(SELECTORS.renderer).toHaveClass("o_grabbing");
+
+ await keyDown("Control");
+ await drop({ column: "21 December 2018", part: 2 });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 7 (copy)",
+ level: 1,
+ colSpan: "21 (1/2) December 2018 -> 21 December 2018",
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ ]);
+});
+
+test("move a pill in another row in multi-level grouped", async () => {
+ onRpc("write", ({ args }) => {
+ expect(args).toEqual([[7], { project_id: 1 }], {
+ message: "should only write on user_id on the correct record",
+ });
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "stage"],
+ domain: [["id", "in", [3, 7]]],
+ });
+
+ expect(`${SELECTORS.pillWrapper}${SELECTORS.draggable}`).toHaveCount(2);
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" }],
+ },
+ {
+ title: "Cancelled",
+ pills: [
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 2",
+ isGroup: true,
+ pills: [{ title: "1", colSpan: "20 (1/2) December 2018 -> 20 December 2018" }],
+ },
+ {
+ title: "Cancelled",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ ]);
+
+ // move a pill (task 7) in the top-level group (User 2)
+ const { drop } = await dragPill("Task 7");
+ await drop({ row: "Cancelled", column: "20 December 2018", part: 2 });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "20 (1/2) December 2018 -> 20 December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Cancelled",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+});
+
+test("move a pill in another row in multi-level grouped (many2many case)", async () => {
+ expect.assertions(5);
+
+ Tasks._fields.user_ids = fields.Many2many({ string: "Assignees", relation: "res.users" });
+ Tasks._records[1].user_ids = [1, 2];
+
+ onRpc("write", ({ args }) => {
+ expect(args[0]).toEqual([2], { message: "should write on the correct record" });
+ expect(args[1]).toEqual({ user_ids: false }, { message: "should write these changes" });
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id", "user_ids"],
+ domain: [
+ ["user_id", "=", 2],
+ ["project_id", "=", 1],
+ ],
+ });
+
+ // sanity check
+ expect(queryAllTexts(`${SELECTORS.pillWrapper}${SELECTORS.draggable}`)).toEqual([
+ "Task 3",
+ "Task 2",
+ "Task 2",
+ ]);
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Undefined Assignees",
+ pills: [
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "User 1",
+ pills: [
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ ],
+ },
+ {
+ title: "User 2",
+ pills: [
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ ],
+ },
+ ]);
+
+ // move a pill (first task 2) in "Undefined Assignees"
+ const { drop } = await dragPill("Task 2", { nth: 1 });
+ await drop({ row: "Undefined Assignees", column: "17 December 2018", part: 2 });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "User 2",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Project 1",
+ isGroup: true,
+ pills: [
+ { title: "1", colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018" },
+ { title: "1", colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ {
+ title: "Undefined Assignees",
+ pills: [
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ]);
+});
+
+test("grey pills should not be resizable nor draggable", async () => {
+ expect.assertions(4);
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id"],
+ domain: [["id", "=", 7]],
+ });
+
+ const groupPill = queryOne(`${SELECTORS.pillWrapper}.o_gantt_group_pill`);
+ expect(groupPill).not.toHaveClass(CLASSES.resizable);
+ expect(groupPill).not.toHaveClass(CLASSES.draggable);
+
+ const rowPill = queryOne(`${SELECTORS.pillWrapper}:not(.o_gantt_group_pill)`);
+ expect(rowPill).toHaveClass(CLASSES.resizable);
+ expect(rowPill).toHaveClass(CLASSES.draggable);
+});
+
+test("should not be draggable when disable_drag_drop is set", async () => {
+ expect.assertions(1);
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["user_id", "project_id"],
+ domain: [["id", "=", 7]],
+ });
+
+ expect(SELECTORS.draggable).toHaveCount(0);
+});
+
+test("view reload when scale changes", async () => {
+ let reloadCount = 0;
+ onRpc("get_gantt_data", () => {
+ reloadCount++;
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+
+ arch: '',
+ });
+ expect(reloadCount).toBe(1, { message: "view should have loaded" });
+
+ await setScale(4);
+ await ganttControlsChanges();
+ expect(reloadCount).toBe(2, {
+ message: "view should have reloaded when switching scale to week",
+ });
+
+ await setScale(2);
+ await ganttControlsChanges();
+ expect(reloadCount).toBe(3, {
+ message: "view should have reloaded when switching scale to month",
+ });
+
+ await setScale(0);
+ await ganttControlsChanges();
+ expect(reloadCount).toBe(4, {
+ message: "view should have reloaded when switching scale to year",
+ });
+});
+
+test("view reload when period changes", async () => {
+ let reloadCount = 0;
+ onRpc("get_gantt_data", () => {
+ reloadCount++;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect(reloadCount).toBe(1, { message: "view should have loaded" });
+
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-02-28" });
+ expect(reloadCount).toBe(2);
+
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-01-31" });
+ expect(reloadCount).toBe(3);
+});
+
+test("unavailabilities should not be reloaded when period changes if display_unavailability is not set", async () => {
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step("get_gantt_data");
+ expect(kwargs.unavailability_fields).toEqual([]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect.verifySteps(["get_gantt_data"]);
+
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-02-28" });
+ expect.verifySteps(["get_gantt_data"]);
+
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-01-31" });
+ expect.verifySteps(["get_gantt_data"]);
+});
+
+test("close tooltip when drag pill", async () => {
+ Tasks._records[1].start = "2018-12-16 03:00:00";
+ Tasks._views = { form: "" };
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 1",
+ colSpan: "16 W51 2018 -> Out of bounds (33) ",
+ level: 0,
+ },
+ {
+ title: "Task 2",
+ colSpan: "16 W51 2018 -> 22 (1/2) W51 2018",
+ level: 1,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 W51 2018 -> 20 (1/2) W51 2018",
+ level: 2,
+ },
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) W51 2018 -> 20 W51 2018",
+ level: 2,
+ },
+ ],
+ },
+ ]);
+ // open popover
+ await contains(getPill("Task 4")).click();
+ expect(".o_popover").toHaveCount(1);
+
+ // enable the drag feature and move the pill
+ const { moveTo } = await dragPill("Task 4");
+ expect(".o_popover").toHaveCount(1, {
+ message: "popover should is still opened as the pill did not move yet",
+ });
+ await moveTo({ pill: "Task 2" });
+ // check popover
+ expect(".o_popover").toHaveCount(0, {
+ message: "popover should have been closed",
+ });
+});
+
+test("drag&drop on other pill in grouped view", async () => {
+ Tasks._records[0].start = "2018-12-16 05:00:00";
+ Tasks._records[0].stop = "2018-12-16 07:00:00";
+ Tasks._records[1].stop = "2018-12-17 13:00:00";
+ Tasks._views = { form: `` };
+
+ const def = new Deferred();
+ onRpc("write", () => def);
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ groupBy: ["project_id"],
+ });
+
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "16 W51 2018 -> 16 (1/2) W51 2018" },
+ { title: "Task 2", level: 0, colSpan: "17 (1/2) W51 2018 -> 17 W51 2018" },
+ { title: "Task 4", level: 0, colSpan: "20 W51 2018 -> 20 (1/2) W51 2018" },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [{ title: "Task 7", level: 0, colSpan: "20 (1/2) W51 2018 -> 20 W51 2018" }],
+ },
+ ]);
+ await contains(getPill("Task 2")).click();
+
+ expect(".o_popover").toHaveCount(1);
+
+ const { drop } = await dragPill("Task 2");
+ await drop({ pill: "Task 1" });
+
+ await contains(document.body).click(); // To simulate the full 'pointerup' sequence
+
+ def.resolve();
+ await animationFrame();
+
+ expect(".popover").toHaveCount(0);
+ expect(getGridContent().rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ { title: "Task 2", level: 0, colSpan: "16 W51 2018 -> 16 (1/2) W51 2018" },
+ { title: "Task 1", level: 1, colSpan: "16 W51 2018 -> 16 (1/2) W51 2018" },
+ { title: "Task 4", level: 0, colSpan: "20 W51 2018 -> 20 (1/2) W51 2018" },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [{ title: "Task 7", level: 0, colSpan: "20 (1/2) W51 2018 -> 20 W51 2018" }],
+ },
+ ]);
+});
+
+test("display mode button", async () => {
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.dense).toHaveCount(1);
+ expect(SELECTORS.sparse).toHaveCount(0);
+
+ const rowsInSparseMode = [
+ {
+ title: "Task 5",
+ },
+ {
+ title: "Task 1",
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ ],
+ },
+ {
+ title: "Task 2",
+ pills: [
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ ],
+ },
+ {
+ title: "Task 4",
+ pills: [
+ {
+ title: "Task 4",
+ level: 0,
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ },
+ ],
+ },
+ {
+ title: "Task 7",
+ pills: [
+ {
+ title: "Task 7",
+ level: 0,
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ },
+ ],
+ },
+ {
+ title: "Task 3",
+ pills: [
+ { title: "Task 3", level: 0, colSpan: "27 December 2018 -> 03 (1/2) January 2019" },
+ ],
+ },
+ ];
+
+ expect(getGridContent().rows).toEqual(rowsInSparseMode);
+
+ await click(SELECTORS.dense);
+ await animationFrame();
+ expect(SELECTORS.dense).toHaveCount(0);
+ expect(SELECTORS.sparse).toHaveCount(1);
+
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ title: "Task 1",
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 1,
+ },
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 2,
+ },
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 2,
+ },
+ {
+ title: "Task 3",
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+
+ await click(SELECTORS.sparse);
+ await animationFrame();
+ expect(SELECTORS.dense).toHaveCount(1);
+ expect(SELECTORS.sparse).toHaveCount(0);
+
+ expect(getGridContent().rows).toEqual(rowsInSparseMode);
+
+ expect.verifySteps([]);
+});
+
+test("unavailabilities fetched with right parameters", async () => {
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step(Object.values(pick(kwargs, "start_date", "stop_date", "scale")));
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00", "day"]]);
+ await setScale(4);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00", "week"]]);
+ await setScale(2);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00", "month"]]);
+ await setScale(0);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-11-30 23:00:00", "2018-12-31 23:00:00", "year"]]);
+ await selectGanttRange({ startDate: "2018-12-31", stopDate: "2019-06-15" });
+ expect.verifySteps([["2018-11-30 23:00:00", "2019-06-30 23:00:00", "year"]]);
+});
+
+test("progress bars fetched with the right start/stop dates", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step([kwargs.start_date, kwargs.stop_date]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00"]]);
+ await setScale(4);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00"]]);
+ await setScale(2);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-12-19 23:00:00", "2018-12-22 23:00:00"]]);
+ await setScale(0);
+ await ganttControlsChanges();
+ expect.verifySteps([["2018-11-30 23:00:00", "2018-12-31 23:00:00"]]);
+ await selectGanttRange({ startDate: "2018-12-31", stopDate: "2019-06-15" });
+ expect.verifySteps([["2018-11-30 23:00:00", "2019-06-30 23:00:00"]]);
+});
+
+test("focus today with scroll (in range & outside)", async () => {
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ expect(queryOne(".o_gantt_cell.o_gantt_today")).toBe(getCell("20 December 2018"));
+ let { columnHeaders } = getGridContent();
+ expect(columnHeaders).toHaveLength(34);
+ expect(columnHeaders[0].title).toBe("03"); // December
+ expect(columnHeaders.at(-1).title).toBe("05"); // January
+
+ await scroll(".o_content", { left: 800 });
+ await animationFrame();
+
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ columnHeaders = getGridContent().columnHeaders;
+ expect(columnHeaders).toHaveLength(34);
+ expect(columnHeaders[0].title).toBe("14"); // December
+ expect(columnHeaders.at(-1).title).toBe("16"); // January
+
+ await focusToday();
+ await ganttControlsChanges();
+
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ columnHeaders = getGridContent().columnHeaders;
+ expect(columnHeaders).toHaveLength(34);
+ expect(columnHeaders[0].title).toBe("03"); // December
+ expect(columnHeaders.at(-1).title).toBe("05"); // January
+
+ await scroll(".o_content", { left: 2000 });
+ await animationFrame();
+
+ expect(".o_gantt_cell.o_gantt_today").not.toBeVisible();
+ columnHeaders = getGridContent().columnHeaders;
+ expect(columnHeaders).toHaveLength(34);
+ expect(columnHeaders[0].title).toBe("07"); // January
+ expect(columnHeaders.at(-1).title).toBe("09"); // February
+
+ await focusToday();
+ await ganttControlsChanges();
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ columnHeaders = getGridContent().columnHeaders;
+ expect(columnHeaders).toHaveLength(34);
+ expect(columnHeaders[0].title).toBe("03"); // December
+ expect(columnHeaders.at(-1).title).toBe("05"); // January
+});
+
+test("focus today with range change (in range & outside)", async () => {
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ expect(queryOne(".o_gantt_cell.o_gantt_today")).toBe(getCell("20 December 2018"));
+ let gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(gridContent.columnHeaders).toHaveLength(34);
+ expect(gridContent.columnHeaders[0].title).toBe("03"); // December
+ expect(gridContent.columnHeaders.at(-1).title).toBe("05"); // January
+
+ await selectGanttRange({ startDate: "2018-11-15", stopDate: "2019-02-15" });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ expect(queryOne(".o_gantt_cell.o_gantt_today")).toBe(getCell("20 December 2018"));
+ gridContent = getGridContent();
+ expect(gridContent.range).toBe("From: 11/15/2018 to: 02/15/2019");
+ expect(gridContent.columnHeaders).toHaveLength(34);
+ expect(gridContent.columnHeaders[0].title).toBe("03"); // December
+ expect(gridContent.columnHeaders.at(-1).title).toBe("05"); // January
+ await focusToday();
+ await ganttControlsChanges();
+ // nothing happens
+
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-02-28" });
+ expect(getGridContent().range).toBe("From: 01/01/2019 to: 02/28/2019");
+ expect.verifySteps(["get_gantt_data"]);
+ expect(".o_gantt_cell.o_gantt_today").not.toBeVisible();
+
+ await focusToday();
+ await ganttControlsChanges();
+ expect.verifySteps(["get_gantt_data"]);
+ expect(".o_gantt_cell.o_gantt_today").toBeVisible();
+ expect(getGridContent().range).toBe("From: 11/21/2018 to: 01/17/2019");
+});
+
+test("set scale: should keep focused date", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ // set focus around 23 January 2019
+ await scroll(".o_content", { left: 2000 });
+ await animationFrame();
+ expect(getCell("23 January 2019")).toBeVisible();
+ // day view
+ await setScale(5);
+ await ganttControlsChanges();
+ expect(getCell("12pm 23 January 2019")).toBeVisible();
+ // week view
+ await setScale(4);
+ await ganttControlsChanges();
+ expect(getCell("23 W4 2019")).toBeVisible();
+ // year view
+ await setScale(0);
+ await ganttControlsChanges();
+ expect(getCell("January 2019")).toBeVisible();
+});
+
+test("set start/stop date: should keep focused date", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ // set focus around 23 January 2019
+ await scroll(".o_content", { left: 2000 });
+ await animationFrame();
+ await selectGanttRange({ startDate: "2018-12-01", stopDate: "2019-05-28" });
+ expect(getCell("23 January 2019")).toBeVisible();
+ await selectGanttRange({ startDate: "2019-01-22", stopDate: "2019-05-28" });
+ expect(getCell("23 January 2019")).toBeVisible();
+ await selectGanttRange({ startDate: "2018-12-01", stopDate: "2019-01-22" });
+ expect(getCell("22 January 2019")).toBeVisible();
+});
+
+test("focus first pill on row header click", async () => {
+ Tasks._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ start: "2018-11-30 23:00:00",
+ stop: "2018-12-01 23:00:00",
+ user_id: 1,
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ start: "2019-02-27 23:00:00",
+ stop: "2019-02-28 23:00:00",
+ user_id: 1,
+ },
+ ];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ // set focus around 23 January 2019
+ await scroll(".o_content", { left: 2000 });
+ await animationFrame();
+ expect(SELECTORS.pill).toHaveCount(0);
+
+ await click(SELECTORS.rowHeader);
+ await animationFrame();
+ expect(SELECTORS.pill).toHaveCount(1);
+ expect(SELECTORS.pill).toHaveText("Task 1");
+});
+
+test("Select a range via the range menu", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ let content = getGridContent();
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+
+ await selectRange("Today");
+ content = getGridContent();
+ expect(content.range).toBe("12/20/2018");
+
+ await selectRange("This week");
+ content = getGridContent();
+ expect(content.range).toBe("W51 2018");
+
+ await selectRange("This month");
+ content = getGridContent();
+ expect(content.range).toBe("December 2018");
+
+ await selectRange("This quarter");
+ content = getGridContent();
+ expect(content.range).toBe("Q4 2018");
+
+ await selectRange("This year");
+ content = getGridContent();
+ expect(content.range).toBe("2018");
+});
+
+test("Select range with left/rigth arrows", async () => {
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect.step(kwargs.domain);
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+ expect.verifySteps([
+ ["&", ["start", "<", "2018-12-31 23:00:00"], ["stop", ">", "2018-11-30 23:00:00"]],
+ ]);
+
+ let content = getGridContent();
+ expect(content.range).toBe("December 2018");
+
+ for (let i = 0; i < 3; i++) {
+ await click(SELECTORS.nextButton);
+ }
+ await click(SELECTORS.previousButton);
+ await ganttControlsChanges();
+
+ expect.verifySteps([
+ ["&", ["start", "<", "2019-02-28 23:00:00"], ["stop", ">", "2019-01-31 23:00:00"]],
+ ]);
+ content = getGridContent();
+ expect(content.range).toBe("February 2019");
+
+ await press("alt+n");
+ await ganttControlsChanges();
+ expect.verifySteps([
+ ["&", ["start", "<", "2019-03-31 23:00:00"], ["stop", ">", "2019-02-28 23:00:00"]],
+ ]);
+ content = getGridContent();
+ expect(content.range).toBe("March 2019");
+});
+
+test("Select scale with +/- buttons", async () => {
+ onRpc("get_gantt_data", () => {
+ expect.step("get_gantt_data");
+ });
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ });
+
+ expect(getActiveScale()).toBe(5);
+ expect(SELECTORS.minusButton).toBeEnabled();
+ expect(SELECTORS.plusButton).not.toBeEnabled();
+ expect.verifySteps(["get_gantt_data"]);
+
+ for (let i = 0; i < 9; i++) {
+ await click(SELECTORS.minusButton);
+ }
+ await ganttControlsChanges();
+
+ expect(getActiveScale()).toBe(0);
+ expect(SELECTORS.minusButton).not.toBeEnabled();
+ expect(SELECTORS.plusButton).toBeEnabled();
+ expect.verifySteps(["get_gantt_data"]);
+
+ await click(SELECTORS.plusButton);
+ await click(SELECTORS.plusButton);
+ await ganttControlsChanges();
+
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.minusButton).toBeEnabled();
+ expect(SELECTORS.plusButton).toBeEnabled();
+ expect.verifySteps(["get_gantt_data"]);
+
+ await press("alt+i");
+ await ganttControlsChanges();
+ expect(getActiveScale()).toBe(3);
+ expect(SELECTORS.minusButton).toBeEnabled();
+ expect(SELECTORS.plusButton).toBeEnabled();
+ expect.verifySteps(["get_gantt_data"]);
+});
+
+test("make tooltip visible for a long pill", async () => {
+ mockDate("2024-03-01 00:00:00");
+ Tasks._records.length = 1;
+ Tasks._records[0].start = "2024-01-16 00:00:00";
+ Tasks._records[0].stop = "2024-11-16 00:00:00";
+ await mountGanttView({
+ resModel: "tasks",
+ arch: '',
+ context: {
+ default_start_date: "2024-01-01",
+ default_stop_date: "2024-12-31",
+ },
+ });
+ const { left: pillLeft, right: pillRight } = getPill("Task 1").getBoundingClientRect();
+ expect(pillLeft).toBeLessThan(0);
+ expect(pillRight).toBeGreaterThan(window.innerWidth);
+ expect(".o_popover").toHaveCount(0);
+
+ await contains(getPill("Task 1")).click();
+ expect(".o_popover").toHaveCount(1);
+ const popover = queryOne(".o_popover");
+ const { left: popoverLeft, right: popoverRight } = popover.getBoundingClientRect();
+ expect(popoverLeft).toBeWithin(0, window.innerWidth);
+ expect(popoverRight).toBeWithin(0, window.innerWidth);
+});
+
+test("date fields: domain", async () => {
+ expect.assertions(4);
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ const domains = [
+ ["&", ["start", "<", "2018-12-21"], ["stop", ">=", "2018-12-20"]],
+ ["&", ["start", "<", "2018-12-23"], ["stop", ">=", "2018-12-16"]],
+ ["&", ["start", "<", "2019-01-01"], ["stop", ">=", "2018-01-01"]],
+ ["&", ["start", "<", "2019-01-01"], ["stop", ">=", "2018-12-01"]],
+ ];
+ onRpc("get_gantt_data", ({ kwargs }) => {
+ expect(kwargs.domain).toEqual(domains.pop());
+ });
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ await selectRange("This year");
+ await selectRange("This week");
+ await selectRange("Today");
+});
+
+test("date fields: pill columns", async () => {
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ Tasks._records = Tasks._records.slice(0, 1);
+ Tasks._records[0].start = "2018-12-20";
+ Tasks._records[0].stop = "2018-12-22";
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 22 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+});
+
+test.tags("desktop")("date fields: resize a pill", async () => {
+ expect.assertions(4);
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ Tasks._records = Tasks._records.slice(0, 1);
+ Tasks._records[0].start = "2018-12-20";
+ Tasks._records[0].stop = "2018-12-22";
+ onRpc("write", ({ args }) => {
+ expect(args[0]).toEqual([1]);
+ // initial dates -- start: '"2018-12-20"', stop: '"2018-12-22"'
+ expect(args[1]).toEqual({ stop: "2018-12-21" });
+ });
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 22 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+ await resizePill(getPillWrapper("Task 1"), "end", -1);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 21 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+});
+
+test("date fields: drag a pill", async () => {
+ expect.assertions(4);
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ Tasks._records = Tasks._records.slice(0, 1);
+ Tasks._records[0].start = "2018-12-20";
+ Tasks._records[0].stop = "2018-12-22";
+ onRpc("write", ({ args }) => {
+ expect(args[0]).toEqual([1]);
+ // initial dates -- start: '"2018-12-20"', stop: '"2018-12-22"'
+ expect(args[1]).toEqual({ start: "2018-12-19", stop: "2018-12-21" });
+ });
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 22 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+ const { drop } = await dragPill("Task 1");
+ await drop({ column: "19 December 2018", part: 1 });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "19 December 2018 -> 21 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+});
+
+test("date fields: popover", async () => {
+ expect.assertions(5);
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ Tasks._records = Tasks._records.slice(0, 1);
+ Tasks._records[0].start = "2018-12-20";
+ Tasks._records[0].stop = "2018-12-22";
+ const task1 = Tasks._records[0];
+ const startDateLocalString = deserializeDate(task1.start).toFormat("f");
+ const stopDateLocalString = deserializeDate(task1.stop).toFormat("f");
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 22 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ },
+ ]);
+ expect(".o_popover").toHaveCount(0);
+ await contains(SELECTORS.pill).click();
+ expect(".o_popover").toHaveCount(1);
+ expect(queryAllTexts(".o_popover .popover-body span")).toEqual([
+ "Task 1",
+ startDateLocalString,
+ stopDateLocalString,
+ ]);
+ await contains(".o_popover .popover-header i.fa.fa-close").click();
+ expect(".o_popover").toHaveCount(0);
+});
+
+test("date fields: dialog", async () => {
+ Tasks._fields.start = fields.Date();
+ Tasks._fields.stop = fields.Date();
+ Tasks._records = Tasks._records.slice(0, 1);
+ Tasks._records[0].start = "2018-12-20";
+ Tasks._records[0].stop = "2018-12-22";
+ Tasks._views = {
+ form: `
+
+ `,
+ };
+ await mountGanttView({
+ type: "gantt",
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(".modal").toHaveCount(0);
+ await editPill("Task 1");
+ // check that the dialog is opened with prefilled fields
+ expect(".modal").toHaveCount(1);
+ const modal = queryOne(".modal");
+ expect(modal.querySelector(".o_field_widget[name=start] input")).toHaveValue("12/20/2018");
+ expect(modal.querySelector(".o_field_widget[name=stop] input")).toHaveValue("12/22/2018");
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_concurrency.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_concurrency.test.js
new file mode 100644
index 000000000..3f99e9596
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_concurrency.test.js
@@ -0,0 +1,454 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { Deferred, animationFrame, mockDate } from "@odoo/hoot-mock";
+import { click } from "@odoo/hoot-dom";
+import { onPatched } from "@odoo/owl";
+import {
+ onRpc,
+ patchWithCleanup,
+ toggleMenuItem,
+ toggleSearchBarMenu,
+} from "@web/../tests/web_test_helpers";
+
+import { GanttRenderer } from "@web_gantt/gantt_renderer";
+import { Tasks, defineGanttModels } from "./gantt_mock_models";
+import {
+ SELECTORS,
+ editPill,
+ ganttControlsChanges,
+ getActiveScale,
+ getCellColorProperties,
+ getGridContent,
+ getPillWrapper,
+ mountGanttView,
+ resizePill,
+ selectGanttRange,
+ setScale,
+} from "./web_gantt_test_helpers";
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => mockDate("2018-12-20T08:00:00", +1));
+
+test("concurrent scale switches return in inverse order", async () => {
+ let model;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ model = this.model;
+ onPatched(() => {
+ expect.step("patched");
+ });
+ },
+ });
+
+ let firstReloadProm = null;
+ let reloadProm = null;
+ onRpc("get_gantt_data", () => reloadProm);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps(["patched"]);
+
+ let content = getGridContent();
+ expect(getActiveScale()).toBe(2);
+ expect(content.groupHeaders.map((gh) => gh.title)).toEqual(["December 2018", "January 2019"]);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(model.data.records).toHaveLength(6);
+
+ // switch to 'week' scale (this rpc will be delayed)
+ firstReloadProm = new Deferred();
+ reloadProm = firstReloadProm;
+ await setScale(4);
+ await ganttControlsChanges();
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(4);
+ expect(content.groupHeaders.map((gh) => gh.title)).toEqual(["December 2018", "January 2019"]);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(model.data.records).toHaveLength(6);
+
+ // switch to 'year' scale
+ reloadProm = null;
+ await setScale(0);
+ await ganttControlsChanges();
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(0);
+ expect(content.groupHeaders.map((gh) => gh.title)).toEqual(["2018", "2019"]);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(model.data.records).toHaveLength(6);
+
+ firstReloadProm.resolve();
+ await animationFrame();
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(0);
+ expect(content.groupHeaders.map((gh) => gh.title)).toEqual(["2018", "2019"]);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(model.data.records).toHaveLength(6);
+ expect.verifySteps(["patched"]);
+});
+
+test("concurrent scale switches return with gantt unavailabilities", async () => {
+ const unavailabilities = [
+ [{ start: "2018-12-10 23:00:00", stop: "2018-12-11 23:00:00" }],
+ [{ start: "2018-12-10 23:00:00", stop: "2018-12-11 23:00:00" }],
+ [
+ { start: "2018-07-30 23:00:00", stop: "2018-08-31 23:00:00" },
+ { start: "2018-12-10 23:00:00", stop: "2018-12-11 23:00:00" },
+ ],
+ [{ start: "2018-07-30 23:00:00", stop: "2018-08-31 23:00:00" }],
+ ];
+
+ let model;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ model = this.model;
+ onPatched(() => {
+ expect.step("patched");
+ });
+ },
+ });
+
+ let firstReloadProm = null;
+ let reloadProm = null;
+ onRpc("get_gantt_data", async ({ parent }) => {
+ const result = await parent();
+ result.unavailabilities.__default = { false: unavailabilities.shift() };
+ await reloadProm;
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect.verifySteps(["patched"]);
+
+ let content = getGridContent();
+ expect(getActiveScale()).toBe(2);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(content.groupHeaders.map((h) => h.title)).toEqual(["December 2018", "January 2019"]);
+ expect(model.data.records).toHaveLength(6);
+ expect(getCellColorProperties("08 December 2018")).toEqual([]);
+ expect(getCellColorProperties("11 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+
+ // switch to 'week' scale (this rpc will be delayed)
+ firstReloadProm = new Deferred();
+ reloadProm = firstReloadProm;
+ await setScale(4);
+ await ganttControlsChanges();
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(4);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(content.groupHeaders.map((h) => h.title)).toEqual(["December 2018", "January 2019"]);
+ expect(model.data.records).toHaveLength(6);
+ expect(getCellColorProperties("08 December 2018")).toEqual([]);
+ expect(getCellColorProperties("11 December 2018")).toEqual([
+ "--Gantt__DayOff-background-color",
+ ]);
+
+ // switch to 'year' scale
+ reloadProm = null;
+ await setScale(0);
+ await ganttControlsChanges();
+ expect.verifySteps(["patched"]);
+ await selectGanttRange({ startDate: "2018-01-01", stopDate: "2018-12-31" });
+ expect.verifySteps(["patched"]);
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(0);
+ expect(content.range).toBe("From: 01/01/2018 to: 12/31/2018");
+ expect(content.groupHeaders.map((h) => h.title)).toEqual(["2018"]);
+ expect(model.data.records).toHaveLength(7);
+ expect(getCellColorProperties("August 2018")).toEqual(["--Gantt__DayOff-background-color"]);
+ expect(getCellColorProperties("November 2018")).toEqual([]);
+
+ firstReloadProm.resolve();
+ await animationFrame();
+
+ content = getGridContent();
+ expect(getActiveScale()).toBe(0);
+ expect(content.range).toBe("From: 01/01/2018 to: 12/31/2018");
+ expect(content.groupHeaders.map((h) => h.title)).toEqual(["2018"]);
+ expect(model.data.records).toHaveLength(7);
+ expect(getCellColorProperties("August 2018")).toEqual(["--Gantt__DayOff-background-color"]);
+ expect(getCellColorProperties("November 2018")).toEqual([]);
+ expect.verifySteps([]);
+});
+
+test("concurrent range selections", async () => {
+ let reloadProm = null;
+ let firstReloadProm = null;
+ onRpc("get_gantt_data", () => reloadProm);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ let content = getGridContent();
+ expect(getActiveScale()).toBe(2);
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+
+ reloadProm = new Deferred();
+ firstReloadProm = reloadProm;
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-02-28" });
+ reloadProm = null;
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2019-01-31" });
+ firstReloadProm.resolve();
+ content = getGridContent();
+ expect(content.range).toBe("From: 01/01/2019 to: 01/31/2019");
+});
+
+test("concurrent pill resize and groupBy change", async () => {
+ let awaitWriteDef = false;
+ const writeDef = new Deferred();
+ onRpc(({ args, method }) => {
+ expect.step([method, args]);
+ if (method === "write" && awaitWriteDef) {
+ return writeDef;
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ searchViewArch: `
+
+
+
+ `,
+ domain: [["id", "in", [2, 5]]],
+ });
+ expect.verifySteps([
+ ["get_views", []],
+ ["get_gantt_data", []],
+ ]);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ level: 0,
+ title: "Task 5",
+ },
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ },
+ ]);
+
+ // resize "Task 2" to 1 cell smaller (-1 day) ; this RPC will be delayed
+ awaitWriteDef = true;
+ await resizePill(getPillWrapper("Task 2"), "end", -1);
+
+ expect.verifySteps([["write", [[2], { stop: "2018-12-21 06:29:59" }]]]);
+
+ await toggleSearchBarMenu();
+ await toggleMenuItem("Project");
+ expect.verifySteps([["get_gantt_data", []]]);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Project 1",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ level: 0,
+ title: "Task 5",
+ },
+ ],
+ title: "Project 2",
+ },
+ ]);
+
+ writeDef.resolve();
+ await animationFrame();
+ expect.verifySteps([["get_gantt_data", []]]);
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 21 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Project 1",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ level: 0,
+ title: "Task 5",
+ },
+ ],
+ title: "Project 2",
+ },
+ ]);
+});
+
+test("concurrent pill resizes return in inverse order", async () => {
+ let awaitWriteDef = false;
+ const writeDef = new Deferred();
+ onRpc(({ args, method }) => {
+ expect.step([method, args]);
+ if (method === "write" && awaitWriteDef) {
+ return writeDef;
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 2]],
+ });
+
+ // resize to 1 cell smaller (-1 day) ; this RPC will be delayed
+ awaitWriteDef = true;
+ await resizePill(getPillWrapper("Task 2"), "end", -1);
+
+ // resize to two cells larger (+2 days) ; no delay
+ awaitWriteDef = false;
+ await resizePill(getPillWrapper("Task 2"), "end", +2);
+
+ writeDef.resolve();
+ await animationFrame();
+
+ expect.verifySteps([
+ ["get_views", []],
+ ["get_gantt_data", []],
+ ["write", [[2], { stop: "2018-12-21 06:29:59" }]],
+ ["get_gantt_data", []],
+ ["write", [[2], { stop: "2018-12-24 06:29:59" }]],
+ ["get_gantt_data", []],
+ ]);
+});
+
+test("concurrent pill resizes and open, dialog show updated number", async () => {
+ Tasks._views = {
+ form: `
+
+ `,
+ };
+
+ const def = new Deferred();
+ onRpc("write", () => def);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 2]],
+ });
+
+ await resizePill(getPillWrapper("Task 2"), "end", +2);
+ await editPill("Task 2");
+
+ def.resolve();
+ await animationFrame();
+ expect(`.modal [name=stop] input`).toHaveValue("12/24/2018 07:29:59");
+});
+
+test("concurrent display mode change and fetch", async () => {
+ let def;
+ onRpc("get_gantt_data", () => def);
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "in", [1, 2]]],
+ });
+
+ let content = getGridContent();
+ expect(content.range).toBe("From: 12/01/2018 to: 02/28/2019");
+ const initialRows = [
+ {
+ pills: [
+ { title: "Task 1", level: 0, colSpan: "Out of bounds (1) -> 31 December 2018" },
+ {
+ title: "Task 2",
+ level: 1,
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ },
+ ],
+ },
+ ];
+ expect(content.rows).toEqual(initialRows);
+
+ def = new Deferred();
+ await selectGanttRange({ startDate: "2018-12-01", stopDate: "2019-06-15" });
+ content = getGridContent();
+ expect(content.range).toBe("From: 12/01/2018 to: 06/15/2019");
+ expect(content.rows).toEqual(initialRows);
+
+ await click(SELECTORS.sparse);
+ await animationFrame();
+ content = getGridContent();
+ expect(content.range).toBe("From: 12/01/2018 to: 06/15/2019");
+ expect(content.rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Task 1",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Task 2",
+ },
+ ]);
+
+ def.resolve();
+ await animationFrame();
+ content = getGridContent();
+ expect(content.range).toBe("From: 12/01/2018 to: 06/15/2019");
+ expect(content.rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Task 1",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Task 2",
+ },
+ ]);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_manual.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_manual.test.js
new file mode 100644
index 000000000..73b84fee6
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_manual.test.js
@@ -0,0 +1,102 @@
+import { beforeEach, expect, test } from "@odoo/hoot";
+import { queryFirst } from "@odoo/hoot-dom";
+import { mockDate } from "@odoo/hoot-mock";
+import { mountGanttView } from "./web_gantt_test_helpers";
+import { ResUsers, TASKS_STAGE_SELECTION, Tasks, defineGanttModels } from "./gantt_mock_models";
+
+function randomName(length) {
+ const CHARS = "abcdefghijklmnopqrstuvwxyzàùéèâîûêôäïüëö";
+ return [...Array(length)]
+ .map(() => {
+ const char = CHARS[Math.floor(Math.random() * CHARS.length)];
+ return Math.random() < 0.5 ? char : char.toUpperCase();
+ })
+ .join("");
+}
+
+defineGanttModels();
+beforeEach(() => mockDate("2018-12-20T08:00:00", +1));
+
+test.tags("manual testing").skip("large amount of records (ungrouped)", async () => {
+ const NB_TASKS = 10000;
+
+ Tasks._records = [...Array(NB_TASKS)].map((_, i) => ({
+ id: i + 1,
+ name: `Task ${i + 1}`,
+ start: `2018-12-01 00:00:00`,
+ stop: `2018-12-01 23:00:00`,
+ }));
+
+ console.time("makeView");
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ console.timeEnd("makeView");
+ expect(1).toBe(1);
+});
+
+test.tags("manual testing").skip("large amount of records (one level grouped)", async () => {
+ const NB_USERS = 10000;
+ const NB_TASKS = 10000;
+
+ ResUsers._records = [...Array(NB_USERS)].map((_, i) => ({
+ id: i + 1,
+ name: `${randomName(Math.floor(Math.random() * 8) + 8)} (${i + 1})`,
+ }));
+ Tasks._records = [...Array(NB_TASKS)].map((_, i) => {
+ let day1 = (i % 30) + 1;
+ let day2 = (i % 30) + 2;
+ if (day1 < 10) {
+ day1 = "0" + day1;
+ }
+ if (day2 < 10) {
+ day2 = "0" + day2;
+ }
+ return {
+ id: i + 1,
+ name: `Task ${i + 1}`,
+ user_id: Math.floor(Math.random() * Math.floor(NB_USERS)) + 1,
+ start: `2018-12-${day1} 00:00:00`,
+ stop: `2018-12-${day2} 00:00:00`,
+ };
+ });
+
+ console.time("makeView");
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id"],
+ });
+ console.timeEnd("makeView");
+
+ queryFirst(".o_content").style = "max-height: 600px; overflow-y: scroll;";
+ expect(1).toBe(1);
+});
+
+test.tags("manual testing").skip("large amount of records (two level grouped)", async () => {
+ const NB_USERS = 100;
+ const NB_TASKS = 10000;
+
+ ResUsers._records = [...Array(NB_USERS)].map((_, i) => ({
+ id: i + 1,
+ name: `${randomName(Math.floor(Math.random() * 8) + 8)} (${i + 1})`,
+ }));
+ Tasks._records = [...Array(NB_TASKS)].map((_, i) => ({
+ id: i + 1,
+ name: `Task ${i + 1}`,
+ stage: TASKS_STAGE_SELECTION[i % 2][0],
+ user_id: (i % NB_USERS) + 1,
+ start: "2018-12-01 00:00:00",
+ stop: "2018-12-02 00:00:00",
+ }));
+
+ console.time("makeView");
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id", "stage"],
+ });
+ console.timeEnd("makeView");
+ expect(1).toBe(1);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_mobile.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_mobile.test.js
new file mode 100644
index 000000000..2744d3c0a
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_mobile.test.js
@@ -0,0 +1,378 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
+import { animationFrame, mockDate } from "@odoo/hoot-mock";
+import { contains, getService, mountWithCleanup, onRpc } from "@web/../tests/web_test_helpers";
+import { Domain } from "@web/core/domain";
+import { deserializeDateTime } from "@web/core/l10n/dates";
+import { WebClient } from "@web/webclient/webclient";
+import { Tasks, defineGanttModels } from "./gantt_mock_models";
+import {
+ CLASSES,
+ SELECTORS,
+ getActiveScale,
+ getGridContent,
+ mountGanttView,
+} from "./web_gantt_test_helpers";
+
+defineGanttModels();
+
+describe.current.tags("mobile");
+
+beforeEach(() => mockDate("2018-12-20T08:00:00", +1));
+
+test("empty ungrouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 0]],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe(null);
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(10);
+ expect(columnHeaders.at(0).title).toBe("15");
+ expect(columnHeaders.at(-1).title).toBe("24");
+ expect(rows).toEqual([{}]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("ungrouped gantt rendering", async () => {
+ const task2 = Tasks._records[1];
+ const startDateLocalString = deserializeDateTime(task2.start).toFormat("f");
+ const stopDateLocalString = deserializeDateTime(task2.stop).toFormat("f");
+ Tasks._views.gantt = ``;
+ Tasks._views.search = ``;
+
+ onRpc("get_gantt_data", ({ model }) => expect.step(model));
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [[false, "gantt"]],
+ });
+ expect.verifySteps(["tasks"]);
+ await animationFrame();
+
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe(null);
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(10);
+ expect(columnHeaders.at(0).title).toBe("15");
+ expect(columnHeaders.at(-1).title).toBe("24");
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+ expect(rows).toEqual([
+ {
+ pills: [
+ { title: "Task 1", level: 1, colSpan: "Out of bounds (1) -> Out of bounds (63) " },
+ {
+ title: "Task 2",
+ level: 0,
+ colSpan: "17 (1/2) Dec 2018 -> 22 (1/2) Dec 2018",
+ },
+ {
+ title: "Task 4",
+ level: 2,
+ colSpan: "20 Dec 2018 -> 20 (1/2) Dec 2018",
+ },
+ {
+ title: "Task 7",
+ level: 2,
+ colSpan: "20 (1/2) Dec 2018 -> 20 Dec 2018",
+ },
+ ],
+ },
+ ]);
+
+ // test popover and local timezone
+ expect(`.o_popover`).toHaveCount(0);
+ const task2Pill = queryAll(SELECTORS.pill)[1];
+ expect(task2Pill).toHaveText("Task 2");
+
+ await contains(task2Pill).click();
+ expect(`.o_popover`).toHaveCount(1);
+ expect(queryAllTexts`.o_popover .popover-body span`).toEqual([
+ "Task 2",
+ startDateLocalString,
+ stopDateLocalString,
+ ]);
+
+ await contains(`.o_popover .popover-header i.fa.fa-close`).click();
+ expect(`.o_popover`).toHaveCount(0);
+});
+
+test("ordered gantt view", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["stage_id"],
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(10);
+ expect(columnHeaders.at(0).title).toBe("16");
+ expect(columnHeaders.at(-1).title).toBe("25");
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+ expect(rows).toEqual([
+ {
+ title: "todo",
+ },
+ {
+ title: "in_progress",
+ pills: [
+ { level: 0, colSpan: "Out of bounds (1) -> Out of bounds (63) ", title: "Task 1" },
+ {
+ level: 1,
+ colSpan: "20 (1/2) Dec 2018 -> 20 Dec 2018",
+ title: "Task 7",
+ },
+ ],
+ },
+ {
+ title: "done",
+ pills: [
+ {
+ level: 0,
+ colSpan: "17 (1/2) Dec 2018 -> 22 (1/2) Dec 2018",
+ title: "Task 2",
+ },
+ ],
+ },
+ {
+ title: "cancel",
+ pills: [
+ {
+ level: 0,
+ colSpan: "20 Dec 2018 -> 20 (1/2) Dec 2018",
+ title: "Task 4",
+ },
+ ],
+ },
+ ]);
+});
+
+test("empty single-level grouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ domain: Domain.FALSE.toList(),
+ });
+ const { viewTitle, range, columnHeaders, rows } = getGridContent();
+ expect(viewTitle).toBe("Gantt View");
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(columnHeaders).toHaveLength(10);
+ expect(columnHeaders.at(0).title).toBe("16");
+ expect(columnHeaders.at(-1).title).toBe("25");
+ expect(rows).toEqual([{ title: "" }]);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+});
+
+test("single-level grouped gantt rendering", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ expect(getActiveScale()).toBe(2);
+ expect(SELECTORS.expandCollapseButtons).not.toBeVisible();
+
+ const { range, viewTitle, columnHeaders, rows } = getGridContent();
+ expect(range).toBe("From: 12/01/2018 to: 02/28/2019");
+ expect(viewTitle).toBe("Tasks");
+ expect(columnHeaders).toHaveLength(10);
+ expect(columnHeaders.at(0).title).toBe("16");
+ expect(columnHeaders.at(-1).title).toBe("25");
+ expect(rows).toEqual([
+ {
+ title: "Project 1",
+ pills: [
+ {
+ title: "Task 1",
+ colSpan: "Out of bounds (1) -> Out of bounds (63) ",
+ level: 0,
+ },
+ {
+ title: "Task 2",
+ colSpan: "17 (1/2) Dec 2018 -> 22 (1/2) Dec 2018",
+ level: 1,
+ },
+ {
+ title: "Task 4",
+ colSpan: "20 Dec 2018 -> 20 (1/2) Dec 2018",
+ level: 2,
+ },
+ ],
+ },
+ {
+ title: "Project 2",
+ pills: [
+ {
+ title: "Task 7",
+ colSpan: "20 (1/2) Dec 2018 -> 20 Dec 2018",
+ level: 0,
+ },
+ ],
+ },
+ ]);
+});
+
+test("Controls: rendering is mobile friendly", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+
+ // check toolbar's dropdown
+ await contains("button.dropdown-toggle").click();
+ expect(queryAllTexts`.o-dropdown-item`).toEqual(["Activate sparse mode"]);
+
+ // check that pickers open in dialog
+ await contains(SELECTORS.rangeMenuToggler).click();
+ expect(".modal").toHaveCount(0);
+ await contains(SELECTORS.startDatePicker).click();
+ expect(".modal").toHaveCount(1);
+ expect(".modal-title").toHaveText("Gantt start date");
+ expect(".modal-body .o_datetime_picker").toHaveCount(1);
+ await contains(".modal-header .btn").click();
+ expect(".modal").toHaveCount(0);
+ await contains(SELECTORS.stopDatePicker).click();
+ expect(".modal").toHaveCount(1);
+ expect(".modal-title").toHaveText("Gantt stop date");
+ expect(".modal-body .o_datetime_picker").toHaveCount(1);
+ await contains(".modal-header .btn").click();
+ expect(".modal").toHaveCount(0);
+});
+
+test("Progressbar: check the progressbar percentage visibility.", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+
+ expect(SELECTORS.progressBar).toHaveCount(2);
+ const [progressBar1, progressBar2] = queryAll(SELECTORS.progressBar);
+ expect(progressBar1).toHaveClass("o_gantt_group_success");
+ expect(progressBar2).toHaveClass("o_gantt_group_success");
+ const [rowHeader1, rowHeader2] = [progressBar1.parentElement, progressBar2.parentElement];
+ expect(rowHeader1.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader2.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader1).not.toHaveClass(CLASSES.group);
+ expect(rowHeader2).not.toHaveClass(CLASSES.group);
+ expect(queryAll(SELECTORS.progressBarBackground).map((el) => el.style.width)).toEqual([
+ "50%",
+ "12.5%",
+ ]);
+ expect(SELECTORS.progressBarForeground).toHaveCount(2);
+ expect(queryAllTexts(SELECTORS.progressBarForeground)).toEqual(["50h / 100h", "25h / 200h"]);
+
+ // Check the style of one of the progress bars
+ expect(rowHeader1.children).toHaveLength(2);
+ const rowTitle1 = rowHeader1.children[0];
+ expect(rowTitle1.matches(SELECTORS.rowTitle)).toBe(true);
+ expect(rowTitle1.nextElementSibling).toBe(progressBar1);
+
+ expect(rowHeader1).toHaveStyle({ gridTemplateRows: "36px 35px" });
+ expect(rowTitle1).toHaveStyle({ height: "36px" });
+ expect(progressBar1).toHaveStyle({ height: "35px" });
+});
+
+test("Progressbar: grouped row", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ expect.step("get_gantt_data");
+ const result = await parent();
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+
+ expect(SELECTORS.progressBar).toHaveCount(4);
+ const [progressBar1, progressBar2] = queryAll(SELECTORS.progressBar);
+ expect(progressBar1).toHaveClass("o_gantt_group_success");
+ expect(progressBar2).toHaveClass("o_gantt_group_success");
+ const [rowHeader1, rowHeader2] = [progressBar1.parentElement, progressBar2.parentElement];
+ expect(rowHeader1.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader2.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader1).toHaveClass(CLASSES.group);
+ expect(rowHeader2).not.toHaveClass(CLASSES.group);
+ expect(queryAll(SELECTORS.progressBarBackground).map((el) => el.style.width)).toEqual([
+ "50%",
+ "50%",
+ "12.5%",
+ "12.5%",
+ ]);
+ expect(SELECTORS.progressBarForeground).toHaveCount(4);
+ expect(queryAllTexts(SELECTORS.progressBarForeground)).toEqual([
+ "50h / 100h",
+ "50h / 100h",
+ "25h / 200h",
+ "25h / 200h",
+ ]);
+
+ // Check the style of one of the progress bars
+ expect(rowHeader1.children).toHaveLength(2);
+ const rowTitle1 = rowHeader1.children[0];
+ expect(rowTitle1.matches(SELECTORS.rowTitle)).toBe(true);
+ expect(rowTitle1.nextElementSibling).toBe(progressBar1);
+
+ expect(rowHeader1).toHaveStyle({ gridTemplateRows: "24px 35px" });
+ expect(rowTitle1).toHaveStyle({ height: "24px" });
+ expect(progressBar1).toHaveStyle({ height: "35px" });
+});
+
+test("horizontal scroll applies to the content [SMALL SCREEN]", async () => {
+ Tasks._views.search = ``;
+ Tasks._views.gantt = ``;
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [[false, "gantt"]],
+ });
+ await animationFrame();
+
+ const o_view_controller = queryFirst(".o_view_controller");
+ const o_content = queryFirst(".o_content");
+ const firstColumnHeader = queryFirst(SELECTORS.columnHeader);
+ const initialXHeaderCell = firstColumnHeader.getBoundingClientRect().x;
+
+ expect(o_view_controller).toHaveClass("o_action_delegate_scroll");
+ expect(o_view_controller).toHaveStyle({ overflow: "hidden" });
+ expect(o_content).toHaveStyle({ overflow: "auto" });
+ expect(o_content).toHaveProperty("scrollLeft", 762);
+
+ // Horizontal scroll
+ const newScrollLeft = o_content.scrollLeft - 50;
+ await contains(".o_content").scroll({ left: newScrollLeft });
+
+ expect(o_content).toHaveProperty("scrollLeft", newScrollLeft);
+ expect(firstColumnHeader.getBoundingClientRect().x).toBe(initialXHeaderCell + 50);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_other.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_other.test.js
new file mode 100644
index 000000000..7ecce3b37
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_other.test.js
@@ -0,0 +1,1777 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { queryAll, queryAllTexts, queryFirst } from "@odoo/hoot-dom";
+import { animationFrame, mockDate, mockTimeZone } from "@odoo/hoot-mock";
+import { onRendered, useEffect, useRef } from "@odoo/owl";
+import {
+ contains,
+ defineParams,
+ fields,
+ getService,
+ mountWithCleanup,
+ onRpc,
+ pagerNext,
+ patchWithCleanup,
+ toggleMenuItem,
+ toggleSearchBarMenu,
+} from "@web/../tests/web_test_helpers";
+import { Tasks, defineGanttModels } from "./gantt_mock_models";
+import {
+ CLASSES,
+ SELECTORS,
+ clickCell,
+ dragPill,
+ editPill,
+ ganttControlsChanges,
+ getGridContent,
+ hoverGridCell,
+ mountGanttView,
+ selectGanttRange,
+ setScale,
+} from "./web_gantt_test_helpers";
+
+import { Domain } from "@web/core/domain";
+import { WebClient } from "@web/webclient/webclient";
+import { GanttController } from "@web_gantt/gantt_controller";
+import { GanttRenderer } from "@web_gantt/gantt_renderer";
+import { GanttRowProgressBar } from "@web_gantt/gantt_row_progress_bar";
+
+// Hard-coded daylight saving dates from 2019
+const DST_DATES = {
+ winterToSummer: {
+ before: "2019-03-30",
+ after: "2019-03-31",
+ },
+ summerToWinter: {
+ before: "2019-10-26",
+ after: "2019-10-27",
+ },
+};
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => {
+ mockDate("2018-12-20T08:00:00", +1);
+ defineParams({
+ lang_parameters: {
+ time_format: "%I:%M:%S",
+ },
+ });
+});
+
+test("DST spring forward", async () => {
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [
+ {
+ id: 1,
+ name: "DST Task 1",
+ start: `${DST_DATES.winterToSummer.before} 03:00:00`,
+ stop: `${DST_DATES.winterToSummer.before} 03:30:00`,
+ },
+ {
+ id: 2,
+ name: "DST Task 2",
+ start: `${DST_DATES.winterToSummer.after} 03:00:00`,
+ stop: `${DST_DATES.winterToSummer.after} 03:30:00`,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ context: {
+ initialDate: `${DST_DATES.winterToSummer.before} 08:00:00`,
+ },
+ });
+
+ const { columnHeaders, rows } = getGridContent();
+ expect(columnHeaders.slice(0, 4).map((h) => h.title)).toEqual(["12am", "1am", "2am", "3am"]);
+ expect(columnHeaders.slice(24, 28).map((h) => h.title)).toEqual(["12am", "1am", "3am", "4am"]);
+ expect(rows[0].pills).toEqual([
+ {
+ colSpan: "4am 30 March 2019 -> 4am 30 March 2019",
+ level: 0,
+ title: "DST Task 1",
+ },
+ {
+ colSpan: "5am 31 March 2019 -> 5am 31 March 2019",
+ level: 0,
+ title: "DST Task 2",
+ },
+ ]);
+});
+
+test("DST fall back", async () => {
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [
+ {
+ id: 1,
+ name: "DST Task 1",
+ start: `${DST_DATES.summerToWinter.before} 03:00:00`,
+ stop: `${DST_DATES.summerToWinter.before} 03:30:00`,
+ },
+ {
+ id: 2,
+ name: "DST Task 2",
+ start: `${DST_DATES.summerToWinter.after} 03:00:00`,
+ stop: `${DST_DATES.summerToWinter.after} 03:30:00`,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ context: {
+ initialDate: `${DST_DATES.summerToWinter.before} 08:00:00`,
+ },
+ });
+
+ const { columnHeaders, rows } = getGridContent();
+ expect(columnHeaders.slice(0, 4).map((h) => h.title)).toEqual(["12am", "1am", "2am", "3am"]);
+ expect(columnHeaders.slice(24, 28).map((h) => h.title)).toEqual(["12am", "1am", "2am", "2am"]);
+ expect(rows[0].pills).toEqual([
+ {
+ colSpan: "5am 26 October 2019 -> 5am 26 October 2019",
+ level: 0,
+ title: "DST Task 1",
+ },
+ {
+ colSpan: "4am 27 October 2019 -> 4am 27 October 2019",
+ level: 0,
+ title: "DST Task 2",
+ },
+ ]);
+});
+
+test("Records spanning across DST should be displayed normally", async () => {
+ mockTimeZone("Europe/Brussels");
+
+ Tasks._records = [
+ {
+ id: 1,
+ name: "DST Task 1",
+ start: `${DST_DATES.winterToSummer.before} 03:00:00`,
+ stop: `${DST_DATES.winterToSummer.after} 03:30:00`,
+ },
+ {
+ id: 2,
+ name: "DST Task 2",
+ start: `${DST_DATES.summerToWinter.before} 03:00:00`,
+ stop: `${DST_DATES.summerToWinter.after} 03:30:00`,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ context: {
+ initialDate: `${DST_DATES.summerToWinter.before} 08:00:00`,
+ },
+ });
+ expect(getGridContent().rows).toEqual([
+ {
+ pills: [
+ { title: "DST Task 1", colSpan: "March 2019 -> March 2019", level: 0 },
+ { title: "DST Task 2", colSpan: "October 2019 -> October 2019", level: 0 },
+ ],
+ },
+ ]);
+});
+
+test("delete attribute on dialog", async () => {
+ Tasks._views.form = `
+
+ `;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ await editPill("Task 1");
+ expect(".modal").toHaveCount(1);
+ expect(".o_form_button_remove").toHaveCount(0);
+});
+
+test("move a pill in multi-level group row after collapse and expand grouped row", async () => {
+ onRpc("write", ({ args }) => {
+ expect.step("write");
+ expect(args).toEqual([
+ [7],
+ {
+ project_id: 1,
+ start: "2018-12-11 12:30:12",
+ stop: "2018-12-11 18:29:59",
+ },
+ ]);
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id", "stage"],
+ domain: [["id", "in", [1, 7]]],
+ });
+ expect(getGridContent().rows).toHaveLength(4);
+
+ // collapse the first group
+ await contains(`${SELECTORS.rowHeader}${SELECTORS.group}:nth-child(1)`).click();
+ expect(`${SELECTORS.rowHeader}:nth-child(1)`).not.toHaveClass("o_group_open");
+ // expand the first group
+ await contains(`${SELECTORS.rowHeader}${SELECTORS.group}:nth-child(1)`).click();
+ expect(`${SELECTORS.rowHeader}:nth-child(1)`).toHaveClass("o_group_open");
+
+ // move a pill (task 7) in the other row and in the day 2
+ const { drop } = await dragPill("Task 7");
+ await drop({ column: "11 December 2018", part: 2 });
+ expect.verifySteps(["write"]);
+ expect(getGridContent().rows.filter((x) => x.isGroup)).toHaveLength(1);
+});
+
+test("plan dialog initial domain has the action domain as its only base", async () => {
+ Tasks._views = {
+ gantt: ``,
+ list: `
`,
+ search: `
+
+
+
+ `,
+ };
+ onRpc("get_gantt_data", ({ kwargs }) => expect.step(kwargs.domain.toString()));
+ onRpc("web_search_read", ({ kwargs }) => expect.step(kwargs.domain.toString()));
+ await mountWithCleanup(WebClient);
+ const ganttAction = {
+ name: "Tasks Gantt",
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [[false, "gantt"]],
+ };
+
+ // Load action without domain and open plan dialog
+ await getService("action").doAction(ganttAction);
+ await animationFrame();
+
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect.verifySteps(["|,start,=,false,stop,=,false"]);
+
+ // Load action WITH domain and open plan dialog
+ await getService("action").doAction({
+ ...ganttAction,
+ domain: [["project_id", "=", 1]],
+ });
+ expect.verifySteps([
+ "&,project_id,=,1,&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00",
+ ]);
+
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect.verifySteps(["&,project_id,=,1,|,start,=,false,stop,=,false"]);
+
+ // Load action without domain, activate a filter and then open plan dialog
+ await getService("action").doAction(ganttAction);
+ expect.verifySteps(["&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00"]);
+
+ await toggleSearchBarMenu();
+ await toggleMenuItem("Project 1");
+ expect.verifySteps([
+ "&,project_id,=,1,&,start,<,2019-02-28 23:00:00,stop,>,2018-11-30 23:00:00",
+ ]);
+
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect.verifySteps(["|,start,=,false,stop,=,false"]);
+});
+
+test("No progress bar when no option set.", async () => {
+ onRpc("gantt_progress_bar", () => {
+ throw new Error("Method should not be called");
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(SELECTORS.progressBar).toHaveCount(0);
+});
+
+test("Progress bar rpc is triggered when option set.", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.progressBar).toHaveCount(2);
+ const [progressBar1, progressBar2] = queryAll(SELECTORS.progressBar);
+ expect(progressBar1).toHaveClass("o_gantt_group_success");
+ expect(progressBar2).toHaveClass("o_gantt_group_success");
+ const [rowHeader1, rowHeader2] = [progressBar1.parentElement, progressBar2.parentElement];
+ expect(rowHeader1.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader2.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader1).not.toHaveClass(CLASSES.group);
+ expect(rowHeader2).not.toHaveClass(CLASSES.group);
+ expect(queryAll(SELECTORS.progressBarBackground).map((el) => el.style.width)).toEqual([
+ "50%",
+ "12.5%",
+ ]);
+ await hoverGridCell("16 W51 2018");
+ expect(SELECTORS.progressBarForeground).toHaveText("50h / 100h");
+ await hoverGridCell("16 W51 2018", "User 2");
+ expect(SELECTORS.progressBarForeground).toHaveText("25h / 200h");
+});
+
+test("Progress bar component will not render when hovering cells of the same row", async () => {
+ patchWithCleanup(GanttRowProgressBar.prototype, {
+ setup() {
+ onRendered(() => expect.step("rendering progress bar"));
+ },
+ });
+ onRpc("get_gantt_data", async ({ parent }) => {
+ const result = await parent();
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["rendering progress bar", "rendering progress bar"]);
+ await hoverGridCell("19 W51 2018");
+ expect.verifySteps(["rendering progress bar", "rendering progress bar"]);
+ await hoverGridCell("18 W51 2018");
+ await hoverGridCell("18 W51 2018", "User 2");
+ expect.verifySteps(["rendering progress bar", "rendering progress bar"]);
+});
+
+test("Progress bar when multilevel grouped.", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 100 },
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.progressBar).toHaveCount(4);
+ const [progressBar1, progressBar2] = queryAll(SELECTORS.progressBar);
+ expect(progressBar1).toHaveClass("o_gantt_group_success");
+ expect(progressBar2).toHaveClass("o_gantt_group_success");
+ const [rowHeader1, rowHeader2] = [progressBar1.parentElement, progressBar2.parentElement];
+ expect(rowHeader1.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader2.matches(SELECTORS.rowHeader)).toBe(true);
+ expect(rowHeader1).toHaveClass(CLASSES.group);
+ expect(rowHeader2).not.toHaveClass(CLASSES.group);
+ expect(queryAll(SELECTORS.progressBarBackground).map((el) => el.style.width)).toEqual([
+ "50%",
+ "50%",
+ "12.5%",
+ "12.5%",
+ ]);
+ await hoverGridCell("16 W51 2018");
+ expect(SELECTORS.progressBarForeground).toHaveText("50h / 100h");
+ await hoverGridCell("16 W51 2018", "User 2");
+ expect(SELECTORS.progressBarForeground).toHaveText("25h / 200h");
+});
+
+test("Progress bar warning when max_value is zero", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 50, max_value: 0 },
+ warning: "plop",
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.progressBarWarning).toHaveCount(0);
+ await hoverGridCell("16 W51 2018");
+ expect(SELECTORS.progressBarWarning).toHaveCount(1);
+ expect(queryFirst(SELECTORS.progressBarWarning).parentElement).toHaveText("50h");
+ expect(queryFirst(SELECTORS.progressBarWarning).parentElement).toHaveProperty("title", "plop");
+});
+
+test("Progress bar when value less than hour", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 0.5, max_value: 100 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.progressBar).toHaveCount(1);
+ await hoverGridCell("16 W51 2018");
+ expect(SELECTORS.progressBarForeground).toHaveText("0h30 / 100h");
+});
+
+test("Progress bar danger when ratio > 100", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 1: { value: 150, max_value: 100 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ expect(SELECTORS.progressBar).toHaveCount(1);
+ expect(SELECTORS.progressBarBackground).toHaveStyle("100%");
+ expect(SELECTORS.progressBar).toHaveClass("o_gantt_group_danger");
+ await hoverGridCell("16 W51 2018");
+ expect(queryFirst(SELECTORS.progressBarForeground).parentElement).toHaveClass("text-bg-danger");
+ expect(SELECTORS.progressBarForeground).toHaveText("150h / 100h");
+});
+
+test("Falsy search field will return an empty rows", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ groupBy: ["project_id", "user_id"],
+ domain: [["id", "=", 5]],
+ });
+ expect(".o_gantt_row_sidebar_empty").toHaveCount(1);
+ expect(SELECTORS.progressBar).toHaveCount(0);
+});
+
+test("Search field return rows with progressbar", async () => {
+ onRpc("get_gantt_data", async ({ kwargs, parent }) => {
+ const result = await parent();
+ expect.step("get_gantt_data");
+ expect(kwargs.progress_bar_fields).toEqual(["user_id"]);
+ result.progress_bars.user_id = {
+ 2: { value: 25, max_value: 200 },
+ };
+ return result;
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ groupBy: ["project_id", "user_id"],
+ domain: [["id", "=", 2]],
+ });
+ expect.verifySteps(["get_gantt_data"]);
+ const { rows } = getGridContent();
+ expect(rows.map((r) => r.title)).toEqual(["Project 1", "User 2"]);
+ expect(SELECTORS.progressBar).toHaveCount(1);
+ expect(SELECTORS.progressBarBackground).toHaveStyle("12.5%");
+});
+
+test("add record in empty gantt", async () => {
+ Tasks._records = [];
+ Tasks._fields.stage_id.domain = "[('id', '!=', False)]";
+ Tasks._views.form = `
+
+ `;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ });
+ await hoverGridCell("10 December 2018");
+ await clickCell("10 December 2018");
+ expect(".modal").toHaveCount(1);
+});
+
+test("Only the task name appears in the pill title when the pill_label option is not set", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(queryAllTexts(SELECTORS.pill)).toEqual([
+ "Task 1", // the pill should not include DateTime in the title
+ "Task 2",
+ "Task 4",
+ "Task 7",
+ ]);
+});
+
+test("The date and task name appears in the pill title when the pill_label option is set", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(queryAllTexts(SELECTORS.pill)).toEqual([
+ "11/30 - 12/31 - Task 1", // the task span across in week then DateTime should be displayed on the pill label
+ "Task 2", // the task does not span across in week scale then DateTime shouldn't be displayed on the pill label
+ "Task 4",
+ "Task 7",
+ ]);
+});
+
+test("A task should always have a title (pill_label='1', scale 'week')", async () => {
+ Tasks._fields.allocated_hours = fields.Float({ string: "Allocated Hours" });
+ Tasks._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ start: "2018-12-17 08:30:00",
+ stop: "2018-12-17 19:30:00", // span only one day
+ allocated_hours: 0,
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ start: "2018-12-18 08:30:00",
+ stop: "2018-12-18 19:30:00", // span only one day
+ allocated_hours: 6,
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ start: "2018-12-18 08:30:00",
+ stop: "2018-12-19 19:30:00", // span two days
+ allocated_hours: 6,
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ start: "2018-12-08 08:30:00",
+ stop: "2019-02-18 19:30:00", // span two weeks
+ allocated_hours: 6,
+ },
+ {
+ id: 5,
+ name: "Task 5",
+ start: "2018-12-18 08:30:00",
+ stop: "2019-02-18 19:30:00", // span two months
+ allocated_hours: 6,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ const titleMapping = [
+ { name: "Task 4", title: "12/8 - 2/18 - Task 4" },
+ { name: "Task 1", title: "Task 1" },
+ { name: "Task 2", title: "9:30 AM - 8:30 PM (6h) - Task 2" },
+ { name: "Task 3", title: "Task 3" },
+ { name: "Task 5", title: "12/18 - 2/18 - Task 5" },
+ ];
+ expect(queryAllTexts(".o_gantt_pill")).toEqual(titleMapping.map((e) => e.title));
+ const pills = queryAll(".o_gantt_pill");
+ for (let i = 0; i < pills.length; i++) {
+ await contains(pills[i]).click();
+ expect(".o_popover .popover-header").toHaveText(titleMapping[i].name);
+ }
+});
+
+test("A task should always have a title (pill_label='1', scale 'month')", async () => {
+ Tasks._fields.allocated_hours = fields.Float({ string: "Allocated Hours" });
+ Tasks._records = [
+ {
+ id: 1,
+ name: "Task 1",
+ start: "2018-12-15 08:30:00",
+ stop: "2018-12-15 19:30:00", // span only one day
+ allocated_hours: 0,
+ },
+ {
+ id: 2,
+ name: "Task 2",
+ start: "2018-12-16 08:30:00",
+ stop: "2018-12-16 19:30:00", // span only one day
+ allocated_hours: 6,
+ },
+ {
+ id: 3,
+ name: "Task 3",
+ start: "2018-12-16 08:30:00",
+ stop: "2018-12-17 18:30:00", // span two days
+ allocated_hours: 6,
+ },
+ {
+ id: 4,
+ name: "Task 4",
+ start: "2018-12-16 08:30:00",
+ stop: "2019-02-18 19:30:00", // span two months
+ allocated_hours: 6,
+ },
+ ];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+
+
+ `,
+ });
+ const titleMapping = [
+ { name: "Task 1", title: "Task 1" },
+ { name: "Task 2", title: "9:30 AM - 8:30 PM (6h)" },
+ { name: "Task 3", title: "Task 3" },
+ { name: "Task 4", title: "12/16 - 2/18 - Task 4" },
+ ];
+ expect(queryAllTexts(".o_gantt_pill")).toEqual(titleMapping.map((e) => e.title));
+ const pills = queryAll(".o_gantt_pill");
+ for (let i = 0; i < pills.length; i++) {
+ await contains(pills[i]).click();
+ expect(".o_popover .popover-header").toHaveText(titleMapping[i].name);
+ }
+});
+
+test("position of no content help in sample mode", async () => {
+ patchWithCleanup(GanttController.prototype, {
+ setup() {
+ super.setup(...arguments);
+ const rootRef = useRef("root");
+ useEffect(() => {
+ rootRef.el.querySelector(".o_content.o_view_sample_data").style.position =
+ "relative";
+ });
+ },
+ });
+ patchWithCleanup(GanttRenderer.prototype, {
+ isDisabled(row) {
+ return this.rows.indexOf(row) !== 0;
+ },
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["user_id"],
+ domain: Domain.FALSE.toList(),
+ });
+ expect(".o_view_nocontent").toHaveCount(1);
+ expect(".o_gantt_row_header").not.toHaveClass("o_sample_data_disabled");
+ const noContentHelp = queryFirst(".o_view_nocontent");
+ const noContentHelpTop = noContentHelp.getBoundingClientRect().top;
+ const firstRowHeader = queryFirst(".o_gantt_row_header");
+ const firstRowHeaderBottom = firstRowHeader.getBoundingClientRect().bottom;
+ expect(noContentHelpTop - firstRowHeaderBottom).toBeLessThan(3);
+});
+
+test("gantt view grouped by a boolean field: row titles should be 'True' or 'False'", async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["exclude"],
+ });
+ expect(getGridContent().rows.map((r) => r.title)).toEqual(["False", "True"]);
+});
+
+test("date grid and dst winterToSummer (1 cell part)", async () => {
+ let renderer;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ renderer = this;
+ },
+ });
+
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 8]],
+ context: {
+ initialDate: `${DST_DATES.winterToSummer.before} 08:00:00`,
+ },
+ });
+
+ function getGridInfo() {
+ return renderer.subColumns.map(({ start }) => start.toString());
+ }
+ expect(getGridInfo()).toEqual([
+ "2019-03-30T00:00:00.000+01:00",
+ "2019-03-30T01:00:00.000+01:00",
+ "2019-03-30T02:00:00.000+01:00",
+ "2019-03-30T03:00:00.000+01:00",
+ "2019-03-30T04:00:00.000+01:00",
+ "2019-03-30T05:00:00.000+01:00",
+ "2019-03-30T06:00:00.000+01:00",
+ "2019-03-30T07:00:00.000+01:00",
+ "2019-03-30T08:00:00.000+01:00",
+ "2019-03-30T09:00:00.000+01:00",
+ "2019-03-30T10:00:00.000+01:00",
+ "2019-03-30T11:00:00.000+01:00",
+ "2019-03-30T12:00:00.000+01:00",
+ "2019-03-30T13:00:00.000+01:00",
+ "2019-03-30T14:00:00.000+01:00",
+ "2019-03-30T15:00:00.000+01:00",
+ "2019-03-30T16:00:00.000+01:00",
+ "2019-03-30T17:00:00.000+01:00",
+ "2019-03-30T18:00:00.000+01:00",
+ "2019-03-30T19:00:00.000+01:00",
+ "2019-03-30T20:00:00.000+01:00",
+ "2019-03-30T21:00:00.000+01:00",
+ "2019-03-30T22:00:00.000+01:00",
+ "2019-03-30T23:00:00.000+01:00",
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-03-31T01:00:00.000+01:00",
+ "2019-03-31T03:00:00.000+02:00",
+ "2019-03-31T04:00:00.000+02:00",
+ "2019-03-31T05:00:00.000+02:00",
+ "2019-03-31T06:00:00.000+02:00",
+ "2019-03-31T07:00:00.000+02:00",
+ "2019-03-31T08:00:00.000+02:00",
+ "2019-03-31T09:00:00.000+02:00",
+ "2019-03-31T10:00:00.000+02:00",
+ "2019-03-31T11:00:00.000+02:00",
+ "2019-03-31T12:00:00.000+02:00",
+ "2019-03-31T13:00:00.000+02:00",
+ "2019-03-31T14:00:00.000+02:00",
+ ]);
+
+ await setScale(4);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-03-31", stopDate: "2019-04-07" });
+ expect(getGridInfo()).toEqual([
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-04-01T00:00:00.000+02:00",
+ "2019-04-02T00:00:00.000+02:00",
+ "2019-04-03T00:00:00.000+02:00",
+ "2019-04-04T00:00:00.000+02:00",
+ "2019-04-05T00:00:00.000+02:00",
+ "2019-04-06T00:00:00.000+02:00",
+ "2019-04-07T00:00:00.000+02:00",
+ ]);
+
+ await setScale(2);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-03-01", stopDate: "2019-04-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-03-02T00:00:00.000+01:00",
+ "2019-03-03T00:00:00.000+01:00",
+ "2019-03-04T00:00:00.000+01:00",
+ "2019-03-05T00:00:00.000+01:00",
+ "2019-03-06T00:00:00.000+01:00",
+ "2019-03-07T00:00:00.000+01:00",
+ "2019-03-08T00:00:00.000+01:00",
+ "2019-03-09T00:00:00.000+01:00",
+ "2019-03-10T00:00:00.000+01:00",
+ "2019-03-11T00:00:00.000+01:00",
+ "2019-03-12T00:00:00.000+01:00",
+ "2019-03-13T00:00:00.000+01:00",
+ "2019-03-14T00:00:00.000+01:00",
+ "2019-03-15T00:00:00.000+01:00",
+ "2019-03-16T00:00:00.000+01:00",
+ "2019-03-17T00:00:00.000+01:00",
+ "2019-03-18T00:00:00.000+01:00",
+ "2019-03-19T00:00:00.000+01:00",
+ "2019-03-20T00:00:00.000+01:00",
+ "2019-03-21T00:00:00.000+01:00",
+ "2019-03-22T00:00:00.000+01:00",
+ "2019-03-23T00:00:00.000+01:00",
+ "2019-03-24T00:00:00.000+01:00",
+ "2019-03-25T00:00:00.000+01:00",
+ "2019-03-26T00:00:00.000+01:00",
+ "2019-03-27T00:00:00.000+01:00",
+ "2019-03-28T00:00:00.000+01:00",
+ "2019-03-29T00:00:00.000+01:00",
+ "2019-03-30T00:00:00.000+01:00",
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-04-01T00:00:00.000+02:00",
+ ]);
+
+ await setScale(0);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2020-01-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-01-01T00:00:00.000+01:00",
+ "2019-02-01T00:00:00.000+01:00",
+ "2019-03-01T00:00:00.000+01:00",
+ "2019-04-01T00:00:00.000+02:00",
+ "2019-05-01T00:00:00.000+02:00",
+ "2019-06-01T00:00:00.000+02:00",
+ "2019-07-01T00:00:00.000+02:00",
+ "2019-08-01T00:00:00.000+02:00",
+ "2019-09-01T00:00:00.000+02:00",
+ "2019-10-01T00:00:00.000+02:00",
+ "2019-11-01T00:00:00.000+01:00",
+ "2019-12-01T00:00:00.000+01:00",
+ "2020-01-01T00:00:00.000+01:00",
+ ]);
+});
+
+test("date grid and dst summerToWinter (1 cell part)", async () => {
+ let renderer;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ renderer = this;
+ },
+ });
+
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 8]],
+ context: {
+ initialDate: `${DST_DATES.summerToWinter.before} 08:00:00`,
+ },
+ });
+
+ function getGridInfo() {
+ return renderer.subColumns.map(({ start }) => start.toString());
+ }
+ expect(getGridInfo()).toEqual([
+ "2019-10-26T00:00:00.000+02:00",
+ "2019-10-26T01:00:00.000+02:00",
+ "2019-10-26T02:00:00.000+02:00",
+ "2019-10-26T03:00:00.000+02:00",
+ "2019-10-26T04:00:00.000+02:00",
+ "2019-10-26T05:00:00.000+02:00",
+ "2019-10-26T06:00:00.000+02:00",
+ "2019-10-26T07:00:00.000+02:00",
+ "2019-10-26T08:00:00.000+02:00",
+ "2019-10-26T09:00:00.000+02:00",
+ "2019-10-26T10:00:00.000+02:00",
+ "2019-10-26T11:00:00.000+02:00",
+ "2019-10-26T12:00:00.000+02:00",
+ "2019-10-26T13:00:00.000+02:00",
+ "2019-10-26T14:00:00.000+02:00",
+ "2019-10-26T15:00:00.000+02:00",
+ "2019-10-26T16:00:00.000+02:00",
+ "2019-10-26T17:00:00.000+02:00",
+ "2019-10-26T18:00:00.000+02:00",
+ "2019-10-26T19:00:00.000+02:00",
+ "2019-10-26T20:00:00.000+02:00",
+ "2019-10-26T21:00:00.000+02:00",
+ "2019-10-26T22:00:00.000+02:00",
+ "2019-10-26T23:00:00.000+02:00",
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-27T01:00:00.000+02:00",
+ "2019-10-27T02:00:00.000+02:00",
+ "2019-10-27T02:00:00.000+01:00",
+ "2019-10-27T03:00:00.000+01:00",
+ "2019-10-27T04:00:00.000+01:00",
+ "2019-10-27T05:00:00.000+01:00",
+ "2019-10-27T06:00:00.000+01:00",
+ "2019-10-27T07:00:00.000+01:00",
+ "2019-10-27T08:00:00.000+01:00",
+ "2019-10-27T09:00:00.000+01:00",
+ "2019-10-27T10:00:00.000+01:00",
+ "2019-10-27T11:00:00.000+01:00",
+ "2019-10-27T12:00:00.000+01:00",
+ ]);
+
+ await setScale(4);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-10-27", stopDate: "2019-11-03" });
+ expect(getGridInfo()).toEqual([
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-28T00:00:00.000+01:00",
+ "2019-10-29T00:00:00.000+01:00",
+ "2019-10-30T00:00:00.000+01:00",
+ "2019-10-31T00:00:00.000+01:00",
+ "2019-11-01T00:00:00.000+01:00",
+ "2019-11-02T00:00:00.000+01:00",
+ "2019-11-03T00:00:00.000+01:00",
+ ]);
+
+ await setScale(2);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-10-01", stopDate: "2019-11-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-10-02T00:00:00.000+02:00",
+ "2019-10-03T00:00:00.000+02:00",
+ "2019-10-04T00:00:00.000+02:00",
+ "2019-10-05T00:00:00.000+02:00",
+ "2019-10-06T00:00:00.000+02:00",
+ "2019-10-07T00:00:00.000+02:00",
+ "2019-10-08T00:00:00.000+02:00",
+ "2019-10-09T00:00:00.000+02:00",
+ "2019-10-10T00:00:00.000+02:00",
+ "2019-10-11T00:00:00.000+02:00",
+ "2019-10-12T00:00:00.000+02:00",
+ "2019-10-13T00:00:00.000+02:00",
+ "2019-10-14T00:00:00.000+02:00",
+ "2019-10-15T00:00:00.000+02:00",
+ "2019-10-16T00:00:00.000+02:00",
+ "2019-10-17T00:00:00.000+02:00",
+ "2019-10-18T00:00:00.000+02:00",
+ "2019-10-19T00:00:00.000+02:00",
+ "2019-10-20T00:00:00.000+02:00",
+ "2019-10-21T00:00:00.000+02:00",
+ "2019-10-22T00:00:00.000+02:00",
+ "2019-10-23T00:00:00.000+02:00",
+ "2019-10-24T00:00:00.000+02:00",
+ "2019-10-25T00:00:00.000+02:00",
+ "2019-10-26T00:00:00.000+02:00",
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-28T00:00:00.000+01:00",
+ "2019-10-29T00:00:00.000+01:00",
+ "2019-10-30T00:00:00.000+01:00",
+ "2019-10-31T00:00:00.000+01:00",
+ "2019-11-01T00:00:00.000+01:00",
+ ]);
+
+ await setScale(0);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-01-01", stopDate: "2020-01-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-01-01T00:00:00.000+01:00",
+ "2019-02-01T00:00:00.000+01:00",
+ "2019-03-01T00:00:00.000+01:00",
+ "2019-04-01T00:00:00.000+02:00",
+ "2019-05-01T00:00:00.000+02:00",
+ "2019-06-01T00:00:00.000+02:00",
+ "2019-07-01T00:00:00.000+02:00",
+ "2019-08-01T00:00:00.000+02:00",
+ "2019-09-01T00:00:00.000+02:00",
+ "2019-10-01T00:00:00.000+02:00",
+ "2019-11-01T00:00:00.000+01:00",
+ "2019-12-01T00:00:00.000+01:00",
+ "2020-01-01T00:00:00.000+01:00",
+ ]);
+});
+
+test("date grid and dst winterToSummer (2 cell part)", async () => {
+ let renderer;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ renderer = this;
+ },
+ });
+
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: [["id", "=", 8]],
+ context: {
+ initialDate: `${DST_DATES.winterToSummer.before} 08:00:00`,
+ },
+ });
+
+ function getGridInfo() {
+ return renderer.subColumns.map(({ start }) => start.toString());
+ }
+ expect(getGridInfo()).toEqual([
+ "2019-03-30T00:00:00.000+01:00",
+ "2019-03-30T00:30:00.000+01:00",
+ "2019-03-30T01:00:00.000+01:00",
+ "2019-03-30T01:30:00.000+01:00",
+ "2019-03-30T02:00:00.000+01:00",
+ "2019-03-30T02:30:00.000+01:00",
+ "2019-03-30T03:00:00.000+01:00",
+ "2019-03-30T03:30:00.000+01:00",
+ "2019-03-30T04:00:00.000+01:00",
+ "2019-03-30T04:30:00.000+01:00",
+ "2019-03-30T05:00:00.000+01:00",
+ "2019-03-30T05:30:00.000+01:00",
+ "2019-03-30T06:00:00.000+01:00",
+ "2019-03-30T06:30:00.000+01:00",
+ "2019-03-30T07:00:00.000+01:00",
+ "2019-03-30T07:30:00.000+01:00",
+ "2019-03-30T08:00:00.000+01:00",
+ "2019-03-30T08:30:00.000+01:00",
+ "2019-03-30T09:00:00.000+01:00",
+ "2019-03-30T09:30:00.000+01:00",
+ "2019-03-30T10:00:00.000+01:00",
+ "2019-03-30T10:30:00.000+01:00",
+ "2019-03-30T11:00:00.000+01:00",
+ "2019-03-30T11:30:00.000+01:00",
+ "2019-03-30T12:00:00.000+01:00",
+ "2019-03-30T12:30:00.000+01:00",
+ "2019-03-30T13:00:00.000+01:00",
+ "2019-03-30T13:30:00.000+01:00",
+ "2019-03-30T14:00:00.000+01:00",
+ "2019-03-30T14:30:00.000+01:00",
+ "2019-03-30T15:00:00.000+01:00",
+ "2019-03-30T15:30:00.000+01:00",
+ "2019-03-30T16:00:00.000+01:00",
+ "2019-03-30T16:30:00.000+01:00",
+ "2019-03-30T17:00:00.000+01:00",
+ "2019-03-30T17:30:00.000+01:00",
+ "2019-03-30T18:00:00.000+01:00",
+ "2019-03-30T18:30:00.000+01:00",
+ "2019-03-30T19:00:00.000+01:00",
+ "2019-03-30T19:30:00.000+01:00",
+ "2019-03-30T20:00:00.000+01:00",
+ "2019-03-30T20:30:00.000+01:00",
+ "2019-03-30T21:00:00.000+01:00",
+ "2019-03-30T21:30:00.000+01:00",
+ "2019-03-30T22:00:00.000+01:00",
+ "2019-03-30T22:30:00.000+01:00",
+ "2019-03-30T23:00:00.000+01:00",
+ "2019-03-30T23:30:00.000+01:00",
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-03-31T00:30:00.000+01:00",
+ "2019-03-31T01:00:00.000+01:00",
+ "2019-03-31T01:30:00.000+01:00",
+ "2019-03-31T03:00:00.000+02:00",
+ "2019-03-31T03:30:00.000+02:00",
+ "2019-03-31T04:00:00.000+02:00",
+ "2019-03-31T04:30:00.000+02:00",
+ "2019-03-31T05:00:00.000+02:00",
+ "2019-03-31T05:30:00.000+02:00",
+ "2019-03-31T06:00:00.000+02:00",
+ "2019-03-31T06:30:00.000+02:00",
+ "2019-03-31T07:00:00.000+02:00",
+ "2019-03-31T07:30:00.000+02:00",
+ "2019-03-31T08:00:00.000+02:00",
+ "2019-03-31T08:30:00.000+02:00",
+ "2019-03-31T09:00:00.000+02:00",
+ "2019-03-31T09:30:00.000+02:00",
+ "2019-03-31T10:00:00.000+02:00",
+ "2019-03-31T10:30:00.000+02:00",
+ "2019-03-31T11:00:00.000+02:00",
+ "2019-03-31T11:30:00.000+02:00",
+ "2019-03-31T12:00:00.000+02:00",
+ "2019-03-31T12:30:00.000+02:00",
+ "2019-03-31T13:00:00.000+02:00",
+ "2019-03-31T13:30:00.000+02:00",
+ "2019-03-31T14:00:00.000+02:00",
+ "2019-03-31T14:30:00.000+02:00",
+ ]);
+
+ await setScale(4);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-03-31", stopDate: "2019-04-07" });
+ expect(getGridInfo()).toEqual([
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-03-31T12:00:00.000+02:00",
+ "2019-04-01T00:00:00.000+02:00",
+ "2019-04-01T12:00:00.000+02:00",
+ "2019-04-02T00:00:00.000+02:00",
+ "2019-04-02T12:00:00.000+02:00",
+ "2019-04-03T00:00:00.000+02:00",
+ "2019-04-03T12:00:00.000+02:00",
+ "2019-04-04T00:00:00.000+02:00",
+ "2019-04-04T12:00:00.000+02:00",
+ "2019-04-05T00:00:00.000+02:00",
+ "2019-04-05T12:00:00.000+02:00",
+ "2019-04-06T00:00:00.000+02:00",
+ "2019-04-06T12:00:00.000+02:00",
+ "2019-04-07T00:00:00.000+02:00",
+ "2019-04-07T12:00:00.000+02:00",
+ ]);
+
+ await setScale(2);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-03-01", stopDate: "2019-04-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-03-02T00:00:00.000+01:00",
+ "2019-03-02T12:00:00.000+01:00",
+ "2019-03-03T00:00:00.000+01:00",
+ "2019-03-03T12:00:00.000+01:00",
+ "2019-03-04T00:00:00.000+01:00",
+ "2019-03-04T12:00:00.000+01:00",
+ "2019-03-05T00:00:00.000+01:00",
+ "2019-03-05T12:00:00.000+01:00",
+ "2019-03-06T00:00:00.000+01:00",
+ "2019-03-06T12:00:00.000+01:00",
+ "2019-03-07T00:00:00.000+01:00",
+ "2019-03-07T12:00:00.000+01:00",
+ "2019-03-08T00:00:00.000+01:00",
+ "2019-03-08T12:00:00.000+01:00",
+ "2019-03-09T00:00:00.000+01:00",
+ "2019-03-09T12:00:00.000+01:00",
+ "2019-03-10T00:00:00.000+01:00",
+ "2019-03-10T12:00:00.000+01:00",
+ "2019-03-11T00:00:00.000+01:00",
+ "2019-03-11T12:00:00.000+01:00",
+ "2019-03-12T00:00:00.000+01:00",
+ "2019-03-12T12:00:00.000+01:00",
+ "2019-03-13T00:00:00.000+01:00",
+ "2019-03-13T12:00:00.000+01:00",
+ "2019-03-14T00:00:00.000+01:00",
+ "2019-03-14T12:00:00.000+01:00",
+ "2019-03-15T00:00:00.000+01:00",
+ "2019-03-15T12:00:00.000+01:00",
+ "2019-03-16T00:00:00.000+01:00",
+ "2019-03-16T12:00:00.000+01:00",
+ "2019-03-17T00:00:00.000+01:00",
+ "2019-03-17T12:00:00.000+01:00",
+ "2019-03-18T00:00:00.000+01:00",
+ "2019-03-18T12:00:00.000+01:00",
+ "2019-03-19T00:00:00.000+01:00",
+ "2019-03-19T12:00:00.000+01:00",
+ "2019-03-20T00:00:00.000+01:00",
+ "2019-03-20T12:00:00.000+01:00",
+ "2019-03-21T00:00:00.000+01:00",
+ "2019-03-21T12:00:00.000+01:00",
+ "2019-03-22T00:00:00.000+01:00",
+ "2019-03-22T12:00:00.000+01:00",
+ "2019-03-23T00:00:00.000+01:00",
+ "2019-03-23T12:00:00.000+01:00",
+ "2019-03-24T00:00:00.000+01:00",
+ "2019-03-24T12:00:00.000+01:00",
+ "2019-03-25T00:00:00.000+01:00",
+ "2019-03-25T12:00:00.000+01:00",
+ "2019-03-26T00:00:00.000+01:00",
+ "2019-03-26T12:00:00.000+01:00",
+ "2019-03-27T00:00:00.000+01:00",
+ "2019-03-27T12:00:00.000+01:00",
+ "2019-03-28T00:00:00.000+01:00",
+ "2019-03-28T12:00:00.000+01:00",
+ "2019-03-29T00:00:00.000+01:00",
+ "2019-03-29T12:00:00.000+01:00",
+ "2019-03-30T00:00:00.000+01:00",
+ "2019-03-30T12:00:00.000+01:00",
+ "2019-03-31T00:00:00.000+01:00",
+ "2019-03-31T12:00:00.000+02:00",
+ "2019-04-01T00:00:00.000+02:00",
+ "2019-04-01T12:00:00.000+02:00",
+ ]);
+});
+
+test("date grid and dst summerToWinter (2 cell part)", async () => {
+ let renderer;
+ patchWithCleanup(GanttRenderer.prototype, {
+ setup() {
+ super.setup(...arguments);
+ renderer = this;
+ },
+ });
+
+ mockTimeZone("Europe/Brussels");
+ Tasks._records = [];
+
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ context: {
+ initialDate: `${DST_DATES.summerToWinter.before} 08:00:00`,
+ },
+ });
+
+ function getGridInfo() {
+ return renderer.subColumns.map(({ start }) => start.toString());
+ }
+ expect(getGridInfo()).toEqual([
+ "2019-10-26T00:00:00.000+02:00",
+ "2019-10-26T00:30:00.000+02:00",
+ "2019-10-26T01:00:00.000+02:00",
+ "2019-10-26T01:30:00.000+02:00",
+ "2019-10-26T02:00:00.000+02:00",
+ "2019-10-26T02:30:00.000+02:00",
+ "2019-10-26T03:00:00.000+02:00",
+ "2019-10-26T03:30:00.000+02:00",
+ "2019-10-26T04:00:00.000+02:00",
+ "2019-10-26T04:30:00.000+02:00",
+ "2019-10-26T05:00:00.000+02:00",
+ "2019-10-26T05:30:00.000+02:00",
+ "2019-10-26T06:00:00.000+02:00",
+ "2019-10-26T06:30:00.000+02:00",
+ "2019-10-26T07:00:00.000+02:00",
+ "2019-10-26T07:30:00.000+02:00",
+ "2019-10-26T08:00:00.000+02:00",
+ "2019-10-26T08:30:00.000+02:00",
+ "2019-10-26T09:00:00.000+02:00",
+ "2019-10-26T09:30:00.000+02:00",
+ "2019-10-26T10:00:00.000+02:00",
+ "2019-10-26T10:30:00.000+02:00",
+ "2019-10-26T11:00:00.000+02:00",
+ "2019-10-26T11:30:00.000+02:00",
+ "2019-10-26T12:00:00.000+02:00",
+ "2019-10-26T12:30:00.000+02:00",
+ "2019-10-26T13:00:00.000+02:00",
+ "2019-10-26T13:30:00.000+02:00",
+ "2019-10-26T14:00:00.000+02:00",
+ "2019-10-26T14:30:00.000+02:00",
+ "2019-10-26T15:00:00.000+02:00",
+ "2019-10-26T15:30:00.000+02:00",
+ "2019-10-26T16:00:00.000+02:00",
+ "2019-10-26T16:30:00.000+02:00",
+ "2019-10-26T17:00:00.000+02:00",
+ "2019-10-26T17:30:00.000+02:00",
+ "2019-10-26T18:00:00.000+02:00",
+ "2019-10-26T18:30:00.000+02:00",
+ "2019-10-26T19:00:00.000+02:00",
+ "2019-10-26T19:30:00.000+02:00",
+ "2019-10-26T20:00:00.000+02:00",
+ "2019-10-26T20:30:00.000+02:00",
+ "2019-10-26T21:00:00.000+02:00",
+ "2019-10-26T21:30:00.000+02:00",
+ "2019-10-26T22:00:00.000+02:00",
+ "2019-10-26T22:30:00.000+02:00",
+ "2019-10-26T23:00:00.000+02:00",
+ "2019-10-26T23:30:00.000+02:00",
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-27T00:30:00.000+02:00",
+ "2019-10-27T01:00:00.000+02:00",
+ "2019-10-27T01:30:00.000+02:00",
+ "2019-10-27T02:00:00.000+02:00",
+ "2019-10-27T02:30:00.000+02:00",
+ "2019-10-27T02:00:00.000+01:00",
+ "2019-10-27T02:30:00.000+01:00",
+ "2019-10-27T03:00:00.000+01:00",
+ "2019-10-27T03:30:00.000+01:00",
+ "2019-10-27T04:00:00.000+01:00",
+ "2019-10-27T04:30:00.000+01:00",
+ "2019-10-27T05:00:00.000+01:00",
+ "2019-10-27T05:30:00.000+01:00",
+ "2019-10-27T06:00:00.000+01:00",
+ "2019-10-27T06:30:00.000+01:00",
+ "2019-10-27T07:00:00.000+01:00",
+ "2019-10-27T07:30:00.000+01:00",
+ "2019-10-27T08:00:00.000+01:00",
+ "2019-10-27T08:30:00.000+01:00",
+ "2019-10-27T09:00:00.000+01:00",
+ "2019-10-27T09:30:00.000+01:00",
+ "2019-10-27T10:00:00.000+01:00",
+ "2019-10-27T10:30:00.000+01:00",
+ "2019-10-27T11:00:00.000+01:00",
+ "2019-10-27T11:30:00.000+01:00",
+ "2019-10-27T12:00:00.000+01:00",
+ "2019-10-27T12:30:00.000+01:00",
+ ]);
+
+ await setScale(4);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-10-27", stopDate: "2019-11-03" });
+ expect(getGridInfo()).toEqual([
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-27T12:00:00.000+01:00",
+ "2019-10-28T00:00:00.000+01:00",
+ "2019-10-28T12:00:00.000+01:00",
+ "2019-10-29T00:00:00.000+01:00",
+ "2019-10-29T12:00:00.000+01:00",
+ "2019-10-30T00:00:00.000+01:00",
+ "2019-10-30T12:00:00.000+01:00",
+ "2019-10-31T00:00:00.000+01:00",
+ "2019-10-31T12:00:00.000+01:00",
+ "2019-11-01T00:00:00.000+01:00",
+ "2019-11-01T12:00:00.000+01:00",
+ "2019-11-02T00:00:00.000+01:00",
+ "2019-11-02T12:00:00.000+01:00",
+ "2019-11-03T00:00:00.000+01:00",
+ "2019-11-03T12:00:00.000+01:00",
+ ]);
+
+ await setScale(2);
+ await ganttControlsChanges();
+ await selectGanttRange({ startDate: "2019-10-01", stopDate: "2019-11-01" });
+ expect(getGridInfo()).toEqual([
+ "2019-10-02T00:00:00.000+02:00",
+ "2019-10-02T12:00:00.000+02:00",
+ "2019-10-03T00:00:00.000+02:00",
+ "2019-10-03T12:00:00.000+02:00",
+ "2019-10-04T00:00:00.000+02:00",
+ "2019-10-04T12:00:00.000+02:00",
+ "2019-10-05T00:00:00.000+02:00",
+ "2019-10-05T12:00:00.000+02:00",
+ "2019-10-06T00:00:00.000+02:00",
+ "2019-10-06T12:00:00.000+02:00",
+ "2019-10-07T00:00:00.000+02:00",
+ "2019-10-07T12:00:00.000+02:00",
+ "2019-10-08T00:00:00.000+02:00",
+ "2019-10-08T12:00:00.000+02:00",
+ "2019-10-09T00:00:00.000+02:00",
+ "2019-10-09T12:00:00.000+02:00",
+ "2019-10-10T00:00:00.000+02:00",
+ "2019-10-10T12:00:00.000+02:00",
+ "2019-10-11T00:00:00.000+02:00",
+ "2019-10-11T12:00:00.000+02:00",
+ "2019-10-12T00:00:00.000+02:00",
+ "2019-10-12T12:00:00.000+02:00",
+ "2019-10-13T00:00:00.000+02:00",
+ "2019-10-13T12:00:00.000+02:00",
+ "2019-10-14T00:00:00.000+02:00",
+ "2019-10-14T12:00:00.000+02:00",
+ "2019-10-15T00:00:00.000+02:00",
+ "2019-10-15T12:00:00.000+02:00",
+ "2019-10-16T00:00:00.000+02:00",
+ "2019-10-16T12:00:00.000+02:00",
+ "2019-10-17T00:00:00.000+02:00",
+ "2019-10-17T12:00:00.000+02:00",
+ "2019-10-18T00:00:00.000+02:00",
+ "2019-10-18T12:00:00.000+02:00",
+ "2019-10-19T00:00:00.000+02:00",
+ "2019-10-19T12:00:00.000+02:00",
+ "2019-10-20T00:00:00.000+02:00",
+ "2019-10-20T12:00:00.000+02:00",
+ "2019-10-21T00:00:00.000+02:00",
+ "2019-10-21T12:00:00.000+02:00",
+ "2019-10-22T00:00:00.000+02:00",
+ "2019-10-22T12:00:00.000+02:00",
+ "2019-10-23T00:00:00.000+02:00",
+ "2019-10-23T12:00:00.000+02:00",
+ "2019-10-24T00:00:00.000+02:00",
+ "2019-10-24T12:00:00.000+02:00",
+ "2019-10-25T00:00:00.000+02:00",
+ "2019-10-25T12:00:00.000+02:00",
+ "2019-10-26T00:00:00.000+02:00",
+ "2019-10-26T12:00:00.000+02:00",
+ "2019-10-27T00:00:00.000+02:00",
+ "2019-10-27T12:00:00.000+01:00",
+ "2019-10-28T00:00:00.000+01:00",
+ "2019-10-28T12:00:00.000+01:00",
+ "2019-10-29T00:00:00.000+01:00",
+ "2019-10-29T12:00:00.000+01:00",
+ "2019-10-30T00:00:00.000+01:00",
+ "2019-10-30T12:00:00.000+01:00",
+ "2019-10-31T00:00:00.000+01:00",
+ "2019-10-31T12:00:00.000+01:00",
+ "2019-11-01T00:00:00.000+01:00",
+ "2019-11-01T12:00:00.000+01:00",
+ ]);
+});
+
+test("groups_limit attribute (no groupBy)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(0); // only one group here!
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2"]);
+ const { rows } = getGridContent();
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 04 (1/2) December 2018",
+ level: 0,
+ title: "Task 5",
+ },
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 1,
+ title: "Task 1",
+ },
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 2,
+ title: "Task 4",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 2,
+ title: "Task 7",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ },
+ ]);
+});
+
+test("groups_limit attribute (one groupBy)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ expect.step(`with offset ${kwargs.offset}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["stage_id"],
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(1);
+ expect(".o_pager_value").toHaveText("1-2");
+ expect(".o_pager_limit").toHaveText("4");
+ let rows = getGridContent().rows;
+ expect(rows).toEqual([
+ {
+ title: "todo",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 1,
+ title: "Task 7",
+ },
+ ],
+ title: "in_progress",
+ },
+ ]);
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2", "with offset 0"]);
+
+ await pagerNext();
+ expect(".o_pager_value").toHaveText("3-4");
+ expect(".o_pager_limit").toHaveText("4");
+ rows = getGridContent().rows;
+ expect(rows).toEqual([
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "done",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ title: "cancel",
+ },
+ ]);
+ expect.verifySteps(["get_gantt_data", "with limit 2", "with offset 2"]);
+});
+
+test("groups_limit attribute (two groupBys)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ expect.step(`with offset ${kwargs.offset}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["stage_id", "project_id"],
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(1);
+ expect(".o_pager_value").toHaveText("1-2");
+ expect(".o_pager_limit").toHaveText("5");
+ let rows = getGridContent().rows;
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ title: "todo",
+ },
+ {
+ title: "Project 2",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ title: "1",
+ },
+ ],
+ title: "in_progress",
+ },
+ {
+ pills: [
+ {
+ colSpan: "Out of bounds (1) -> 31 December 2018",
+ level: 0,
+ title: "Task 1",
+ },
+ ],
+ title: "Project 1",
+ },
+ ]);
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2", "with offset 0"]);
+
+ await pagerNext();
+ expect(".o_pager_value").toHaveText("3-4");
+ expect(".o_pager_limit").toHaveText("5");
+ rows = getGridContent().rows;
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ title: "1",
+ },
+ ],
+ title: "in_progress",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 (1/2) December 2018 -> 20 December 2018",
+ level: 0,
+ title: "Task 7",
+ },
+ ],
+ title: "Project 2",
+ },
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ title: "1",
+ },
+ ],
+ title: "done",
+ },
+ {
+ pills: [
+ {
+ colSpan: "17 (1/2) December 2018 -> 22 (1/2) December 2018",
+ level: 0,
+ title: "Task 2",
+ },
+ ],
+ title: "Project 1",
+ },
+ ]);
+ expect.verifySteps(["get_gantt_data", "with limit 2", "with offset 2"]);
+
+ await pagerNext();
+ expect(".o_pager_value").toHaveText("5-5");
+ expect(".o_pager_limit").toHaveText("5");
+ rows = getGridContent().rows;
+ expect(rows).toEqual([
+ {
+ isGroup: true,
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ title: "1",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ title: "1",
+ },
+ ],
+ title: "cancel",
+ },
+ {
+ pills: [
+ {
+ colSpan: "20 December 2018 -> 20 (1/2) December 2018",
+ level: 0,
+ title: "Task 4",
+ },
+ {
+ colSpan: "27 December 2018 -> 03 (1/2) January 2019",
+ level: 0,
+ title: "Task 3",
+ },
+ ],
+ title: "Project 1",
+ },
+ ]);
+ expect.verifySteps(["get_gantt_data", "with limit 2", "with offset 4"]);
+});
+
+test("groups_limit attribute in sample mode (no groupBy)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: Domain.FALSE.toList(),
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(0); // only one group here!
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2"]);
+});
+
+test("groups_limit attribute in sample mode (one groupBy)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ expect.step(`with offset ${kwargs.offset}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: Domain.FALSE.toList(),
+ groupBy: ["stage_id"],
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(1);
+ expect(".o_pager_value").toHaveText("1-2");
+ expect(".o_pager_limit").toHaveText("2");
+ expect(".o_gantt_row_title").toHaveCount(2);
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2", "with offset 0"]);
+});
+
+test("groups_limit attribute in sample mode (two groupBys)", async () => {
+ onRpc(({ method, kwargs }) => {
+ expect.step(method);
+ if (kwargs.limit) {
+ expect.step(`with limit ${kwargs.limit}`);
+ expect.step(`with offset ${kwargs.offset}`);
+ }
+ });
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ domain: Domain.FALSE.toList(),
+ groupBy: ["stage_id", "project_id"],
+ });
+ expect(".o_gantt_view .o_control_panel .o_pager").toHaveCount(1);
+ expect(".o_pager_value").toHaveText("1-2");
+ expect(".o_pager_limit").toHaveText("2");
+ expect.verifySteps(["get_views", "get_gantt_data", "with limit 2", "with offset 0"]);
+});
+
+test("context in action should not override context added by the gantt view", async () => {
+ Tasks._views.form = `
+
+ `;
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ context: {
+ gantt_date: "2018-11-30",
+ gantt_scale: "month",
+ default_user_id: false,
+ },
+ });
+ await hoverGridCell("11 December 2018");
+ await clickCell("11 December 2018");
+ expect(".modal .o_field_many2one[name=user_id]").toHaveCount(1);
+ expect(".modal .o_field_many2one[name=user_id] input").toHaveValue("User 1");
+});
+
+test("The date and task should appear even if the pill is planned on 2 days but displayed in one day by the gantt view", async () => {
+ mockDate("2024-01-01T08:00:00", +0);
+
+ Tasks._records.push(
+ {
+ id: 9,
+ name: "Task 9",
+ allocated_hours: 4,
+ start: "2024-01-01 16:00:00",
+ stop: "2024-01-02 01:00:00",
+ },
+ {
+ id: 10,
+ name: "Task 10",
+ allocated_hours: 4,
+ start: "2024-01-02 16:00:00",
+ stop: "2024-01-03 02:00:00",
+ },
+ {
+ // will be displayed in 2 days
+ id: 11,
+ name: "Task 11",
+ allocated_hours: 4,
+ start: "2024-01-03 16:00:00",
+ stop: "2024-01-04 03:00:00",
+ }
+ );
+ await mountGanttView({
+ resModel: "tasks",
+ arch: `
+
+ `,
+ });
+ expect(".o_gantt_pill").toHaveCount(3, { message: "should have 3 pills in the gantt view" });
+ expect(queryAllTexts(".o_gantt_pill_title")).toEqual([
+ "4:00 PM - 1:00 AM (4h) - Task 9",
+ "4:00 PM - 2:00 AM (4h) - Task 10",
+ "Task 11",
+ ]);
+});
diff --git a/addons_extensions/web_gantt/static/tests/gantt_view_sample.test.js b/addons_extensions/web_gantt/static/tests/gantt_view_sample.test.js
new file mode 100644
index 000000000..fb5c15fc5
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/gantt_view_sample.test.js
@@ -0,0 +1,214 @@
+import { beforeEach, describe, expect, test } from "@odoo/hoot";
+import { queryFirst, queryAll } from "@odoo/hoot-dom";
+import { mockDate, animationFrame } from "@odoo/hoot-mock";
+import { markup } from "@odoo/owl";
+import {
+ getService,
+ mountWithCleanup,
+ switchView,
+ toggleMenuItem,
+ toggleSearchBarMenu,
+} from "@web/../tests/web_test_helpers";
+import { Tasks, defineGanttModels } from "./gantt_mock_models";
+import { SELECTORS, mountGanttView } from "./web_gantt_test_helpers";
+
+import { Domain } from "@web/core/domain";
+import { WebClient } from "@web/webclient/webclient";
+
+describe.current.tags("desktop");
+
+defineGanttModels();
+beforeEach(() => mockDate("2018-12-20T08:00:00", +1));
+
+test(`empty grouped gantt with sample="1"`, async () => {
+ Tasks._views = {
+ gantt: ``,
+ graph: ``,
+ search: ``,
+ };
+
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [
+ [false, "gantt"],
+ [false, "graph"],
+ ],
+ domain: Domain.FALSE.toList(),
+ groupBy: ["project_id"],
+ });
+ await animationFrame();
+
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(queryAll(SELECTORS.pill).length).toBeWithin(0, 16);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+
+ const content = queryFirst(SELECTORS.viewContent).innerHTML;
+ await switchView("gantt");
+ await animationFrame();
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(SELECTORS.viewContent).toHaveProperty("innerHTML", content);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+});
+
+test("empty gantt with sample data and default_group_by", async () => {
+ Tasks._views = {
+ gantt: ``,
+ graph: ``,
+ search: ``,
+ };
+
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [
+ [false, "gantt"],
+ [false, "graph"],
+ ],
+ domain: Domain.FALSE.toList(),
+ });
+ await animationFrame();
+
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(queryAll(SELECTORS.pill).length).toBeWithin(0, 16);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+
+ const content = queryFirst(SELECTORS.viewContent).innerHTML;
+ await switchView("gantt");
+ await animationFrame();
+
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(SELECTORS.viewContent).toHaveProperty("innerHTML", content);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+});
+
+test("empty gantt with sample data and default_group_by (switch view)", async () => {
+ Tasks._views = {
+ gantt: ``,
+ list: `
`,
+ search: ``,
+ };
+
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [
+ [false, "gantt"],
+ [false, "list"],
+ ],
+ domain: Domain.FALSE.toList(),
+ });
+ await animationFrame();
+
+ // the gantt view should be in sample mode
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(queryAll(SELECTORS.pill).length).toBeWithin(0, 16);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+ const content = queryFirst(SELECTORS.viewContent).innerHTML;
+
+ // switch to list view
+ await switchView("list");
+ expect(SELECTORS.view).toHaveCount(0);
+
+ // go back to gantt view
+ await switchView("gantt");
+ await animationFrame();
+
+ expect(SELECTORS.view).toHaveCount(1);
+
+ // the gantt view should be still in sample mode
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+ expect(SELECTORS.viewContent).toHaveProperty("innerHTML", content);
+});
+
+test(`empty gantt with sample="1"`, async () => {
+ Tasks._views = {
+ gantt: ``,
+ graph: ``,
+ search: ``,
+ };
+
+ await mountWithCleanup(WebClient);
+ await getService("action").doAction({
+ res_model: "tasks",
+ type: "ir.actions.act_window",
+ views: [
+ [false, "gantt"],
+ [false, "graph"],
+ ],
+ domain: Domain.FALSE.toList(),
+ });
+ await animationFrame();
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(queryAll(SELECTORS.pill).length).toBeWithin(0, 16);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+
+ const content = queryFirst(SELECTORS.viewContent).innerHTML;
+
+ await switchView("gantt");
+ await animationFrame();
+ expect(SELECTORS.viewContent).toHaveClass("o_view_sample_data");
+ expect(SELECTORS.viewContent).toHaveProperty("innerHTML", content);
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+});
+
+test(`non empty gantt with sample="1"`, async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ searchViewArch: `
+
+
+
+ `,
+ });
+ expect(SELECTORS.viewContent).not.toHaveClass("o_view_sample_data");
+ expect(SELECTORS.cell).toHaveCount(12);
+ expect(SELECTORS.pill).toHaveCount(7);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+
+ await toggleSearchBarMenu();
+ await toggleMenuItem("False Domain");
+ expect(SELECTORS.viewContent).not.toHaveClass("o_view_sample_data");
+ expect(SELECTORS.pill).toHaveCount(0);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+ expect(SELECTORS.cell).toHaveCount(12);
+});
+
+test(`non empty grouped gantt with sample="1"`, async () => {
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ groupBy: ["project_id"],
+ searchViewArch: `
+
+
+
+ `,
+ });
+ expect(SELECTORS.viewContent).not.toHaveClass("o_view_sample_data");
+ expect(SELECTORS.cell).toHaveCount(24);
+ expect(SELECTORS.pill).toHaveCount(7);
+
+ await toggleSearchBarMenu();
+ await toggleMenuItem("False Domain");
+ expect(SELECTORS.viewContent).not.toHaveClass("o_view_sample_data");
+ expect(SELECTORS.pill).toHaveCount(0);
+ expect(SELECTORS.noContentHelper).toHaveCount(0);
+ expect(SELECTORS.cell).toHaveCount(12);
+});
+
+test("no content helper from action when no data and sample mode", async () => {
+ Tasks._records = [];
+ await mountGanttView({
+ resModel: "tasks",
+ arch: ``,
+ noContentHelp: markup(`click to add a partner
`),
+ });
+ expect(SELECTORS.noContentHelper).toHaveCount(1);
+ expect(`${SELECTORS.noContentHelper} p.hello:contains(add a partner)`).toHaveCount(1);
+});
diff --git a/addons_extensions/web_gantt/static/tests/web_gantt_test_helpers.js b/addons_extensions/web_gantt/static/tests/web_gantt_test_helpers.js
new file mode 100644
index 000000000..2240e33cb
--- /dev/null
+++ b/addons_extensions/web_gantt/static/tests/web_gantt_test_helpers.js
@@ -0,0 +1,580 @@
+import {
+ click,
+ hover,
+ queryAll,
+ queryAllTexts,
+ queryFirst,
+ queryOne,
+ queryText,
+ setInputRange,
+} from "@odoo/hoot-dom";
+import { advanceTime, animationFrame, runAllTimers } from "@odoo/hoot-mock";
+import { getPickerCell, zoomOut } from "@web/../tests/core/datetime/datetime_test_helpers";
+import { contains, mountView } from "@web/../tests/web_test_helpers";
+
+/**
+ * @typedef CellHelperOptions
+ * @property {number} [part=1] -- starts at 1
+ * @property {boolean} [ignoreHoverableClass=false]
+ */
+
+/**
+ * @typedef PillHelperOptions
+ * @property {number} [nth=1] -- starts at 1
+ */
+
+/**
+ * @typedef DragPillHelpers
+ * @property {() => Promise} cancel
+ * @property {(params: DragParams) => Promise} drop
+ * @property {(params: DragParams) => Promise} moveTo
+ */
+
+/**
+ * @template T
+ * @typedef {(columnHeader: string, rowHeader: string, options: CellHelperOptions) => T} CellHelper
+ */
+
+/**
+ * @template T
+ * @typedef {(text: string, options: PillHelperOptions) => T} PillHelper
+ */
+
+/** @typedef {CellHelperOptions & { row: number, column: number }} DragGridParams */
+
+/** @typedef {PillHelperOptions & { pill: string }} DragPillParams */
+
+/** @typedef {DragGridParams | DragPillParams} DragParams */
+
+/**
+ * @template {String} T
+ * @param {T} key
+ * @returns {`.${T}`}
+ */
+function makeClassSelector(key) {
+ return `.${key}`;
+}
+
+export const CLASSES = {
+ draggable: "o_draggable",
+ group: "o_gantt_group",
+ highlightedPill: "highlight",
+ resizable: "o_resizable",
+
+ // Connectors
+ highlightedConnector: "o_connector_highlighted",
+ highlightedConnectorCreator: "o_connector_creator_highlight",
+ lockedConnectorCreator: "o_connector_creator_lock", // Connector creators highlight for initial pill
+};
+
+export const SELECTORS = {
+ addButton: ".o_gantt_button_add",
+ cell: ".o_gantt_cell",
+ cellContainer: ".o_gantt_cells",
+ collapseButton: ".o_gantt_button_collapse_rows",
+ dense: ".fa-compress",
+ sparse: ".fa-expand",
+ draggable: makeClassSelector(CLASSES.draggable),
+ expandButton: ".o_gantt_button_expand_rows",
+ expandCollapseButtons: ".o_gantt_button_expand_rows, .o_gantt_button_collapse_rows",
+ group: makeClassSelector(CLASSES.group),
+ groupHeader: ".o_gantt_header_title",
+ columnHeader: ".o_gantt_header_cell",
+ highlightedPill: makeClassSelector(CLASSES.highlightedPill),
+ hoverable: ".o_gantt_hoverable",
+ noContentHelper: ".o_view_nocontent",
+ pill: ".o_gantt_pill",
+ pillWrapper: ".o_gantt_pill_wrapper",
+ progressBar: ".o_gantt_row_header .o_gantt_progress_bar",
+ progressBarBackground: ".o_gantt_row_header .o_gantt_progress_bar > span.bg-opacity-25",
+ progressBarForeground:
+ ".o_gantt_row_header .o_gantt_progress_bar > span > .o_gantt_group_hours",
+ progressBarWarning:
+ ".o_gantt_row_header .o_gantt_progress_bar > .o_gantt_group_hours > .fa-exclamation-triangle",
+ renderer: ".o_gantt_renderer",
+ resizable: makeClassSelector(CLASSES.resizable),
+ resizeBadge: ".o_gantt_pill_resize_badge",
+ resizeEndHandle: ".o_handle_end",
+ resizeHandle: ".o_resize_handle",
+ resizeStartHandle: ".o_handle_start",
+ rowHeader: ".o_gantt_row_header",
+ rowTitle: ".o_gantt_row_title",
+ rowTotal: ".o_gantt_row_total",
+ startDatePicker: ".o_gantt_picker:nth-child(2)",
+ stopDatePicker: ".o_gantt_picker:nth-child(4)",
+ thumbnail: ".o_gantt_row_thumbnail",
+ rangeMenu: ".o_gantt_range_menu",
+ rangeMenuToggler: ".o_gantt_renderer_controls div.dropdown:nth-child(2)",
+ todayButton: ".o_gantt_button_today",
+ toolbar: ".o_gantt_renderer_controls div[name='ganttToolbar']",
+ undraggable: ".o_undraggable",
+ view: ".o_gantt_view",
+ viewContent: ".o_gantt_view .o_content",
+ previousButton: ".o_gantt_renderer_controls button:has(> .fa-arrow-left)",
+ nextButton: ".o_gantt_renderer_controls button:has(> .fa-arrow-right)",
+ minusButton: ".o_gantt_renderer_controls button:has(> .fa-search-minus)",
+ plusButton: ".o_gantt_renderer_controls button:has(> .fa-search-plus)",
+
+ // Connectors
+ connector: ".o_gantt_connector",
+ connectorCreatorBullet: ".o_connector_creator_bullet",
+ connectorCreatorRight: ".o_connector_creator_right",
+ connectorCreatorWrapper: ".o_connector_creator_wrapper",
+ connectorRemoveButton: ".o_connector_stroke_remove_button",
+ connectorRescheduleButton: ".o_connector_stroke_reschedule_button",
+ connectorStroke: ".o_connector_stroke",
+ connectorStrokeButton: ".o_connector_stroke_button",
+ highlightedConnector: makeClassSelector(CLASSES.highlightedConnector),
+};
+
+export async function mountGanttView(params) {
+ const gantt = await mountView({ ...params, type: "gantt" });
+ await animationFrame();
+ return gantt;
+}
+
+export async function ganttControlsChanges() {
+ await runAllTimers();
+ await animationFrame();
+ await animationFrame(); // for potential focusDate
+}
+
+/**
+ * @param {string} selector
+ * @param {DateTime} datetime
+ */
+async function selectDateInDatePicker(selector, datetime) {
+ await contains(selector).click();
+ for (let i = 0; i < 3; i++) {
+ await zoomOut();
+ }
+ await contains(getPickerCell(datetime.year - (datetime.year % 10))).click();
+ await contains(getPickerCell(datetime.year)).click();
+ await contains(getPickerCell(datetime.monthShort)).click();
+ await contains(getPickerCell(datetime.day, true)).click();
+}
+
+/**
+ * @param {Object} param0
+ * @param {string} [param0.startDate]
+ * @param {string} [param0.stopDate]
+ */
+export async function selectGanttRange({ startDate, stopDate }) {
+ const {
+ startDatePicker: START_SELECTOR,
+ stopDatePicker: STOP_SELECTOR,
+ rangeMenuToggler,
+ } = SELECTORS;
+ await click(rangeMenuToggler);
+ await animationFrame();
+ if (startDate) {
+ await selectDateInDatePicker(START_SELECTOR, luxon.DateTime.fromISO(startDate));
+ }
+ if (stopDate) {
+ await selectDateInDatePicker(STOP_SELECTOR, luxon.DateTime.fromISO(stopDate));
+ }
+ await click(".dropdown-item button:contains(Apply)");
+ await ganttControlsChanges();
+}
+
+export async function selectRange(label) {
+ await click(SELECTORS.rangeMenuToggler);
+ await animationFrame();
+ await click(`${SELECTORS.rangeMenu} .dropdown-item:contains(/^${label}$/)`);
+ await ganttControlsChanges();
+}
+
+export function getActiveScale() {
+ return Number(queryFirst(".o_gantt_renderer_controls input").value);
+}
+
+/**
+ * @param {Number} scale
+ */
+export async function setScale(scale) {
+ await setInputRange(".o_gantt_renderer_controls input", scale);
+}
+
+export async function focusToday() {
+ await click(SELECTORS.todayButton);
+}
+
+/** @type {PillHelper>} */
+export async function dragPill(text, options) {
+ /**
+ * @param {DragParams} [params]
+ */
+ const drop = async (params) => {
+ if (params) {
+ await moveTo(params);
+ }
+ await dragActions.drop();
+ };
+
+ /**
+ * @param {DragParams} params
+ */
+ const moveTo = async (params) => {
+ let cell;
+ if (params?.column) {
+ cell = await hoverGridCell(params.column, params.row, params);
+ } else if (params?.pill) {
+ ({ cell } = await hoverPillCell(getPillWrapper(params.pill, params)));
+ }
+ return dragActions.moveTo(cell, {
+ position: getCellPositionOffset(cell, params.part),
+ relative: true,
+ });
+ };
+
+ const pill = getPillWrapper(text, options);
+ pill.scrollIntoView({ behavior: "instant", inline: "center" });
+ const { cell, part } = await hoverPillCell(pill);
+ const dragActions = await contains(pill).drag({
+ // D&D needs the correct initial position since it will attempt an implicit
+ // hover on the pill.
+ position: getCellPositionOffset(cell, part - 1),
+ relative: true,
+ });
+
+ return { ...dragActions, drop, moveTo };
+}
+
+/** @type {PillHelper>} */
+export async function editPill(text, options) {
+ await contains(getPill(text, options)).click();
+ await contains(".o_popover .popover-footer .btn-primary").click();
+}
+
+/**
+ * @param {string} header
+ */
+function findColumnFromHeader(header) {
+ const columnHeaders = getHeaders(SELECTORS.columnHeader);
+ const groupHeaders = getHeaders(SELECTORS.groupHeader);
+ const columnHeader = header.substring(0, header.indexOf(" "));
+ const groupHeader = header.substring(header.indexOf(" ") + 1);
+ const groupRange = groupHeaders.find((header) => header.title === groupHeader).range;
+ return columnHeaders.find(
+ (header) =>
+ header.title === columnHeader &&
+ header.range[0] >= groupRange[0] &&
+ header.range[1] <= groupRange[1]
+ ).range[0];
+}
+
+/** @type {CellHelper} */
+export function getCell(columnHeader, rowHeader = null, options) {
+ const columnIndex = findColumnFromHeader(columnHeader);
+ const cells = queryAll(`${SELECTORS.cell}[data-col='${columnIndex}']`);
+ if (!cells.length) {
+ throw new Error(`Could not find cell at column ${columnHeader}`);
+ }
+ if (rowHeader === null) {
+ return cells[0];
+ }
+ const row = queryAll(`.o_gantt_row_header:contains(${rowHeader})`)?.[(options?.num || 1) - 1];
+ if (!row) {
+ throw new Error(`Could not find row ${rowHeader}`);
+ }
+ const rowId = row.getAttribute("data-row-id");
+ return cells.find((cell) => cell.getAttribute("data-row-id") === rowId);
+}
+
+/** @type {CellHelper} */
+export function getCellColorProperties(columnHeader, rowHeader = null, options) {
+ const cell = getCell(columnHeader, rowHeader, options);
+ const cssVarRegex = /(--[\w-]+)/g;
+
+ if (cell.style.background) {
+ return cell.style.background.match(cssVarRegex);
+ } else if (cell.style.backgroundColor) {
+ return cell.style.backgroundColor.match(cssVarRegex);
+ } else if (cell.style.backgroundImage) {
+ return cell.style.backgroundImage.match(cssVarRegex);
+ }
+
+ return [];
+}
+
+/**
+ * @param {HTMLElement} pill
+ * @returns {HTMLElement}
+ */
+export function getCellFromPill(pill) {
+ if (!pill.matches(SELECTORS.pillWrapper)) {
+ pill = pill.closest(SELECTORS.pillWrapper);
+ }
+ const { row, column } = getGridStyle(pill);
+ for (const cell of queryAll(SELECTORS.cell)) {
+ const { row: cellRow, column: cellColumn } = getGridStyle(cell);
+ if (row[0] < cellRow[1] && column[0] < cellColumn[1]) {
+ return cell;
+ }
+ }
+ throw new Error(`Could not find hoverable cell for pill "${queryText(pill)}".`);
+}
+
+/**
+ * @param {string} str
+ */
+function parseNumber(str) {
+ return parseInt(str.match(/\d+/)?.[0]) || 1;
+}
+
+/**
+ * @param {string} selector
+ */
+function getHeaders(selector) {
+ const groupHeaders = [];
+ for (const el of queryAll(selector)) {
+ const { column: range } = getGridStyle(el);
+ groupHeaders.push({
+ range,
+ title: el.textContent,
+ });
+ }
+ return groupHeaders;
+}
+
+export function getGridContent() {
+ const columnHeaders = getHeaders(SELECTORS.columnHeader);
+ const groupHeaders = getHeaders(SELECTORS.groupHeader);
+ const range = queryAllTexts(SELECTORS.rangeMenuToggler)[0] || null;
+ const viewTitle = queryAllTexts(".o_gantt_title")[0] || null;
+ const colsRange = queryFirst(SELECTORS.columnHeader)
+ .style.getPropertyValue("grid-column")
+ .split("/");
+ const cellParts = parseNumber(colsRange[1]) - parseNumber(colsRange[0]);
+ const pillEls = new Set(queryAll(`${SELECTORS.cellContainer} ${SELECTORS.pillWrapper}`));
+ const rowEls = queryAll(`.o_gantt_row_headers > ${SELECTORS.rowHeader}`);
+ const singleRowMode = rowEls.length === 0;
+ if (singleRowMode) {
+ rowEls.push(document.createElement("div"));
+ }
+ const totalRow = queryFirst(SELECTORS.rowTotal);
+ const totalPillEls = new Set(queryAll(`.o_gantt_row_total ${SELECTORS.pillWrapper}`));
+ if (totalRow) {
+ totalRow._isTotal = true;
+ rowEls.push(totalRow);
+ }
+ const rows = [];
+ for (const rowEl of rowEls) {
+ const isGroup = rowEl.classList.contains(CLASSES.group);
+ const { row: gridRow } = getGridStyle(rowEl);
+ const row = singleRowMode ? {} : { title: queryText(rowEl) };
+ if (isGroup) {
+ row.isGroup = true;
+ }
+ if (rowEl._isTotal) {
+ row.isTotalRow = true;
+ }
+ const pills = [];
+ for (const pillEl of rowEl._isTotal ? totalPillEls : pillEls) {
+ const pillRowLevel = parseNumber(pillEl.style.gridRowStart);
+ const { column: gridColumn } = getGridStyle(pillEl);
+ const pillInRow = pillRowLevel >= gridRow[0] && pillRowLevel < gridRow[1];
+ if (singleRowMode || pillInRow || rowEl._isTotal) {
+ let start = columnHeaders.find(
+ (header) => gridColumn[0] >= header.range[0] && gridColumn[0] < header.range[1]
+ )?.title;
+ let end = columnHeaders.find(
+ (header) => gridColumn[1] > header.range[0] && gridColumn[1] <= header.range[1]
+ )?.title;
+ const startPart = (gridColumn[0] - 1) % cellParts;
+ const endPart = (gridColumn[1] - 1) % cellParts;
+ if (startPart && start) {
+ start += ` (${startPart}/${cellParts})`;
+ }
+ if (endPart && end) {
+ end += ` (${endPart}/${cellParts})`;
+ }
+ const pill = {
+ title: queryText(pillEl),
+ colSpan: `${start || "Out of bounds (" + gridColumn[0] + ")"} ${
+ start
+ ? groupHeaders.find(
+ (header) =>
+ gridColumn[0] >= header.range[0] &&
+ gridColumn[0] < header.range[1]
+ ).title
+ : ""
+ } -> ${end || "Out of bounds (" + gridColumn[1] + ")"} ${
+ end
+ ? groupHeaders.find(
+ (header) =>
+ gridColumn[1] > header.range[0] &&
+ gridColumn[1] <= header.range[1]
+ ).title
+ : ""
+ }`,
+ };
+ if (!isGroup) {
+ pill.level = singleRowMode ? pillRowLevel - 1 : pillRowLevel - gridRow[0];
+ }
+ pills.push(pill);
+ pillEls.delete(pillEl);
+ }
+ }
+ if (pills.length) {
+ row.pills = pills;
+ }
+ rows.push(row);
+ }
+
+ return { columnHeaders, groupHeaders, range, rows, viewTitle };
+}
+
+/**
+ * @param {HTMLElement} el
+ */
+export function getGridStyle(el) {
+ /**
+ * @param {"row" | "column"} prop
+ * @returns {[number, number]}
+ */
+ const getGridProp = (prop) => {
+ return [
+ parseNumber(style.getPropertyValue(`grid-${prop}-start`)),
+ parseNumber(style.getPropertyValue(`grid-${prop}-end`)),
+ ];
+ };
+
+ const style = getComputedStyle(el);
+
+ return {
+ row: getGridProp("row"),
+ column: getGridProp("column"),
+ };
+}
+
+function getCellPositionOffset(cell, part) {
+ const position = { x: 1 };
+ if (part > 1) {
+ const rect = cell.getBoundingClientRect();
+ // Calculate cell parts
+ const colsRange = queryFirst(SELECTORS.columnHeader)
+ .style.getPropertyValue("grid-column")
+ .split("/");
+ const cellParts = parseNumber(colsRange[1]) - parseNumber(colsRange[0]);
+ const partWidth = rect.width / cellParts;
+ position.x += Math.ceil(partWidth * (part - 1));
+ }
+ return position;
+}
+
+/**
+ * @param {HTMLElement} cell
+ * @param {CellHelperOptions} [options]
+ */
+async function hoverCell(cell, options) {
+ const part = options?.part ?? 1;
+ await hover(cell, { position: getCellPositionOffset(cell, part), relative: true });
+ await animationFrame();
+ await advanceTime(1000);
+}
+
+/**
+ * Hovers a cell found from given grid coordinates.
+ * @type {CellHelper>}
+ */
+export async function hoverGridCell(columnHeader, rowHeader = null, options) {
+ const cell = getCell(columnHeader, rowHeader, options);
+ await hoverCell(cell, options);
+ return cell;
+}
+
+/**
+ * Click on a cell found from given grid coordinates.
+ * @type {CellHelper>}
+ */
+export async function clickCell(columnHeader, rowHeader = null, options) {
+ const cell = getCell(columnHeader, rowHeader, options);
+ await contains(cell).click();
+}
+
+/**
+ * Hovers a cell found from a pill element.
+ * @param {HTMLElement} pill
+ */
+async function hoverPillCell(pill) {
+ const cell = getCellFromPill(pill);
+ const pStart = getGridStyle(pill).column[0];
+ const cellStyle = getGridStyle(cell).column[0];
+ const part = pStart - cellStyle + 1;
+ await hoverCell(cell, { part });
+ return { cell, part };
+}
+
+/**
+ * @param {HTMLElement} pill
+ * @param {"start" | "end"} side
+ * @param {number | { x: number }} deltaOrPosition
+ * @param {boolean} [shouldDrop=true]
+ */
+export async function resizePill(pill, side, deltaOrPosition, shouldDrop = true) {
+ await hover(pill);
+
+ const { row, column } = getGridStyle(pill);
+
+ // Calculate cell parts
+ const colsRange = queryFirst(SELECTORS.columnHeader)
+ .style.getPropertyValue("grid-column")
+ .split("/");
+ const cellParts = parseNumber(colsRange[1]) - parseNumber(colsRange[0]);
+
+ // Calculate delta or position
+ const delta = typeof deltaOrPosition === "object" ? 0 : deltaOrPosition;
+ const position = typeof deltaOrPosition === "object" ? deltaOrPosition : {};
+ const targetColumn = (side === "start" ? column[0] : column[1]) + delta * cellParts;
+
+ let targetCell;
+ let targetPart;
+ for (const cell of queryAll(SELECTORS.cell)) {
+ const { row: cRow, column: cCol } = getGridStyle(cell);
+ if (cRow[0] > row[0] || cRow[1] < row[1]) {
+ continue;
+ }
+ if (cCol[1] < targetColumn) {
+ continue;
+ }
+
+ if (targetColumn < cCol[0]) {
+ break;
+ }
+
+ targetCell = cell;
+ targetPart = targetColumn - cCol[0];
+ }
+
+ // Assign position if delta
+ if (!position.x) {
+ const { width } = targetCell.getBoundingClientRect();
+ position.x = targetPart * Math.floor(width / cellParts);
+ }
+
+ // Actual drag actions
+ const { moveTo, drop } = await contains(
+ pill.querySelector(
+ side === "start" ? SELECTORS.resizeStartHandle : SELECTORS.resizeEndHandle
+ )
+ ).drag();
+
+ await moveTo(targetCell, { position, relative: true });
+
+ if (shouldDrop) {
+ await drop();
+ } else {
+ return drop;
+ }
+}
+
+/** @type {PillHelper} */
+export function getPill(text, options) {
+ return queryOne(`${SELECTORS.pill}:contains(${text}):eq(${(options?.nth ?? 1) - 1})`);
+}
+
+/** @type {PillHelper} */
+export function getPillWrapper(text, options) {
+ return getPill(text, options).closest(SELECTORS.pillWrapper);
+}
diff --git a/addons_extensions/web_gantt/tests/__init__.py b/addons_extensions/web_gantt/tests/__init__.py
new file mode 100644
index 000000000..f04b5cae1
--- /dev/null
+++ b/addons_extensions/web_gantt/tests/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import test_acl
diff --git a/addons_extensions/web_gantt/tests/test_acl.py b/addons_extensions/web_gantt/tests/test_acl.py
new file mode 100644
index 000000000..619cd6abb
--- /dev/null
+++ b/addons_extensions/web_gantt/tests/test_acl.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from lxml import etree
+
+from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
+
+
+class TestACL(TransactionCaseWithUserDemo):
+ def setUp(self):
+ super().setUp()
+ self.user_manager = self.env['res.users'].create({
+ 'login': 'demo123',
+ 'password': 'demo',
+ 'partner_id': self.partner_demo.id,
+ 'groups_id': [(6, 0, [self.env.ref('base.group_system').id])],
+ })
+ self.env["ir.ui.view"].create({
+ "name": "Add delete attribute on gantt view",
+ "model": "res.company",
+ "type": 'gantt',
+ "arch": """
+
+
+
+ """,
+ })
+
+ def test_view_delete_button_visibility(self):
+ # the demo user can't unlink
+ company_view = self.env['res.company']\
+ .with_user(self.user_demo)\
+ .get_view(False, 'gantt')
+ view_arch = etree.fromstring(company_view['arch'])
+ self.assertEqual(view_arch.get('delete'), 'False')
+
+ # the manager user can unlink
+ company_view = self.env['res.company']\
+ .with_user(self.user_manager)\
+ .get_view(False, 'gantt')
+ view_arch = etree.fromstring(company_view['arch'])
+ self.assertIsNone(view_arch.get('delete'))