)]}'
{"version": 3, "sources": ["/web/static/src/views/graph/graph_arch_parser.js", "/web/static/src/views/graph/graph_controller.js", "/web/static/src/views/graph/graph_model.js", "/web/static/src/views/graph/graph_renderer.js", "/web/static/src/views/graph/graph_search_model.js", "/web/static/src/views/graph/graph_view.js", "/web/static/src/views/pivot/pivot_arch_parser.js", "/web/static/src/views/pivot/pivot_controller.js", "/web/static/src/views/pivot/pivot_header.js", "/web/static/src/views/pivot/pivot_model.js", "/web/static/src/views/pivot/pivot_renderer.js", "/web/static/src/views/pivot/pivot_search_model.js", "/web/static/src/views/pivot/pivot_view.js", "/mail/static/src/views/web/activity/activity_arch_parser.js", "/mail/static/src/views/web/activity/activity_cell.js", "/mail/static/src/views/web/activity/activity_compiler.js", "/mail/static/src/views/web/activity/activity_controller.js", "/mail/static/src/views/web/activity/activity_model.js", "/mail/static/src/views/web/activity/activity_record.js", "/mail/static/src/views/web/activity/activity_renderer.js", "/mail/static/src/views/web/activity/activity_view.js", "/crm/static/src/views/forecast_graph/forecast_graph_view.js", "/crm/static/src/views/forecast_pivot/forecast_pivot_view.js", "/stock/static/src/stock_forecasted/forecasted_graph.js", "/web_enterprise/static/src/views/pivot/pivot_renderer.js", "/web_map/static/src/map_view/map_arch_parser.js", "/web_map/static/src/map_view/map_controller.js", "/web_map/static/src/map_view/map_model.js", "/web_map/static/src/map_view/map_renderer.js", "/web_map/static/src/map_view/map_view.js", "/web_gantt/static/src/gantt_arch_parser.js", "/web_gantt/static/src/gantt_compiler.js", "/web_gantt/static/src/gantt_connector.js", "/web_gantt/static/src/gantt_controller.js", "/web_gantt/static/src/gantt_helpers.js", "/web_gantt/static/src/gantt_mock_server.js", "/web_gantt/static/src/gantt_model.js", "/web_gantt/static/src/gantt_popover.js", "/web_gantt/static/src/gantt_popover_in_dialog.js", "/web_gantt/static/src/gantt_renderer.js", "/web_gantt/static/src/gantt_renderer_controls.js", "/web_gantt/static/src/gantt_resize_badge.js", "/web_gantt/static/src/gantt_row_progress_bar.js", "/web_gantt/static/src/gantt_sample_server.js", "/web_gantt/static/src/gantt_view.js", "/web_cohort/static/src/cohort_arch_parser.js", "/web_cohort/static/src/cohort_controller.js", "/web_cohort/static/src/cohort_model.js", "/web_cohort/static/src/cohort_renderer.js", "/web_cohort/static/src/cohort_view.js", "/web_cohort/static/src/cohort_view_sample_server.js", "/hr/static/src/views/hr_graph_view.js", "/hr/static/src/views/hr_pivot_view.js", "/web_hierarchy/static/src/hierarchy_arch_parser.js", "/web_hierarchy/static/src/hierarchy_card.js", "/web_hierarchy/static/src/hierarchy_compiler.js", "/web_hierarchy/static/src/hierarchy_controller.js", "/web_hierarchy/static/src/hierarchy_model.js", "/web_hierarchy/static/src/hierarchy_node_draggable.js", "/web_hierarchy/static/src/hierarchy_renderer.js", "/web_hierarchy/static/src/hierarchy_view.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_card.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_model.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_renderer.js", "/knowledge/static/src/views/hierarchy/knowledge_hierarchy_view.js", "/helpdesk/static/src/views/helpdesk_ticket_graph/helpdesk_ticket_graph_model.js", "/helpdesk/static/src/views/helpdesk_ticket_graph/helpdesk_ticket_graph_view.js", "/helpdesk/static/src/views/helpdesk_ticket_pivot/helpdesk_ticket_pivot_model.js", "/helpdesk/static/src/views/helpdesk_ticket_pivot/helpdesk_ticket_pivot_view.js", "/hr_skills/static/src/views/skills_graph.js", "/web_grid/static/src/components/float_factor_grid_cell.js", "/web_grid/static/src/components/float_time_grid_cell.js", "/web_grid/static/src/components/float_toggle_grid_cell.js", "/web_grid/static/src/components/grid_cell.js", "/web_grid/static/src/components/grid_component/grid_component.js", "/web_grid/static/src/components/grid_row/grid_row.js", "/web_grid/static/src/components/many2one_grid_row/many2one_grid_row.js", "/web_grid/static/src/hooks/grid_cell_hook.js", "/web_grid/static/src/hooks/input_hook.js", "/web_grid/static/src/views/grid_arch_parser.js", "/web_grid/static/src/views/grid_controller.js", "/web_grid/static/src/views/grid_model.js", "/web_grid/static/src/views/grid_renderer.js", "/web_grid/static/src/views/grid_view.js", "/analytic_enterprise/static/src/analytic_line_grid/analytic_line_grid_model.js", "/analytic_enterprise/static/src/analytic_line_grid/analytic_line_grid_view.js", "/hr_gantt/static/src/hr_gantt_employee_avatar.js", "/hr_gantt/static/src/hr_gantt_renderer.js", "/hr_gantt/static/src/hr_gantt_view.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_card.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_renderer.js", "/hr_org_chart/static/src/views/hr_employee_hierarchy/hr_employee_hierarchy_view.js", "/spreadsheet_edition/static/src/assets/graph_view/graph_view.js", "/spreadsheet_edition/static/src/assets/pivot_view/pivot_view.js", "/stock_enterprise/static/src/map_view/map_model.js", "/stock_enterprise/static/src/map_view/map_renderer.js", "/stock_enterprise/static/src/map_view/map_view.js"], "mappings": "AAAA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACznBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC95BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3FA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChrBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5UA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjMA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzuBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjmCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1+EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjIA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/GA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7EA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACrEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9nCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AClBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACxKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzJA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnLA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3LA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtqCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtnBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvEA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACfA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACdA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nconst MODES = [\"bar\", \"line\", \"pie\"];\nconst ORDERS = [\"ASC\", \"DESC\", \"asc\", \"desc\", null];\n\nexport class GraphArchParser {\n    parse(arch, fields = {}) {\n        const archInfo = { fields, fieldAttrs: {}, groupBy: [], measures: [] };\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"graph\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    if (node.hasAttribute(\"stacked\")) {\n                        archInfo.stacked = exprToBoolean(node.getAttribute(\"stacked\"));\n                    }\n                    if (node.hasAttribute(\"cumulated\")) {\n                        archInfo.cumulated = exprToBoolean(node.getAttribute(\"cumulated\"));\n                    }\n                    if (node.hasAttribute(\"cumulated_start\")) {\n                        archInfo.cumulatedStart = exprToBoolean(\n                            node.getAttribute(\"cumulated_start\")\n                        );\n                    }\n                    const mode = node.getAttribute(\"type\");\n                    if (mode && MODES.includes(mode)) {\n                        archInfo.mode = mode;\n                    }\n                    const order = node.getAttribute(\"order\");\n                    if (order && ORDERS.includes(order)) {\n                        archInfo.order = order.toUpperCase();\n                    }\n                    const title = node.getAttribute(\"string\");\n                    if (title) {\n                        archInfo.title = title;\n                    }\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n                    if (fieldName === \"id\") {\n                        break;\n                    }\n                    const string = node.getAttribute(\"string\");\n                    if (string) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].string = string;\n                    }\n                    const widget = node.getAttribute(\"widget\");\n                    if (widget) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].widget = widget;\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        if (!archInfo.fieldAttrs[fieldName]) {\n                            archInfo.fieldAttrs[fieldName] = {};\n                        }\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n                    const isMeasure = node.getAttribute(\"type\") === \"measure\";\n                    if (isMeasure) {\n                        archInfo.measures.push(fieldName);\n                        // the last field with type=\"measure\" (if any) will be used as measure else __count\n                        archInfo.measure = fieldName;\n                    } else {\n                        const { type } = archInfo.fields[fieldName]; // exists (rng validation)\n                        if (GROUPABLE_TYPES.includes(type)) {\n                            let groupBy = fieldName;\n                            const interval = node.getAttribute(\"interval\");\n                            if (interval) {\n                                groupBy += `:${interval}`;\n                            }\n                            archInfo.groupBy.push(groupBy);\n                        }\n                    }\n                    break;\n                }\n            }\n        });\n        return archInfo;\n    }\n}\n", "import { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class GraphController extends Component {\n    static template = \"web.GraphView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                return { metaData: this.model.metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    /**\n     * @returns {Object}\n     */\n    getContext() {\n        // expand context object? change keys?\n        const { measure, groupBy, mode } = this.model.metaData;\n        const context = {\n            graph_measure: measure,\n            graph_mode: mode,\n            graph_groupbys: groupBy.map((gb) => gb.spec),\n        };\n        if (mode !== \"pie\") {\n            context.graph_order = this.model.metaData.order;\n            context.graph_stacked = this.model.metaData.stacked;\n            if (mode === \"line\") {\n                context.graph_cumulated = this.model.metaData.cumulated;\n            }\n        }\n        return context;\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { sortBy, groupBy } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { rankInterval } from \"@web/search/utils/dates\";\nimport { getGroupBy } from \"@web/search/utils/group_by\";\nimport { GROUPABLE_TYPES } from \"@web/search/utils/misc\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\nimport { Domain } from \"@web/core/domain\";\n\nexport const SEP = \" / \";\n\nexport const SEQUENTIAL_TYPES = [\"date\", \"datetime\"];\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nclass DateClasses {\n    // We view the param \"array\" as a matrix of values and undefined.\n    // An equivalence class is formed of defined values of a column.\n    // So nothing has to do with dates but we only use Dateclasses to manage\n    // identification of dates.\n    /**\n     * @param {(any[])[]} array\n     */\n    constructor(array) {\n        this.__referenceIndex = null;\n        this.__array = array;\n        for (let i = 0; i < this.__array.length; i++) {\n            const arr = this.__array[i];\n            if (arr.length && this.__referenceIndex === null) {\n                this.__referenceIndex = i;\n            }\n        }\n    }\n\n    /**\n     * @param {number} index\n     * @param {any} o\n     * @returns {string}\n     */\n    classLabel(index, o) {\n        return `${this.__array[index].indexOf(o)}`;\n    }\n\n    /**\n     * @param {string} classLabel\n     * @returns {any[]}\n     */\n    classMembers(classLabel) {\n        const classNumber = Number(classLabel);\n        const classMembers = new Set();\n        for (const arr of this.__array) {\n            if (arr[classNumber] !== undefined) {\n                classMembers.add(arr[classNumber]);\n            }\n        }\n        return [...classMembers];\n    }\n\n    /**\n     * @param {string} classLabel\n     * @param {number} [index]\n     * @returns {any}\n     */\n    representative(classLabel, index) {\n        const classNumber = Number(classLabel);\n        const i = index === undefined ? this.__referenceIndex : index;\n        if (i === null) {\n            return null;\n        }\n        return this.__array[i][classNumber];\n    }\n\n    /**\n     * @param {number} index\n     * @returns {number}\n     */\n    arrayLength(index) {\n        return this.__array[index].length;\n    }\n}\n\nexport class GraphModel extends Model {\n    /**\n     * @override\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _fetchDataPoints = this._fetchDataPoints.bind(this);\n        this._fetchDataPoints = (...args) => {\n            return this.race.add(_fetchDataPoints(...args));\n        };\n\n        this.initialGroupBy = null;\n\n        this.metaData = params;\n        this.data = null;\n        this.searchParams = null;\n        // This dataset will be added as a line plot on top of stacked bar chart.\n        this.lineOverlayDataset = null;\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        if (!this.initialGroupBy) {\n            this.initialGroupBy = searchParams.context.graph_groupbys || this.metaData.groupBy; // = arch groupBy --> change that\n        }\n        const metaData = this._buildMetaData();\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            metaData.groupBy.map((gb) => gb.fieldName)\n        );\n        await this._fetchDataPoints(metaData);\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.dataPoints.length > 0;\n    }\n\n    /**\n     * Only supposed to be called to change one or several parameters among\n     * \"measure\", \"mode\", \"order\", \"stacked\" and \"cumulated\".\n     * @param {Object} params\n     */\n    async updateMetaData(params) {\n        if (\"measure\" in params) {\n            const metaData = this._buildMetaData(params);\n            await this._fetchDataPoints(metaData);\n            this.useSampleModel = false;\n        } else {\n            await this.race.getCurrentProm();\n            this.metaData = Object.assign({}, this.metaData, params);\n            this._prepareData();\n        }\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     * @param {Object} [params={}]\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const { comparison, domain, context, groupBy } = this.searchParams;\n\n        const metaData = Object.assign({}, this.metaData, { context });\n        if (comparison) {\n            metaData.domains = comparison.domains;\n            metaData.comparisonField = comparison.fieldName;\n        } else {\n            metaData.domains = [{ arrayRepr: domain, description: null }];\n        }\n        metaData.measure = context.graph_measure || metaData.measure;\n        metaData.mode = context.graph_mode || metaData.mode;\n        metaData.groupBy = groupBy.length ? groupBy : this.initialGroupBy;\n        if (metaData.mode !== \"pie\") {\n            metaData.order = \"graph_order\" in context ? context.graph_order : metaData.order;\n            if (comparison) {\n                metaData.stacked = false;\n            } else if (\"graph_stacked\" in context) {\n                metaData.stacked = context.graph_stacked;\n            }\n            if (metaData.mode === \"line\") {\n                metaData.cumulated =\n                    \"graph_cumulated\" in context ? context.graph_cumulated : metaData.cumulated;\n            }\n        }\n\n        this._normalize(metaData);\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...(metaData.viewMeasures || []),\n            metaData.measure,\n        ]);\n\n        return Object.assign(metaData, params);\n    }\n\n    /**\n     * Fetch the data points determined by the metaData. This function has\n     * several side effects. It can alter this.metaData and set this.dataPoints.\n     * @protected\n     * @param {Object} metaData\n     */\n    async _fetchDataPoints(metaData) {\n        this.dataPoints = await this.keepLast.add(this._loadDataPoints(metaData));\n        this.metaData = metaData;\n        this._prepareData();\n    }\n\n    /**\n     * Separates dataPoints coming from the read_group(s) into different\n     * datasets. This function returns the parameters data and labels used\n     * to produce the charts.\n     * @protected\n     * @param {Object[]}\n     * @returns {Object}\n     */\n    _getData(dataPoints) {\n        const { comparisonField, groupBy, mode } = this.metaData;\n\n        let identify = false;\n        if (comparisonField && groupBy.length && groupBy[0].fieldName === comparisonField) {\n            identify = true;\n        }\n        const dateClasses = identify ? this._getDateClasses(dataPoints) : null;\n\n        // dataPoints --> labels\n        let labels = [];\n        const labelMap = {};\n        for (const dataPt of dataPoints) {\n            const x = dataPt.labels.slice(0, mode === \"pie\" ? undefined : 1);\n            const trueLabel = x.length ? x.join(SEP) : _t(\"Total\");\n            if (dateClasses) {\n                x[0] = dateClasses.classLabel(dataPt.originIndex, x[0]);\n            }\n            const key = JSON.stringify(x);\n            if (labelMap[key] === undefined) {\n                labelMap[key] = labels.length;\n                if (dateClasses) {\n                    if (mode === \"pie\") {\n                        x[0] = dateClasses.classMembers(x[0]).join(\", \");\n                    } else {\n                        x[0] = dateClasses.representative(x[0]);\n                    }\n                }\n                const label = x.length ? x.join(SEP) : _t(\"Total\");\n                labels.push(label);\n            }\n            dataPt.labelIndex = labelMap[key];\n            dataPt.trueLabel = trueLabel;\n        }\n\n        // dataPoints + labels --> datasetsTmp --> datasets\n        const datasetsTmp = {};\n        for (const dataPt of dataPoints) {\n            const {\n                domain,\n                labelIndex,\n                originIndex,\n                trueLabel,\n                value,\n                identifier,\n                cumulatedStart,\n            } = dataPt;\n            const datasetLabel = this._getDatasetLabel(dataPt);\n            if (!(datasetLabel in datasetsTmp)) {\n                let dataLength = labels.length;\n                if (mode !== \"pie\" && dateClasses) {\n                    dataLength = dateClasses.arrayLength(originIndex);\n                }\n                datasetsTmp[datasetLabel] = {\n                    data: new Array(dataLength).fill(0),\n                    cumulatedStart,\n                    trueLabels: labels.slice(0, dataLength), // should be good // check this in case identify = true\n                    domains: new Array(dataLength).fill([]),\n                    label: datasetLabel,\n                    originIndex: originIndex,\n                    identifiers: new Set(),\n                };\n            }\n            datasetsTmp[datasetLabel].data[labelIndex] = value;\n            datasetsTmp[datasetLabel].domains[labelIndex] = domain;\n            datasetsTmp[datasetLabel].trueLabels[labelIndex] = trueLabel;\n            datasetsTmp[datasetLabel].identifiers.add(identifier);\n        }\n        // sort by origin\n        let datasets = sortBy(Object.values(datasetsTmp), \"originIndex\");\n\n        if (mode === \"pie\") {\n            // We kinda have a matrix. We remove the zero columns and rows. This is a global operation.\n            // That's why it cannot be done before.\n            datasets = datasets.filter((dataset) => dataset.data.some((v) => Boolean(v)));\n            const labelsToKeepIndexes = {};\n            labels.forEach((_, index) => {\n                if (datasets.some((dataset) => Boolean(dataset.data[index]))) {\n                    labelsToKeepIndexes[index] = true;\n                }\n            });\n            labels = labels.filter((_, index) => labelsToKeepIndexes[index]);\n            for (const dataset of datasets) {\n                dataset.data = dataset.data.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.domains = dataset.domains.filter((_, index) => labelsToKeepIndexes[index]);\n                dataset.trueLabels = dataset.trueLabels.filter(\n                    (_, index) => labelsToKeepIndexes[index]\n                );\n            }\n        }\n\n        return { datasets, labels };\n    }\n\n    _getLabel(description) {\n        if (!description) {\n            return _t(\"Sum\");\n        } else {\n            return _t(\"Sum (%s)\", description);\n        }\n    }\n\n    _getLineOverlayDataset() {\n        const { domains, stacked } = this.metaData;\n        const data = this.data;\n        let lineOverlayDataset = null;\n        if (stacked) {\n            const stacks = groupBy(data.datasets, (dataset) => dataset.originIndex);\n            if (Object.keys(stacks).length == 1) {\n                const [[originIndex, datasets]] = Object.entries(stacks);\n                if (datasets.length > 1) {\n                    const data = [];\n                    for (const dataset of datasets) {\n                        for (let i = 0; i < dataset.data.length; i++) {\n                            data[i] = (data[i] || 0) + dataset.data[i];\n                        }\n                    }\n                    lineOverlayDataset = {\n                        label: this._getLabel(domains[originIndex].description),\n                        data,\n                        trueLabels: datasets[0].trueLabels,\n                    };\n                }\n            }\n        }\n        return lineOverlayDataset;\n    }\n\n    /**\n     * Determines the dataset to which the data point belongs.\n     * @protected\n     * @param {Object} dataPoint\n     * @returns {string}\n     */\n    _getDatasetLabel(dataPoint) {\n        const { measure, measures, domains, mode } = this.metaData;\n        const { labels, originIndex } = dataPoint;\n        if (mode === \"pie\") {\n            return domains[originIndex].description || \"\";\n        }\n        // ([origin] + second to last groupBys) or measure\n        let datasetLabel = labels.slice(1).join(SEP);\n        if (domains.length > 1) {\n            datasetLabel =\n                domains[originIndex].description + (datasetLabel ? SEP + datasetLabel : \"\");\n        }\n        datasetLabel = datasetLabel || measures[measure].string;\n        return datasetLabel;\n    }\n\n    /**\n     * @protected\n     * @param {Object[]} dataPoints\n     * @returns {DateClasses}\n     */\n    _getDateClasses(dataPoints) {\n        const { domains } = this.metaData;\n        const dateSets = domains.map(() => new Set());\n        for (const { labels, originIndex } of dataPoints) {\n            const date = labels[0];\n            dateSets[originIndex].add(date);\n        }\n        const arrays = dateSets.map((dateSet) => [...dateSet]);\n        return new DateClasses(arrays);\n    }\n\n    /**\n     * @protected\n     * @returns {string}\n     */\n    _getDefaultFilterLabel(field) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Eventually filters and sort data points.\n     * @protected\n     * @returns {Object[]}\n     */\n    _getProcessedDataPoints() {\n        const { domains, groupBy, mode, order } = this.metaData;\n        let processedDataPoints = [];\n        if (mode === \"line\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.labels[0] !== this._getDefaultFilterLabel(groupBy[0])\n            );\n        } else if (mode === \"pie\") {\n            processedDataPoints = this.dataPoints.filter(\n                (dataPoint) => dataPoint.value > 0 && dataPoint.count !== 0\n            );\n        } else {\n            processedDataPoints = this.dataPoints.filter((dataPoint) => dataPoint.count !== 0);\n        }\n\n        if (order !== null && mode !== \"pie\" && domains.length === 1 && groupBy.length > 0) {\n            // group data by their x-axis value, and then sort datapoints\n            // based on the sum of values by group in ascending/descending order\n            const groupedDataPoints = {};\n            for (const dataPt of processedDataPoints) {\n                const key = dataPt.labels[0]; // = x-axis value under the current assumptions\n                if (!groupedDataPoints[key]) {\n                    groupedDataPoints[key] = [];\n                }\n                groupedDataPoints[key].push(dataPt);\n            }\n            const groups = Object.values(groupedDataPoints);\n            const groupTotal = (group) => group.reduce((sum, dataPt) => sum + dataPt.value, 0);\n            processedDataPoints = sortBy(groups, groupTotal, order.toLowerCase()).flat();\n        }\n\n        return processedDataPoints;\n    }\n\n    /**\n     * Fetch and process graph data.  It is basically a(some) read_group(s)\n     * with correct fields for each domain.  We have to do some light processing\n     * to separate date groups in the field list, because they can be defined\n     * with an aggregation function, such as my_date:week.\n     * @protected\n     * @param {Object} metaData\n     * @returns {Object[]}\n     */\n    async _loadDataPoints(metaData) {\n        const { measure, domains, fields, groupBy, resModel, cumulatedStart } = metaData;\n        const fieldName = groupBy[0]?.fieldName;\n        const sequential_field =\n            cumulatedStart && SEQUENTIAL_TYPES.includes(fields[fieldName]?.type) ? fieldName : null;\n        const sequential_spec = sequential_field && groupBy[0].spec;\n        const measures = [\"__count\"];\n        if (measure !== \"__count\") {\n            let { aggregator, type } = fields[measure];\n            if (type === \"many2one\") {\n                aggregator = \"count_distinct\";\n            }\n            if (aggregator === undefined) {\n                throw new Error(\n                    `No aggregate function has been provided for the measure '${measure}'`\n                );\n            }\n            measures.push(`${measure}:${aggregator}`);\n        }\n\n        const numbering = {}; // used to avoid ambiguity with many2one with values with same labels:\n        // for instance [1, \"ABC\"] [3, \"ABC\"] should be distinguished.\n\n        const proms = domains.map(async (domain, originIndex) => {\n            const data = await this.orm.webReadGroup(\n                resModel,\n                domain.arrayRepr,\n                measures,\n                groupBy.map((gb) => gb.spec),\n                {\n                    lazy: false, // what is this thing???\n                    context: { fill_temporal: true, ...this.searchParams.context },\n                }\n            );\n            let start = false;\n            if (\n                cumulatedStart &&\n                sequential_field &&\n                data.groups.length &&\n                domain.arrayRepr.some((leaf) => leaf.length === 3 && leaf[0] == sequential_field)\n            ) {\n                const first_date = data.groups[0].__range[sequential_spec].from;\n                const new_domain = Domain.combine(\n                    [\n                        new Domain([[sequential_field, \"<\", first_date]]),\n                        Domain.removeDomainLeaves(domain.arrayRepr, [sequential_field]),\n                    ],\n                    \"AND\"\n                ).toList();\n                start = await this.orm.webReadGroup(\n                    resModel,\n                    new_domain,\n                    measures,\n                    groupBy.filter((gb) => gb.fieldName != sequential_field).map((gb) => gb.spec),\n                    {\n                        lazy: false, // what is this thing???\n                        context: { ...this.searchParams.context },\n                    }\n                );\n            }\n            const dataPoints = [];\n            const cumulatedStartValue = {};\n            if (start) {\n                for (const group of start.groups) {\n                    const rawValues = [];\n                    for (const gb of groupBy.filter((gb) => gb.fieldName != sequential_field)) {\n                        rawValues.push({ [gb.spec]: group[gb.spec] });\n                    }\n                    cumulatedStartValue[JSON.stringify(rawValues)] = group[measure];\n                }\n            }\n            for (const group of data.groups) {\n                const { __domain, __count } = group;\n                const labels = [];\n                const rawValues = [];\n                for (const gb of groupBy) {\n                    let label;\n                    const val = group[gb.spec];\n                    rawValues.push({ [gb.spec]: val });\n                    const fieldName = gb.fieldName;\n                    const { type } = fields[fieldName];\n                    if (type === \"boolean\") {\n                        label = `${val}`; // toUpperCase?\n                    } else if (val === false) {\n                        label = this._getDefaultFilterLabel(gb);\n                    } else if ([\"many2many\", \"many2one\"].includes(type)) {\n                        const [id, name] = val;\n                        const key = JSON.stringify([fieldName, name]);\n                        if (!numbering[key]) {\n                            numbering[key] = {};\n                        }\n                        const numbers = numbering[key];\n                        if (!numbers[id]) {\n                            numbers[id] = Object.keys(numbers).length + 1;\n                        }\n                        const num = numbers[id];\n                        label = num === 1 ? name : `${name} (${num})`;\n                    } else if (type === \"selection\") {\n                        const selected = fields[fieldName].selection.find((s) => s[0] === val);\n                        label = selected[1];\n                    } else {\n                        label = val;\n                    }\n                    labels.push(label);\n                }\n\n                let value = group[measure];\n                if (value instanceof Array) {\n                    // case where measure is a many2one and is used as groupBy\n                    value = 1;\n                }\n                if (!Number.isInteger(value)) {\n                    metaData.allIntegers = false;\n                }\n                const group_id = JSON.stringify(rawValues.slice(1));\n                dataPoints.push({\n                    count: __count,\n                    domain: __domain,\n                    value,\n                    labels,\n                    originIndex,\n                    identifier: JSON.stringify(rawValues),\n                    cumulatedStart: cumulatedStartValue[group_id] || 0,\n                });\n            }\n            return dataPoints;\n        });\n        const promResults = await Promise.all(proms);\n        return promResults.flat();\n    }\n\n    /**\n     * Process metaData.groupBy in order to keep only the finest interval option for\n     * elements based on date/datetime field (e.g. 'date:year'). This means that\n     * 'week' is prefered to 'month'. The field stays at the place of its first occurence.\n     * For instance,\n     * ['foo', 'date:month', 'bar', 'date:week'] becomes ['foo', 'date:week', 'bar'].\n     * @protected\n     * @param {Object} metaData\n     */\n    _normalize(metaData) {\n        const { fields } = metaData;\n        const groupBy = [];\n        for (const gb of metaData.groupBy) {\n            let ngb = gb;\n            if (typeof gb === \"string\") {\n                ngb = getGroupBy(gb, fields);\n            }\n            groupBy.push(ngb);\n        }\n\n        const processedGroupBy = [];\n        for (const gb of groupBy) {\n            const { fieldName, interval } = gb;\n            if (!fieldName.includes(\".\")) {\n                const { groupable, type } = fields[fieldName];\n                if (\n                    // cf. _description_groupable in odoo/fields.py\n                    !groupable ||\n                    [\"id\", \"__count\"].includes(fieldName) ||\n                    !GROUPABLE_TYPES.includes(type)\n                ) {\n                    continue;\n                }\n            }\n            const index = processedGroupBy.findIndex((gb) => gb.fieldName === fieldName);\n            if (index === -1) {\n                processedGroupBy.push(gb);\n            } else if (interval) {\n                const registeredInterval = processedGroupBy[index].interval;\n                if (rankInterval(registeredInterval) < rankInterval(interval)) {\n                    processedGroupBy.splice(index, 1, gb);\n                }\n            }\n        }\n        metaData.groupBy = processedGroupBy;\n\n        metaData.measure = processMeasure(metaData.measure);\n    }\n\n    /**\n     * @protected\n     */\n    _prepareData() {\n        const processedDataPoints = this._getProcessedDataPoints();\n        this.data = this._getData(processedDataPoints);\n        this.lineOverlayDataset = null;\n        if (this.metaData.mode === \"bar\") {\n            this.lineOverlayDataset = this._getLineOverlayDataset();\n        }\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport {\n    getBorderWhite,\n    DEFAULT_BG,\n    getColor,\n    getCustomColor,\n    lightenColor,\n    darkenColor,\n} from \"@web/core/colors/colors\";\nimport { registry } from \"@web/core/registry\";\nimport { formatFloat } from \"@web/views/fields/formatters\";\nimport { SEP } from \"./graph_model\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { loadBundle } from \"@web/core/assets\";\nimport { renderToString } from \"@web/core/utils/render\";\nimport { useService } from \"@web/core/utils/hooks\";\n\nimport { Component, onWillUnmount, useEffect, useRef, onWillStart } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { cookie } from \"@web/core/browser/cookie\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst NO_DATA = _t(\"No data\");\nconst formatters = registry.category(\"formatters\");\n\nconst colorScheme = cookie.get(\"color_scheme\");\nconst GRAPH_LEGEND_COLOR = getCustomColor(colorScheme, \"#111827\", \"#ffffff\");\nconst GRAPH_GRID_COLOR = getCustomColor(colorScheme, \"rgba(0,0,0,.1)\", \"rgba(255,255,255,.15\");\nconst GRAPH_LABEL_COLOR = getCustomColor(colorScheme, \"#111827\", \"#E4E4E4\");\nconst NO_DATA_COLOR = getCustomColor(colorScheme, DEFAULT_BG, \"#3C3E4B\");\n\n/**\n * Custom Plugin for Line chart:\n * Draw the scale grid on top of the chart to\n * see this last one correctly.\n */\nconst gridOnTop = {\n    id: \"gridOnTop\",\n    afterDraw: (chart) => {\n        const elements = chart.getDatasetMeta(0).data || [];\n        const ctx = chart.ctx;\n        const chartArea = chart.chartArea;\n        const yAxis = chart.scales.y;\n        const xAxis = chart.scales.x;\n\n        ctx.lineWidth = 1;\n        ctx.strokeStyle = GRAPH_GRID_COLOR;\n\n        // Draw Y axis scale\n        yAxis.ticks.forEach((value, index) => {\n            const y = yAxis.getPixelForTick(index);\n            ctx.beginPath();\n            // Draw the line scale\n            ctx.moveTo(chartArea.left, y);\n            ctx.lineTo(chartArea.right, y);\n            // Draw the tick mark\n            ctx.moveTo(chartArea.left - 8, y);\n            ctx.lineTo(chartArea.left, y);\n            ctx.setLineDash([]);\n            ctx.stroke();\n        });\n\n        // Draw X axis tick marks\n        xAxis.ticks.forEach((value, tickIndex) => {\n            const x = xAxis.getPixelForTick(tickIndex);\n            ctx.beginPath();\n            ctx.moveTo(x, chartArea.bottom);\n            ctx.lineTo(x, chartArea.bottom + 8);\n            ctx.stroke();\n        });\n\n        // Draw the X axis dashed line\n        elements.forEach((point, eltIndex) => {\n            xAxis.ticks.forEach((value, tickIndex) => {\n                if (point.active && eltIndex === tickIndex) {\n                    const x = xAxis.getPixelForTick(tickIndex);\n                    ctx.beginPath();\n                    ctx.moveTo(x, chartArea.top);\n                    ctx.lineTo(x, chartArea.bottom);\n                    ctx.strokeStyle = GRAPH_GRID_COLOR;\n                    ctx.stroke();\n                }\n            });\n        });\n    },\n};\n\n/**\n * @param {Object} chartArea\n * @returns {string}\n */\nfunction getMaxWidth(chartArea) {\n    const { left, right } = chartArea;\n    return Math.floor((right - left) / 1.618) + \"px\";\n}\n\n/**\n * Used to avoid too long legend items.\n * @param {string} label\n * @returns {string} shortened version of the input label\n */\nfunction shortenLabel(label) {\n    // string returned could be wrong if a groupby value contain a \" / \"!\n    const groups = label.toString().split(SEP);\n    let shortLabel = groups.slice(0, 3).join(SEP);\n    if (shortLabel.length > 30) {\n        shortLabel = `${shortLabel.slice(0, 30)}...`;\n    } else if (groups.length > 3) {\n        shortLabel = `${shortLabel}${SEP}...`;\n    }\n    return shortLabel;\n}\n\nexport class GraphRenderer extends Component {\n    static template = \"web.GraphRenderer\";\n    static components = { Dropdown, DropdownItem, ReportViewMeasures };\n    static props = [\"class?\", \"model\", \"buttonTemplate\"];\n\n    setup() {\n        this.model = this.props.model;\n\n        this.rootRef = useRef(\"root\");\n        this.canvasRef = useRef(\"canvas\");\n        this.containerRef = useRef(\"container\");\n        this.actionService = useService(\"action\");\n\n        this.chart = null;\n        this.tooltip = null;\n        this.legendTooltip = null;\n\n        onWillStart(async () => {\n            await loadBundle(\"web.chartjs_lib\");\n        });\n\n        useEffect(() => this.renderChart());\n        onWillUnmount(this.onWillUnmount);\n    }\n\n    onWillUnmount() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n    }\n\n    /**\n     * This function aims to remove a suitable number of lines from the\n     * tooltip in order to make it reasonably visible. A message indicating\n     * the number of lines is added if necessary.\n     * @param {HTMLElement} tooltip\n     * @param {number} maxTooltipHeight this the max height in pixels of the tooltip\n     */\n    adjustTooltipHeight(tooltip, maxTooltipHeight) {\n        const sizeOneLine = tooltip.querySelector(\"tbody tr\").clientHeight;\n        const tbodySize = tooltip.querySelector(\"tbody\").clientHeight;\n        const toKeep = Math.max(\n            0,\n            Math.floor((maxTooltipHeight - (tooltip.clientHeight - tbodySize)) / sizeOneLine) - 1\n        );\n        const lines = tooltip.querySelectorAll(\"tbody tr\");\n        const toRemove = lines.length - toKeep;\n        if (toRemove > 0) {\n            for (let index = toKeep; index < lines.length; ++index) {\n                lines[index].remove();\n            }\n            const tr = document.createElement(\"tr\");\n            const td = document.createElement(\"td\");\n            tr.classList.add(\"o_show_more\", \"text-center\", \"fw-bold\");\n            td.setAttribute(\"colspan\", \"2\");\n            td.innerText = _t(\"...\");\n            tr.appendChild(td);\n            tooltip.querySelector(\"tbody\").appendChild(tr);\n        }\n    }\n\n    /**\n     * Creates a custom HTML tooltip.\n     * @param {Object} data\n     * @param {Object} metaData\n     * @param {Object} context see chartjs documentation\n     */\n    customTooltip(data, metaData, context) {\n        const tooltipModel = context.tooltip;\n        const { measure, measures, disableLinking, mode } = metaData;\n        this.containerRef.el.style.cursor = \"\";\n        this.removeTooltips();\n        if (tooltipModel.opacity === 0 || tooltipModel.dataPoints.length === 0) {\n            return;\n        }\n        if (!disableLinking && mode !== \"line\") {\n            this.containerRef.el.style.cursor = \"pointer\";\n        }\n        const chartAreaTop = this.chart.chartArea.top;\n        const viewContentTop = this.containerRef.el.getBoundingClientRect().top;\n        const innerHTML = renderToString(\"web.GraphRenderer.CustomTooltip\", {\n            maxWidth: getMaxWidth(this.chart.chartArea),\n            measure: measures[measure].string,\n            mode: this.model.metaData.mode,\n            tooltipItems: this.getTooltipItems(data, metaData, tooltipModel),\n        });\n        const template = Object.assign(document.createElement(\"template\"), { innerHTML });\n        const tooltip = template.content.firstChild;\n        this.containerRef.el.prepend(tooltip);\n\n        let top;\n        const tooltipHeight = tooltip.clientHeight;\n        const minTopAllowed = Math.floor(chartAreaTop);\n        const maxTopAllowed = Math.floor(window.innerHeight - (viewContentTop + tooltipHeight)) - 2;\n        const y = Math.floor(tooltipModel.y);\n        if (minTopAllowed <= maxTopAllowed) {\n            // Here we know that the full tooltip can fit in the screen.\n            // We put it in the position where Chart.js would put it\n            // if two conditions are respected:\n            //  1: the tooltip is not cut (because we know it is possible to not cut it)\n            //  2: the tooltip does not hide the legend.\n            // If it is not possible to use the Chart.js proposition (y)\n            // we use the best approximated value.\n            if (y <= maxTopAllowed) {\n                if (y >= minTopAllowed) {\n                    top = y;\n                } else {\n                    top = minTopAllowed;\n                }\n            } else {\n                top = maxTopAllowed;\n            }\n        } else {\n            // Here we know that we cannot satisfy condition 1 above,\n            // so we position the tooltip at the minimal position and\n            // cut it the minimum possible.\n            top = minTopAllowed;\n            const maxTooltipHeight = window.innerHeight - (viewContentTop + chartAreaTop) - 2;\n            this.adjustTooltipHeight(tooltip, maxTooltipHeight);\n        }\n        this.fixTooltipLeftPosition(tooltip, tooltipModel.x);\n        tooltip.style.top = Math.floor(top) + \"px\";\n\n        this.tooltip = tooltip;\n    }\n\n    /**\n     * Sets best left position of a tooltip approaching the proposal x.\n     * @param {HTMLElement} tooltip\n     * @param {number} x\n     */\n    fixTooltipLeftPosition(tooltip, x) {\n        let left;\n        const tooltipWidth = tooltip.clientWidth;\n        const minLeftAllowed = Math.floor(this.chart.chartArea.left + 2);\n        const maxLeftAllowed = Math.floor(this.chart.chartArea.right - tooltipWidth - 2);\n        x = Math.floor(x);\n        if (x < minLeftAllowed) {\n            left = minLeftAllowed;\n        } else if (x > maxLeftAllowed) {\n            left = maxLeftAllowed;\n        } else {\n            left = x;\n        }\n        tooltip.style.left = `${left}px`;\n    }\n\n    /**\n     * Used to format correctly the values in tooltip and y.\n     * @param {number} value\n     * @param {boolean} [allIntegers=true]\n     * @returns {string}\n     */\n    formatValue(value, allIntegers = true, formatType = \"\") {\n        const largeNumber = Math.abs(value) >= 1000;\n        if (formatType) {\n            return formatters.get(formatType)(value);\n        }\n        if (allIntegers && !largeNumber) {\n            return String(value);\n        }\n        if (largeNumber) {\n            return formatFloat(value, { humanReadable: true, decimals: 2, minDigits: 1 });\n        }\n        return formatFloat(value);\n    }\n\n    /**\n     * Returns the bar chart data\n     * @returns {Object}\n     */\n    getBarChartData() {\n        // style data\n        const { domains, stacked } = this.model.metaData;\n        const { data, lineOverlayDataset } = this.model;\n        for (let index = 0; index < data.datasets.length; ++index) {\n            const dataset = data.datasets[index];\n            const itemColor = getColor(index, colorScheme, data.datasets.length);\n            // used when stacked\n            if (stacked) {\n                dataset.stack = domains[dataset.originIndex].description || \"\";\n            }\n            // set dataset color\n            dataset.backgroundColor = itemColor;\n            dataset.borderRadius = 4;\n        }\n        if (lineOverlayDataset) {\n            // Mutate the lineOverlayDataset to include the config on how it will be displayed.\n            Object.assign(lineOverlayDataset, {\n                type: \"line\",\n                order: -1,\n                tension: 0,\n                fill: false,\n                pointHitRadius: 20,\n                pointRadius: 5,\n                pointHoverRadius: 10,\n                backgroundColor: getCustomColor(colorScheme, \"#343a40\", \"#e9ecef\"),\n                borderColor: getCustomColor(colorScheme, \"rgba(0,0,0,.3)\", \"rgba(255,255,255,.5)\"),\n                borderWidth: 2,\n                lineWidth: 3,\n            });\n            // We're not mutating the original datasets (`this.model.data.datasets`)\n            // because some part of the code depends on it.\n            return {\n                ...data,\n                datasets: [...data.datasets, lineOverlayDataset],\n            };\n        }\n\n        return data;\n    }\n\n    /**\n     * Returns the chart config.\n     * @returns {Object}\n     */\n    getChartConfig() {\n        const { mode } = this.model.metaData;\n        let data;\n        switch (mode) {\n            case \"bar\":\n                data = this.getBarChartData();\n                break;\n            case \"line\":\n                data = this.getLineChartData();\n                break;\n            case \"pie\":\n                data = this.getPieChartData();\n        }\n        const options = this.prepareOptions();\n        const config = { data, options, type: mode };\n        if (mode === \"line\") {\n            config.plugins = [gridOnTop];\n        }\n        return config;\n    }\n\n    /**\n     * Returns the animation options.\n     * 1. This adds progressive animation for Bar & Line charts.\n     * 2. Reduce animation duration for Pie chart.\n     * @returns {Object}\n     */\n    getAnimationOptions() {\n        let delayed;\n        const { mode } = this.model.metaData;\n        const labelsCount = this.model.data.labels.length;\n        const gap = 350;\n        const animationOptions = {};\n        if (mode === \"pie\") {\n            animationOptions.offset = { duration: 200 };\n        } else {\n            animationOptions.duration = 600;\n            animationOptions.onComplete = () => {\n                delayed = true;\n            };\n            animationOptions.delay = (context) => {\n                let delay = 0;\n                if ((mode === \"bar\" || mode === \"line\") && !delayed) {\n                    delay = context.dataIndex * (gap / labelsCount);\n                }\n                return delay;\n            };\n        }\n        return animationOptions;\n    }\n\n    /**\n     * Returns an object used to style chart elements independently from\n     * the datasets.\n     * @returns {Object}\n     */\n    getElementOptions() {\n        const { mode, stacked } = this.model.metaData;\n        const elementOptions = {};\n        if (mode === \"bar\") {\n            elementOptions.bar = { borderWidth: 1 };\n        } else if (mode === \"line\") {\n            elementOptions.line = { fill: stacked, tension: 0 };\n        }\n        return elementOptions;\n    }\n\n    /**\n     * @returns {Object}\n     */\n    getLegendOptions() {\n        const { mode } = this.model.metaData;\n        const legendOptions = {\n            onHover: this.onLegendHover.bind(this),\n            onLeave: this.onLegendLeave.bind(this),\n        };\n        if (mode === \"line\") {\n            legendOptions.onClick = this.onLegendClick.bind(this);\n        }\n        if (mode === \"pie\") {\n            legendOptions.labels = {\n                generateLabels: (chart) => {\n                    return chart.data.labels.map((label, index) => {\n                        const hidden = !chart.getDataVisibility(index);\n                        const fullText = label;\n                        const text = shortenLabel(fullText);\n                        const fillStyle =\n                            label === NO_DATA\n                                ? NO_DATA_COLOR\n                                : getColor(index, colorScheme, chart.data.labels.length);\n                        return {\n                            text,\n                            fullText,\n                            fillStyle,\n                            hidden,\n                            index,\n                            fontColor: GRAPH_LEGEND_COLOR,\n                            lineWidth: 0,\n                        };\n                    });\n                },\n            };\n        } else {\n            legendOptions.position = \"top\";\n            legendOptions.align = \"end\";\n            const referenceColor = mode === \"bar\" ? \"backgroundColor\" : \"borderColor\";\n            legendOptions.labels = {\n                generateLabels: (chart) => {\n                    const { data } = chart;\n                    const labels = data.datasets.map((dataset, index) => {\n                        return {\n                            text: shortenLabel(dataset.label),\n                            fullText: dataset.label,\n                            fillStyle: dataset[referenceColor],\n                            hidden: !chart.isDatasetVisible(index),\n                            lineCap: dataset.borderCapStyle,\n                            lineDash: dataset.borderDash,\n                            lineDashOffset: dataset.borderDashOffset,\n                            lineJoin: dataset.borderJoinStyle,\n                            lineWidth: dataset.borderWidth,\n                            strokeStyle: dataset[referenceColor],\n                            pointStyle: dataset.pointStyle,\n                            datasetIndex: index,\n                            fontColor: GRAPH_LEGEND_COLOR,\n                        };\n                    });\n                    return labels;\n                },\n            };\n        }\n        return legendOptions;\n    }\n\n    /**\n     * Returns line chart data.\n     * @returns {Object}\n     */\n    getLineChartData() {\n        const { cumulated } = this.model.metaData;\n        const data = this.model.data;\n        for (let index = 0; index < data.datasets.length; ++index) {\n            const dataset = data.datasets[index];\n            const itemColor = getColor(index, colorScheme, data.datasets.length);\n            dataset.backgroundColor = getCustomColor(\n                colorScheme,\n                lightenColor(itemColor, 0.5),\n                darkenColor(itemColor, 0.5)\n            );\n            dataset.cubicInterpolationMode = \"monotone\";\n            dataset.borderColor = itemColor;\n            dataset.borderWidth = 2;\n            dataset.hoverBackgroundColor = dataset.borderColor;\n            dataset.pointRadius = 3;\n            dataset.pointHoverRadius = 6;\n            if (cumulated) {\n                let accumulator = dataset.cumulatedStart;\n                dataset.data = dataset.data.map((value) => {\n                    accumulator += value;\n                    return accumulator;\n                });\n            }\n            if (data.labels.length === 1) {\n                // shift of the real value to right. This is done to\n                // center the points in the chart. See data.labels below in\n                // Chart parameters\n                dataset.data.unshift(undefined);\n                dataset.trueLabels.unshift(undefined);\n                dataset.domains.unshift(undefined);\n            }\n            dataset.pointBackgroundColor = dataset.borderColor;\n        }\n        // center the points in the chart (without that code they are put\n        // on the left and the graph seems empty)\n        data.labels = data.labels.length > 1 ? data.labels : [\"\", ...data.labels, \"\"];\n        return data;\n    }\n\n    /**\n     * Returns pie chart data.\n     * @returns {Object}\n     */\n    getPieChartData() {\n        const { domains } = this.model.metaData;\n        const data = this.model.data;\n        // style/complete data\n        // give same color to same groups from different origins\n        const colors = data.labels.map((_, index) =>\n            getColor(index, colorScheme, data.labels.length)\n        );\n        const borderColor = getBorderWhite(colorScheme);\n        for (const dataset of data.datasets) {\n            dataset.backgroundColor = colors;\n            dataset.hoverBackgroundColor = colors;\n            dataset.borderColor = borderColor;\n            dataset.hoverOffset = 60;\n        }\n        // make sure there is a zone associated with every origin\n        const representedOriginIndexes = new Set(\n            data.datasets.map((dataset) => dataset.originIndex)\n        );\n        let addNoDataToLegend = false;\n        const fakeData = new Array(data.labels.length + 1);\n        fakeData[data.labels.length] = 1;\n        const fakeTrueLabels = new Array(data.labels.length + 1);\n        fakeTrueLabels[data.labels.length] = NO_DATA;\n        for (let index = 0; index < domains.length; ++index) {\n            if (!representedOriginIndexes.has(index)) {\n                data.datasets.push({\n                    label: domains[index].description,\n                    data: fakeData,\n                    trueLabels: fakeTrueLabels,\n                    backgroundColor: [...colors, NO_DATA_COLOR],\n                    borderColor,\n                });\n                addNoDataToLegend = true;\n            }\n        }\n        if (addNoDataToLegend) {\n            data.labels.push(NO_DATA);\n        }\n\n        return data;\n    }\n\n    /**\n     * Returns the options used to generate the chart axes.\n     * @returns {Object}\n     */\n    getScaleOptions() {\n        const { labels } = this.model.data;\n        const { fieldAttrs, measure, measures, mode, stacked } = this.model.metaData;\n        if (mode === \"pie\") {\n            return {};\n        }\n        const xAxe = {\n            type: \"category\",\n            ticks: {\n                callback: (val, index) => {\n                    const value = labels[index];\n                    return shortenLabel(value);\n                },\n                color: GRAPH_LABEL_COLOR,\n            },\n            grid: {\n                color: \"transparent\",\n            },\n            border: {\n                display: false,\n            },\n        };\n        const yAxe = {\n            beginAtZero: true,\n            type: \"linear\",\n            title: {\n                text: measures[measure].string,\n                color:\n                    cookie.get(\"color_scheme\") === \"dark\"\n                        ? getColor(15, cookie.get(\"color_scheme\"))\n                        : null,\n            },\n            ticks: {\n                callback: (value) => this.formatValue(value, false, fieldAttrs[measure]?.widget),\n                color: GRAPH_LABEL_COLOR,\n            },\n            stacked: mode === \"line\" && stacked ? stacked : undefined,\n            grid: {\n                display: mode === \"line\" ? false : true,\n                color: GRAPH_GRID_COLOR,\n            },\n            border: {\n                display: false,\n            },\n            suggestedMax: 0,\n            suggestedMin: 0,\n        };\n        return { x: xAxe, y: yAxe };\n    }\n\n    /**\n     * This function extracts the information from the data points in\n     * tooltipModel.dataPoints (corresponding to datapoints over a given\n     * label determined by the mouse position) that will be displayed in a\n     * custom tooltip.\n     * @param {Object} data\n     * @param {Object} metaData\n     * @param {Object} tooltipModel see chartjs documentation\n     * @returns {Object[]}\n     */\n    getTooltipItems(data, metaData, tooltipModel) {\n        const { allIntegers, domains, mode, groupBy, measure } = metaData;\n        const sortedDataPoints = sortBy(tooltipModel.dataPoints, \"raw\", \"desc\");\n        const items = [];\n        for (const item of sortedDataPoints) {\n            const index = item.dataIndex;\n            // If `datasetIndex` is not found in the `datasets`, then it refers to the `lineOverlayDataset`.\n            const dataset = data.datasets[item.datasetIndex] || this.model.lineOverlayDataset;\n            let label = dataset.trueLabels[index];\n            let value = dataset.data[index];\n            const measureWidget = metaData.fieldAttrs[measure]?.widget;\n            value = this.formatValue(value, allIntegers, measureWidget);\n            let boxColor;\n            let percentage;\n            if (mode === \"pie\") {\n                if (label === NO_DATA) {\n                    value = this.formatValue(0, allIntegers, measureWidget);\n                }\n                if (domains.length > 1) {\n                    label = `${dataset.label} / ${label}`;\n                }\n                boxColor = dataset.backgroundColor[index];\n                const totalData = dataset.data.reduce((a, b) => a + b, 0);\n                percentage = totalData && ((dataset.data[index] * 100) / totalData).toFixed(2);\n            } else {\n                if (groupBy.length > 1 || domains.length > 1) {\n                    label = `${label} / ${dataset.label}`;\n                }\n                boxColor = mode === \"bar\" ? dataset.backgroundColor : dataset.borderColor;\n            }\n            items.push({ label, value, boxColor, percentage });\n        }\n        return items;\n    }\n\n    /**\n     * Returns the options used to generate chart tooltips.\n     * @returns {Object}\n     */\n    getTooltipOptions() {\n        const { data, metaData } = this.model;\n        const { mode } = metaData;\n        const tooltipOptions = {\n            enabled: false,\n            external: this.customTooltip.bind(this, data, metaData),\n        };\n        if (mode === \"line\") {\n            tooltipOptions.mode = \"index\";\n            tooltipOptions.intersect = false;\n            tooltipOptions.position = \"average\";\n        }\n        if (mode === \"bar\") {\n            tooltipOptions.xAlign = \"center\";\n            tooltipOptions.yAlign = \"bottom\";\n        }\n        if (mode === \"pie\") {\n            tooltipOptions.xAlign = \"center\";\n            tooltipOptions.yAlign = \"center\";\n        }\n        return tooltipOptions;\n    }\n\n    /**\n     * If a group has been clicked on, display a view of its records.\n     * @param {MouseEvent} ev\n     */\n    onGraphClicked(ev) {\n        const [activeElement] = this.chart.getElementsAtEventForMode(\n            ev,\n            \"nearest\",\n            { intersect: true },\n            false\n        );\n        if (!activeElement) {\n            return;\n        }\n        const { datasetIndex, index } = activeElement;\n        const { domains } = this.chart.data.datasets[datasetIndex];\n        if (domains) {\n            this.onGraphClickedFinal(domains[index]);\n        }\n    }\n\n    /**\n     * Overrides the default legend 'onClick' behaviour. This is done to\n     * remove all existing tooltips right before updating the chart.\n     * @param {Event} ev\n     * @param {Object} legendItem\n     */\n    onLegendClick(ev, legendItem) {\n        this.removeTooltips();\n        // Default 'onClick' fallback. See web/static/lib/Chart/Chart.js#15138\n        const index = legendItem.datasetIndex;\n        const meta = this.chart.getDatasetMeta(index);\n        meta.hidden = meta.hidden === null ? !this.chart.data.datasets[index].hidden : null;\n        this.chart.update();\n    }\n\n    /**\n     * If the text of a legend item has been shortened and the user mouse\n     * hovers that item (actually the event type is mousemove), a tooltip\n     * with the item full text is displayed.\n     * @param {Event} ev\n     * @param {Object} legendItem\n     */\n    onLegendHover(ev, legendItem) {\n        ev = ev.native;\n        this.canvasRef.el.style.cursor = \"pointer\";\n        /**\n         * The string legendItem.text is an initial segment of legendItem.fullText.\n         * If the two coincide, no need to generate a tooltip. If a tooltip\n         * for the legend already exists, it is already good and does not\n         * need to be recreated.\n         */\n        const { fullText, text } = legendItem;\n        if (this.legendTooltip || text === fullText) {\n            return;\n        }\n        const viewContentTop = this.canvasRef.el.getBoundingClientRect().top;\n        const legendTooltip = Object.assign(document.createElement(\"div\"), {\n            className: \"o_tooltip_legend popover p-3 pe-none position-absolute\",\n            innerText: fullText,\n        });\n        legendTooltip.style.top = `${ev.clientY - viewContentTop}px`;\n        legendTooltip.style.maxWidth = getMaxWidth(this.chart.chartArea);\n        this.containerRef.el.appendChild(legendTooltip);\n        this.fixTooltipLeftPosition(legendTooltip, ev.clientX);\n        this.legendTooltip = legendTooltip;\n    }\n\n    /**\n     * If there's a legend tooltip and the user mouse out of the\n     * corresponding legend item, the tooltip is removed.\n     */\n    onLegendLeave() {\n        this.canvasRef.el.style.cursor = \"\";\n        this.removeLegendTooltip();\n    }\n\n    /**\n     * Prepares options for the chart according to the current mode\n     * (= chart type). This function returns the parameter options used to\n     * instantiate the chart.\n     */\n    prepareOptions() {\n        const { disableLinking, mode } = this.model.metaData;\n        const options = {\n            maintainAspectRatio: false,\n            scales: this.getScaleOptions(),\n            plugins: {\n                legend: this.getLegendOptions(),\n                tooltip: this.getTooltipOptions(),\n            },\n            elements: this.getElementOptions(),\n            onResize: () => {\n                this.resizeChart(options);\n            },\n            animation: this.getAnimationOptions(),\n        };\n        if (!disableLinking && mode !== \"line\") {\n            options.onClick = this.onGraphClicked.bind(this);\n        }\n        if (mode === \"line\") {\n            options.interaction = {\n                mode: \"index\",\n                intersect: false,\n            };\n        }\n        if (mode === \"pie\") {\n            options.radius = \"90%\";\n        }\n        return options;\n    }\n\n    /**\n     * Adapt Pie chart layout on mobile\n     * @param {Object} context\n     */\n    resizeChart(context) {\n        const { mode } = this.model.metaData;\n        if (mode === \"pie\") {\n            if (this.env.isSmall) {\n                context.plugins.legend.position = \"bottom\";\n                context.plugins.legend.align = \"center\";\n            } else {\n                context.plugins.legend.position = \"right\";\n                context.plugins.legend.align = \"start\";\n            }\n        }\n    }\n\n    /**\n     * Removes the legend tooltip (if any).\n     */\n    removeLegendTooltip() {\n        if (this.legendTooltip) {\n            this.legendTooltip.remove();\n            this.legendTooltip = null;\n        }\n    }\n\n    /**\n     * Removes all existing tooltips (if any).\n     */\n    removeTooltips() {\n        if (this.tooltip) {\n            this.tooltip.remove();\n            this.tooltip = null;\n        }\n        this.removeLegendTooltip();\n    }\n\n    /**\n     * Instantiates a Chart (Chart.js lib) to render the graph according to\n     * the current config.\n     */\n    renderChart() {\n        if (this.chart) {\n            this.chart.destroy();\n        }\n        if (this.canvasRef.el) {\n            const config = this.getChartConfig();\n            this.chart = new Chart(this.canvasRef.el, config);\n        }\n    }\n\n    /**\n     * Execute the action to open the view on the current model.\n     *\n     * @param {Array} domain\n     * @param {Array} views\n     * @param {Object} context\n     */\n    openView(domain, views, context) {\n        this.actionService.doAction(\n            {\n                context,\n                domain,\n                name: this.model.metaData.title,\n                res_model: this.model.metaData.resModel,\n                target: \"current\",\n                type: \"ir.actions.act_window\",\n                views,\n            },\n            {\n                viewType: \"list\",\n            }\n        );\n    }\n    /**\n     * @param {string} domain the domain of the clicked area\n     */\n    onGraphClickedFinal(domain) {\n        const { context } = this.model.metaData;\n\n        Object.keys(context).forEach((x) => {\n            if (x === \"group_by\" || x.startsWith(\"search_default_\")) {\n                delete context[x];\n            }\n        });\n\n        const views = {};\n        for (const [viewId, viewType] of this.env.config.views || []) {\n            views[viewType] = viewId;\n        }\n        function getView(viewType) {\n            return [views[viewType] || false, viewType];\n        }\n        const actionViews = [getView(\"list\"), getView(\"form\")];\n        this.openView(domain, actionViews, context);\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.updateMetaData({ measure });\n    }\n\n    /**\n     * @param {\"bar\"|\"line\"|\"pie\"} mode\n     */\n    onModeSelected(mode) {\n        if (this.model.metaData.mode != mode) {\n            this.model.updateMetaData({ mode });\n        }\n    }\n\n    /**\n     * @param {\"ASC\"|\"DESC\"} order\n     */\n    toggleOrder(order) {\n        const { order: currentOrder } = this.model.metaData;\n        const nextOrder = currentOrder === order ? null : order;\n        this.model.updateMetaData({ order: nextOrder });\n    }\n\n    toggleStacked() {\n        const { stacked } = this.model.metaData;\n        this.model.updateMetaData({ stacked: !stacked });\n    }\n\n    toggleCumulated() {\n        const { cumulated } = this.model.metaData;\n        this.model.updateMetaData({ cumulated: !cumulated });\n    }\n}\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class GraphSearchModel extends SearchModel {\n    _getIrFilterDescription() {\n        this.preparingIrFilterDescription = true;\n        const result = super._getIrFilterDescription(...arguments);\n        this.preparingIrFilterDescription = false;\n        return result;\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const { context, type } = this.searchItems[searchItemId];\n        if (!this.preparingIrFilterDescription && type === \"favorite\" && context.graph_groupbys) {\n            return context.graph_groupbys;\n        }\n        return super._getSearchItemGroupBys(...arguments);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { GraphArchParser } from \"./graph_arch_parser\";\nimport { GraphModel } from \"./graph_model\";\nimport { GraphController } from \"./graph_controller\";\nimport { GraphRenderer } from \"./graph_renderer\";\nimport { GraphSearchModel } from \"./graph_search_model\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const graphView = {\n    type: \"graph\",\n    Controller: GraphController,\n    Renderer: GraphRenderer,\n    Model: GraphModel,\n    ArchParser: GraphArchParser,\n    SearchModel: GraphSearchModel,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"comparison\", \"favorite\"],\n    buttonTemplate: \"web.GraphView.Buttons\",\n\n    props: (genericProps, view) => {\n        let modelParams;\n        if (genericProps.state) {\n            modelParams = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch, fields);\n            modelParams = {\n                disableLinking: Boolean(archInfo.disableLinking),\n                fieldAttrs: archInfo.fieldAttrs,\n                fields: fields,\n                groupBy: archInfo.groupBy,\n                measure: archInfo.measure || \"__count\",\n                viewMeasures: archInfo.measures,\n                mode: archInfo.mode || \"bar\",\n                order: archInfo.order || null,\n                resModel: resModel,\n                stacked: \"stacked\" in archInfo ? archInfo.stacked : true,\n                cumulated: archInfo.cumulated || false,\n                cumulatedStart: archInfo.cumulatedStart || false,\n                title: archInfo.title || _t(\"Untitled\"),\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nviewRegistry.add(\"graph\", graphView);\n", "import { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\n\nexport class PivotArchParser {\n    parse(arch) {\n        const archInfo = {\n            activeMeasures: [], // store the defined active measures\n            colGroupBys: [], // store the defined group_by used on cols\n            defaultOrder: null,\n            fieldAttrs: {},\n            rowGroupBys: [], // store the defined group_by used on rows\n            widgets: {}, // wigdets defined in the arch\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"pivot\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    if (node.hasAttribute(\"default_order\")) {\n                        archInfo.defaultOrder = node.getAttribute(\"default_order\");\n                    }\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.title = node.getAttribute(\"string\");\n                    }\n                    if (node.hasAttribute(\"display_quantity\")) {\n                        archInfo.displayQuantity = exprToBoolean(\n                            node.getAttribute(\"display_quantity\")\n                        );\n                    }\n                    break;\n                }\n                case \"field\": {\n                    let fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n\n                    archInfo.fieldAttrs[fieldName] = {};\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.fieldAttrs[fieldName].string = node.getAttribute(\"string\");\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n\n                    if (node.hasAttribute(\"interval\")) {\n                        fieldName += \":\" + node.getAttribute(\"interval\");\n                    }\n                    if (node.hasAttribute(\"widget\")) {\n                        archInfo.widgets[fieldName] = node.getAttribute(\"widget\");\n                    }\n                    if (node.getAttribute(\"type\") === \"measure\" || node.hasAttribute(\"operator\")) {\n                        archInfo.activeMeasures.push(fieldName);\n                    }\n                    if (node.getAttribute(\"type\") === \"col\") {\n                        archInfo.colGroupBys.push(fieldName);\n                    }\n                    if (node.getAttribute(\"type\") === \"row\") {\n                        archInfo.rowGroupBys.push(fieldName);\n                    }\n                    break;\n                }\n            }\n        });\n\n        return archInfo;\n    }\n}\n", "import { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nexport class PivotController extends Component {\n    static template = \"web.PivotView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                const { data, metaData } = this.model;\n                return { data, metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n    /**\n     * @returns {Object}\n     */\n    getContext() {\n        return {\n            pivot_measures: this.model.metaData.activeMeasures,\n            pivot_column_groupby: this.model.metaData.fullColGroupBys,\n            pivot_row_groupby: this.model.metaData.fullRowGroupBys,\n        };\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { CheckboxItem } from \"@web/core/dropdown/checkbox_item\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { sortBy } from \"@web/core/utils/arrays\";\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { CustomGroupByItem } from \"@web/search/custom_group_by_item/custom_group_by_item\";\nimport { PropertiesGroupByItem } from \"@web/search/properties_group_by_item/properties_group_by_item\";\nimport { getIntervalOptions } from \"@web/search/utils/dates\";\nimport { FACET_ICONS, GROUPABLE_TYPES } from \"@web/search/utils/misc\";\n\nexport class PivotHeader extends Component {\n    static template = \"web.PivotHeader\";\n    static components = {\n        CustomGroupByItem,\n        Dropdown,\n        CheckboxItem,\n        PropertiesGroupByItem,\n    };\n    static defaultProps = {\n        isInHead: false,\n        isXAxis: false,\n        showCaretDown: false,\n    };\n    static props = {\n        cell: Object,\n        isInHead: { type: Boolean, optional: true },\n        isXAxis: { type: Boolean, optional: true },\n        customGroupBys: Object,\n        onAddCustomGroupBy: Function,\n        onItemSelected: Function,\n        onClick: Function,\n        slots: { optional: true },\n    };\n\n    setup() {\n        this.icon = FACET_ICONS.groupBy;\n        const fields = [];\n        for (const [fieldName, field] of Object.entries(this.env.searchModel.searchViewFields)) {\n            if (this.validateField(fieldName, field)) {\n                fields.push(Object.assign({ name: fieldName }, field));\n            }\n        }\n        this.fields = sortBy(fields, \"string\");\n        this.l10n = localization;\n        this.dropdownState = useDropdownState();\n\n        useBus(this.env.searchModel, \"update\", this.render);\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get hideCustomGroupBy() {\n        return this.env.searchModel.hideCustomGroupBy || false;\n    }\n\n    /**\n     * @returns {Object[]}\n     */\n    get items() {\n        let items = this.env.searchModel.getSearchItems(\n            (searchItem) =>\n                [\"groupBy\", \"dateGroupBy\"].includes(searchItem.type) && !searchItem.custom\n        );\n        if (items.length === 0) {\n            items = this.fields;\n        }\n\n        // Add custom groupbys\n        let groupNumber = 1 + Math.max(0, ...items.map(({ groupNumber: n }) => n));\n        for (const [fieldName, customGroupBy] of this.props.customGroupBys.entries()) {\n            items.push({ ...customGroupBy, name: fieldName, groupNumber: groupNumber++ });\n        }\n\n        return items.map((item) => ({\n            ...item,\n            id: item.id || item.name,\n            fieldName: item.fieldName || item.name,\n            description: item.description || item.string,\n            isActive: false,\n            options:\n                item.options || [\"date\", \"datetime\"].includes(item.type)\n                    ? getIntervalOptions()\n                    : undefined,\n        }));\n    }\n\n    get cell() {\n        return this.props.cell;\n    }\n\n    /**\n     * Retrieve the padding of a left header.\n     * @returns {Number} Padding\n     */\n    get padding() {\n        return 5 + this.cell.indent * 30;\n    }\n\n    /**\n     * @param {string} fieldName\n     * @param {Object} field\n     * @returns {boolean}\n     */\n    validateField(fieldName, field) {\n        const { groupable, type } = field;\n        return (\n            groupable &&\n            fieldName !== \"id\" &&\n            GROUPABLE_TYPES.includes(type)\n        );\n    }\n\n    /**\n     * @override\n     * @param {Object} param0\n     * @param {number} param0.itemId\n     * @param {number} [param0.optionId]\n     */\n    onGroupBySelected({ itemId, optionId }) {\n        // Here, we purposely do not call super.onGroupBySelected as we don't want\n        // to change the group-by on the model, only inside the pivot\n        const item = this.items.find(({ id }) => id === itemId);\n        this.props.onItemSelected({\n            itemId,\n            optionId,\n            fieldName: item.fieldName,\n            interval: optionId,\n            groupId: this.cell.groupId,\n        });\n    }\n\n    /**\n     * @param {string} fieldName\n     */\n    onAddCustomGroup(fieldName) {\n        this.props.onAddCustomGroupBy(fieldName);\n    }\n\n    /**\n     * @param {Event} event\n     */\n    onClick(event) {\n        if (this.cell.isLeaf && !this.cell.isFolded) {\n            this.dropdownState.open();\n        }\n        this.props.onClick();\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { cartesian, sections, sortBy, symmetricalDifference } from \"@web/core/utils/arrays\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { DEFAULT_INTERVAL } from \"@web/search/utils/dates\";\nimport { addPropertyFieldDefs, Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\n\n/**\n * @param {number} value\n * @param {number} comparisonValue\n * @returns {number}\n */\nfunction computeVariation(value, comparisonValue) {\n    if (isNaN(value) || isNaN(comparisonValue)) {\n        return NaN;\n    }\n    if (comparisonValue === 0) {\n        if (value === 0) {\n            return 0;\n        } else if (value > 0) {\n            return 1;\n        } else {\n            return -1;\n        }\n    }\n    return (value - comparisonValue) / Math.abs(comparisonValue);\n}\n\n/**\n * Pivot Model\n *\n * The pivot model keeps an in-memory representation of the pivot table that is\n * displayed on the screen.  The exact layout of this representation is not so\n * simple, because a pivot table is at its core a 2-dimensional object, but\n * with a 'list' component: some rows/cols can be expanded so we zoom into the\n * structure.\n *\n * However, we need to be able to manipulate the data in a somewhat efficient\n * way, and to transform it into a list of lines to be displayed by the renderer.\n *\n * Basicaly the pivot table presents aggregated values for various groups of records\n * in one domain. If a comparison is asked for, two domains are considered.\n *\n * Let us consider a simple example and let us fix the vocabulary (let us suppose we are in June 2020):\n * ___________________________________________________________________________________________________________________________________________\n * |                    |   Total                                                                                                             |\n * |                    |_____________________________________________________________________________________________________________________|\n * |                    |   Sale Team 1                         |  Sale Team 2                         |                                      |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   Sales total                         |  Sales total                         |  Sales total                         |\n * |                    |_______________________________________|______________________________________|______________________________________|\n * |                    |   May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |  May 2020   | June 2020  | Variation |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n * | Total              |     85       |     110    |  29.4%    |     40      |    30      |   -25%    |    125      |    140     |     12%   |\n * |    Europe          |     25       |     35     |    40%    |     40      |    30      |   -25%    |     65      |     65     |      0%   |\n * |        Brussels    |      0       |     15     |   100%    |     30      |    30      |     0%    |     30      |     45     |     50%   |\n * |        Paris       |     25       |     20     |   -20%    |     10      |     0      |  -100%    |     35      |     20     |  -42.8%   |\n * |    North America   |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |        Washington  |     60       |     75     |    25%    |             |            |           |     60      |     75     |     25%   |\n * |____________________|______________|____________|___________|_____________|____________|___________|_____________|____________|___________|\n *\n *\n * META DATA:\n *\n * In the above pivot table, the records have been grouped using the fields\n *\n *      continent_id, city_id\n *\n * for rows and\n *\n *      sale_team_id\n *\n * for columns.\n *\n * The measure is the field 'sales_total'.\n *\n * Two domains are considered: 'May 2020' and 'June 2020'.\n *\n * In the model,\n *\n *      - rowGroupBys is the list [continent_id, city_id]\n *      - colGroupBys is the list [sale_team_id]\n *      - measures is the list [sales_total]\n *      - domains is the list [d1, d2] with d1 and d2 domain expressions\n *          for say sale_date in May 2020 and June 2020, for instance\n *          d1 = [['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31]]\n *      - origins is the list ['May 2020', 'June 2020']\n *\n * DATA:\n *\n * Recall that a group is constituted by records (in a given domain)\n * that have the same (raw) values for a list of fields.\n * Thus the group itself is identified by this list and the domain.\n * In comparison mode, the same group (forgetting the domain part or 'originIndex')\n * can be eventually found in the two domains.\n * This defines the way in which the groups are identified or not.\n *\n * In the above table, (forgetting the domain) the following groups are found:\n *\n *      the 'row groups'\n *      - Total\n *      - Europe\n *      - America\n *      - Europe, Brussels\n *      - Europe, Paris\n *      - America, Washington\n *\n *      the 'col groups'\n *\n *      - Total\n *      - Sale Team 1\n *      - Sale Team 2\n *\n *      and all non trivial combinations of row groups and col groups\n *\n *      - Europe, Sale Team 1\n *      - Europe, Brussels, Sale Team 2\n *      - America, Washington, Sale Team 1\n *      - ...\n *\n * The list of fields is created from the concatenation of two lists of fields, the first in\n *\n * [], [f1], [f1, f2], ... [f1, f2, ..., fn]  for [f1, f2, ..., fn] the full list of groupbys\n * (called rowGroupBys) used to create row groups\n *\n * In the example: [], [continent_id], [continent_id, city_id].\n *\n * and the second in\n * [], [g1], [g1, g2], ... [g1, g2, ..., gm]  for [g1, g2, ..., gm] the full list of groupbys\n * (called colGroupBys) used to create col groups.\n *\n * In the example: [], [sale_team_id].\n *\n * Thus there are (n+1)*(m+1) lists of fields possible.\n *\n * In the example: 6 lists possible, namely [],\n *                                          [continent_id], [sale_team_id],\n *                                          [continent_id, sale_team_id], [continent_id, city_id],\n *                                          [continent_id, city_id, sale_team_id]\n *\n * A given list is thus of the form [f1,..., fi, g1,..., gj] or better [[f1,...,fi], [g1,...,gj]]\n *\n * For each list of fields possible and each domain considered, one read_group is done\n * and gives results of the form (an exception for list [])\n *\n * g = {\n *  f1: v1, ..., fi: vi,\n *  g1: w1, ..., gj: wj,\n *  m1: x1, ..., mk: xk,\n *  __count: c,\n *  __domain: d\n * }\n *\n * where v1,...,vi,w1,...,Wj are 'values' for the corresponding fields and\n * m1,...,mk are the fields selected as measures.\n *\n * For example, g = {\n *      continent_id: [1, 'Europe']\n *      sale_team_id: [1, 'Sale Team 1']\n *      sales_count: 25,\n *      __count: 4\n *      __domain: [\n *                  ['sale_date', >=, 2020-05-01], ['sale_date', '<=', 2020-05-31],\n *                  ['continent_id', '=', 1],\n *                  ['sale_team_id', '=', 1]\n *                ]\n * }\n *\n * Thus the above group g is fully determined by [[v1,...,vi], [w1,...,wj]] and the base domain\n * or the corresponding 'originIndex'.\n *\n * When j=0, g corresponds to a row group (or also row header) and is of the form [[v1,...,vi], []] or more simply [v1,...vi]\n * (not forgetting the list [v1,...vi] comes from left).\n * When i=0, g corresponds to a col group (or col header) and is of the form [[], [w1,...,wj]] or more simply [w1,...,wj].\n *\n * A generic group g as above [[v1,...,vi], [w1,...,wj]] corresponds to the two headers [[v1,...,vi], []]\n * and [[], [w1,...,wj]].\n *\n * Here is a description of the data structure manipulated by the pivot model.\n *\n * Five objects contain all the data from the read_groups\n *\n *      - rowGroupTree: contains information on row headers\n *             the nodes correspond to the groups of the form [[v1,...,vi], []]\n *             The root is [[], []].\n *             A node [[v1,...,vl], []] has as direct children the nodes of the form [[v1,...,vl,v], []],\n *             this means that a direct child is obtained by grouping records using the single field fi+1\n *\n *             The structure at each level is of the form\n *\n *             {\n *                  root: {\n *                      values: [v1,...,vl],\n *                      labels: [la1,...,lal]\n *                  },\n *                  directSubTrees: {\n *                      v => {\n *                              root: {\n *                                  values: [v1,...,vl,v]\n *                                  labels: [label1,...,labell,label]\n *                              },\n *                              directSubTrees: {...}\n *                          },\n *                      v' => {...},\n *                      ...\n *                  }\n *             }\n *\n *             (directSubTrees is a Map instance)\n *\n *             In the example, the rowGroupTree is:\n *\n *             {\n *                  root: {\n *                      values: [],\n *                      labels: []\n *                  },\n *                  directSubTrees: {\n *                      1 => {\n *                              root: {\n *                                  values: [1],\n *                                  labels: ['Europe'],\n *                              },\n *                              directSubTrees: {\n *                                  1 => {\n *                                          root: {\n *                                              values: [1, 1],\n *                                              labels: ['Europe', 'Brussels'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                                  2 => {\n *                                          root: {\n *                                              values: [1, 2],\n *                                              labels: ['Europe', 'Paris'],\n *                                          },\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                          },\n *                      2 => {\n *                              root: {\n *                                  values: [2],\n *                                  labels: ['America'],\n *                              },\n *                              directSubTrees: {\n *                                  3 => {\n *                                          root: {\n *                                              values: [2, 3],\n *                                              labels: ['America', 'Washington'],\n *                                          }\n *                                          directSubTrees: new Map(),\n *                                  },\n *                              },\n *                      },\n *                  },\n *             }\n *\n *      - colGroupTree: contains information on col headers\n *              The same as above with right instead of left\n *\n *      - measurements: contains information on measure values for all the groups\n *\n *              the object keys are of the form JSON.stringify([[v1,...,vi], [w1,...,wj]])\n *              and values are arrays of length equal to number of origins containing objects of the form\n *                  {m1: x1,...,mk: xk}\n *              The structure looks like\n *\n *              {\n *                  JSON.stringify([[], []]): [{m1: x1,...,mk: xk}, {m1: x1',...,mk: xk'},...]\n *                  ....\n *                  JSON.stringify([[v1,...,vi], [w1,...,wj]]): [{m1: y1',...,mk: yk'}, {m1: y1',...,mk: yk'},...],\n *                  ....\n *                  JSON.stringify([[v1,...,vn], [w1,...,wm]]): [{m1: z1',...,mk: zk'}, {m1: z1',...,mk: zk'},...],\n *              }\n *              Thus the structure contains all information for all groups and all origins on measure values.\n *\n *\n *              this.measurments[\"[[], []]\"][0]['foo'] gives the value of the measure 'foo' for the group 'Total' and the\n *              first domain (origin).\n *\n *              In the example:\n *                  {\n *                      \"[[], []]\": [{'sales_total': 125}, {'sales_total': 140}]                      (total/total)\n *                      ...\n *                      \"[[1, 2], [2]]\": [{'sales_total': 10}, {'sales_total': 0}]                   (Europe/Paris/Sale Team 2)\n *                      ...\n *                  }\n *\n *      - counts: contains information on the number of records in each groups\n *              The structure is similar to the above but the arrays contains numbers (counts)\n *      - groupDomains:\n *              The structure is similar to the above but the arrays contains domains\n *\n *      With this light data structures, all manipulation done by the model are eased and redundancies are limited.\n *      Each time a rendering or an export of the data has to be done, the pivot table is generated by the getTable function.\n */\n\n/**\n * @typedef Meta\n * @property {string[]} activeMeasures\n * @property {string[]} colGroupBys\n * @property {boolean} disableLinking\n * @property {Object} fields\n * @property {Object} measures\n * @property {string} resModel\n * @property {string[]} rowGroupBys\n * @property {string} title\n * @property {boolean} useSampleModel\n * @property {Object} widgets\n * @property {Map} customGroupBys\n * @property {string[]} expandedRowGroupBys\n * @property {string[]} expandedColGroupBys\n * @property {Object} sortedColumn\n * @property {Array[]} domains\n * @property {string[]} origins\n */\n\n/**\n * @typedef Data\n * @property {Object} colGroupTree\n * @property {Object} rowGroupTree\n * @property {Object} groupDomains\n * @property {Object} measurements\n * @property {Object} counts\n * @property {Object} numbering\n */\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\n/**\n * @typedef Config\n * @property {MetaData} metaData\n * @property {Data} data\n */\n\nexport class PivotModel extends Model {\n    /**\n     * @override\n     * @param {Object} params.metaData\n     * @param {string[]} params.metaData.activeMeasures\n     * @param {string[]} params.metaData.colGroupBys\n     * @param {Object} params.metaData.fields\n     * @param {Object[]} params.metaData.measures\n     * @param {string} params.metaData.resModel\n     * @param {string[]} params.metaData.rowGroupBys\n     * @param {string|null} params.metaData.defaultOrder\n     * @param {boolean} params.metaData.disableLinking\n     * @param {boolean} params.metaData.useSampleModel\n     * @param {Map} [params.metaData.customGroupBys={}]\n     * @param {string[]} [params.metaData.expandedColGroupBys=[]]\n     * @param {string[]} [params.metaData.expandedRowGroupBys=[]]\n     * @param {Object|null} [params.metaData.sortedColumn=null]\n     * @param {Object} [params.data] previously exported data\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _loadData = this._loadData.bind(this);\n        this._loadData = (...args) => {\n            return this.race.add(_loadData(...args));\n        };\n\n        let sortedColumn = params.metaData.sortedColumn || null;\n        if (!sortedColumn && params.metaData.defaultOrder) {\n            const defaultOrder = params.metaData.defaultOrder.split(\" \");\n            sortedColumn = {\n                groupId: [[], []],\n                measure: defaultOrder[0],\n                order: defaultOrder[1] ? defaultOrder[1] : \"asc\",\n            };\n        }\n\n        this.searchParams = {\n            context: {},\n            domain: [],\n            domains: [],\n            groupBy: [],\n        };\n        this.data = params.data || {\n            colGroupTree: null,\n            rowGroupTree: null,\n            groupDomains: {},\n            measurements: {},\n            counts: {},\n            numbering: {},\n        };\n        const metaData = Object.assign({}, params.metaData, {\n            customGroupBys: params.metaData.customGroupBys || new Map(),\n            expandedRowGroupBys: params.metaData.expandedRowGroupBys || [],\n            expandedColGroupBys: params.metaData.expandedColGroupBys || [],\n            sortedColumn,\n        });\n        this.metaData = this._buildMetaData(metaData);\n\n        this.reload = false; // used to discriminate between the first load and subsequent reloads\n        this.nextActiveMeasures = null; // allows to toggle several measures consecutively\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add a groupBy to rowGroupBys or colGroupBys according to provided type.\n     *\n     * @param {Object} params\n     * @param {Array[]} params.groupId\n     * @param {string} params.fieldName\n     * @param {'row'|'col'} params.type\n     * @param {boolean} [params.custom=false]\n     * @param {string} [params.interval]\n     */\n    async addGroupBy(params) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const { groupId, fieldName, type, custom } = params;\n        let { interval } = params;\n        const metaData = this._buildMetaData();\n        if (custom && !metaData.customGroupBys.has(fieldName)) {\n            const field = metaData.fields[fieldName];\n            if (!interval && [\"date\", \"datetime\"].includes(field.type)) {\n                interval = DEFAULT_INTERVAL;\n            }\n            metaData.customGroupBys.set(fieldName, {\n                ...field,\n                id: fieldName,\n            });\n        }\n\n        let groupBy = fieldName;\n        if (interval) {\n            groupBy = `${groupBy}:${interval}`;\n        }\n        if (type === \"row\") {\n            metaData.expandedRowGroupBys.push(groupBy);\n        } else {\n            metaData.expandedColGroupBys.push(groupBy);\n        }\n        const config = { metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.metaData = metaData;\n        this.notify();\n    }\n    /**\n     * Close the group with id given by groupId. A type must be specified\n     * in case groupId is [[], []] (the id of the group 'Total') because this\n     * group is present in both colGroupTree and rowGroupTree.\n     *\n     * @param {Array[]} groupId\n     * @param {'row'|'col'} type\n     */\n    closeGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloading the table\n        }\n\n        let groupBys;\n        let expandedGroupBys;\n        let keyPart;\n        let group;\n        let tree;\n        if (type === \"row\") {\n            groupBys = this.metaData.rowGroupBys;\n            expandedGroupBys = this.metaData.expandedRowGroupBys;\n            tree = this.data.rowGroupTree;\n            group = this._findGroup(this.data.rowGroupTree, groupId[0]);\n            keyPart = 0;\n        } else {\n            groupBys = this.metaData.colGroupBys;\n            expandedGroupBys = this.metaData.expandedColGroupBys;\n            tree = this.data.colGroupTree;\n            group = this._findGroup(this.data.colGroupTree, groupId[1]);\n            keyPart = 1;\n        }\n\n        const groupIdPart = groupId[keyPart];\n        const range = groupIdPart.map((_, index) => index);\n        function keep(key) {\n            const idPart = JSON.parse(key)[keyPart];\n            return (\n                range.some((index) => groupIdPart[index] !== idPart[index]) ||\n                idPart.length === groupIdPart.length\n            );\n        }\n        function omitKeys(object) {\n            const newObject = {};\n            for (const key in object) {\n                if (keep(key)) {\n                    newObject[key] = object[key];\n                }\n            }\n            return newObject;\n        }\n        this.data.measurements = omitKeys(this.data.measurements);\n        this.data.counts = omitKeys(this.data.counts);\n        this.data.groupDomains = omitKeys(this.data.groupDomains);\n\n        group.directSubTrees.clear();\n        delete group.sortedKeys;\n        var newGroupBysLength = this._getTreeHeight(tree) - 1;\n        if (newGroupBysLength <= groupBys.length) {\n            expandedGroupBys.splice(0);\n            groupBys.splice(newGroupBysLength);\n        } else {\n            expandedGroupBys.splice(newGroupBysLength - groupBys.length);\n        }\n        this.notify();\n    }\n    /**\n     * Reload the view with the current rowGroupBys and colGroupBys\n     * This is the easiest way to expand all the groups that are not expanded\n     */\n    async expandAll() {\n        const config = { metaData: this.metaData, data: this.data };\n        await this._loadData(config, false);\n        this.notify();\n    }\n    /**\n     * Expand a group by using groupBy to split it and trigger a re-rendering.\n     *\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     */\n    async expandGroup(groupId, type) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        await this._expandGroup(groupId, type, config);\n        this.notify();\n    }\n    /**\n     * Export model data in a form suitable for an easy encoding of the pivot\n     * table in excell.\n     *\n     * @returns {Object}\n     */\n    exportData() {\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n\n        const table = this.getTable();\n\n        // process headers\n        const headers = table.headers;\n        let colGroupHeaderRows;\n        let measureRow = [];\n        let originRow = [];\n\n        function processHeader(header) {\n            const inTotalColumn = header.groupId[1].length === 0;\n            return {\n                title: header.title,\n                width: header.width,\n                height: header.height,\n                is_bold: !!header.measure && inTotalColumn,\n            };\n        }\n\n        if (originCount > 1) {\n            colGroupHeaderRows = headers.slice(0, headers.length - 2);\n            measureRow = headers[headers.length - 2].map(processHeader);\n            originRow = headers[headers.length - 1].map(processHeader);\n        } else {\n            colGroupHeaderRows = headers.slice(0, headers.length - 1);\n            measureRow = headers[headers.length - 1].map(processHeader);\n        }\n\n        // remove the empty headers on left side\n        colGroupHeaderRows[0].splice(0, 1);\n\n        colGroupHeaderRows = colGroupHeaderRows.map((headerRow) => {\n            return headerRow.map(processHeader);\n        });\n\n        // process rows\n        const tableRows = table.rows.map((row) => {\n            return {\n                title: row.title,\n                indent: row.indent,\n                values: row.subGroupMeasurements.map((measurement) => {\n                    let value = measurement.value;\n                    if (value === undefined) {\n                        value = \"\";\n                    } else if (measurement.originIndexes.length > 1) {\n                        // in that case the value is a variation and a\n                        // number between 0 and 1\n                        value = value * 100;\n                    }\n                    return {\n                        is_bold: measurement.isBold,\n                        value: value,\n                    };\n                }),\n            };\n        });\n\n        return {\n            model: this.metaData.resModel,\n            title: this.metaData.title,\n            col_group_headers: colGroupHeaderRows,\n            measure_headers: measureRow,\n            origin_headers: originRow,\n            rows: tableRows,\n            measure_count: measureCount,\n            origin_count: originCount,\n        };\n    }\n    /**\n     * Swap the pivot columns and the rows. The flip operation is synchronous.\n     * However, we must wait for a potential pending reload to complete before\n     * flipping the axes. This method is thus async.\n     */\n    async flip() {\n        await this.race.getCurrentProm();\n\n        // swap the data: the main column and the main row\n        let temp = this.data.rowGroupTree;\n        this.data.rowGroupTree = this.data.colGroupTree;\n        this.data.colGroupTree = temp;\n\n        // we need to update the record metaData: (expanded) row and col groupBys\n        temp = this.metaData.rowGroupBys;\n        this.metaData.rowGroupBys = this.metaData.colGroupBys;\n        this.metaData.colGroupBys = temp;\n        temp = this.metaData.expandedColGroupBys;\n        this.metaData.expandedColGroupBys = this.metaData.expandedRowGroupBys;\n        this.metaData.expandedRowGroupBys = temp;\n\n        function twistKey(key) {\n            return JSON.stringify(JSON.parse(key).reverse());\n        }\n\n        function twist(object) {\n            const newObject = {};\n            Object.keys(object).forEach((key) => {\n                const value = object[key];\n                newObject[twistKey(key)] = value;\n            });\n            return newObject;\n        }\n\n        this.data.measurements = twist(this.data.measurements);\n        this.data.counts = twist(this.data.counts);\n        this.data.groupDomains = twist(this.data.groupDomains);\n\n        this.notify();\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @returns {Array[]}\n     */\n    getGroupDomain(group) {\n        const config = { metaData: this.metaData, data: this.data };\n        return this._getGroupDomain(group, config);\n    }\n    /**\n     * Returns a description of the pivot table.\n     *\n     * @returns {Object}\n     */\n    getTable() {\n        const headers = this._getTableHeaders();\n        return {\n            headers: headers,\n            rows: this._getTableRows(this.data.rowGroupTree, headers[headers.length - 1]),\n        };\n    }\n    /**\n     * Returns the total number of columns of the pivot table.\n     *\n     * @returns {integer}\n     */\n    getTableWidth() {\n        var leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        return leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] + 2;\n    }\n    /**\n     * @returns {boolean} true iff there's no data in the table\n     */\n    hasData() {\n        return this._hasData(this.data);\n    }\n    /**\n     * @override\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n        const processedMeasures = processMeasure(searchParams.context.pivot_measures);\n        const activeMeasures = processedMeasures || this.metaData.activeMeasures;\n        const metaData = this._buildMetaData({ activeMeasures });\n        if (!this.reload) {\n            metaData.rowGroupBys =\n                searchParams.context.pivot_row_groupby ||\n                (searchParams.groupBy.length ? searchParams.groupBy : metaData.rowGroupBys);\n            this.reload = true;\n        } else {\n            metaData.rowGroupBys = searchParams.groupBy.length\n                ? searchParams.groupBy\n                : searchParams.context.pivot_row_groupby || metaData.rowGroupBys;\n        }\n        metaData.colGroupBys =\n            searchParams.context.pivot_column_groupby || this.metaData.colGroupBys;\n\n        if (JSON.stringify(metaData.rowGroupBys) !== JSON.stringify(this.metaData.rowGroupBys)) {\n            metaData.expandedRowGroupBys = [];\n        }\n        if (JSON.stringify(metaData.colGroupBys) !== JSON.stringify(this.metaData.colGroupBys)) {\n            metaData.expandedColGroupBys = [];\n        }\n\n        const allActivesMeasures = new Set(this.metaData.activeMeasures);\n        if (processedMeasures) {\n            processedMeasures.forEach((e) => allActivesMeasures.add(e));\n        }\n\n        metaData.measures = computeReportMeasures(metaData.fields, metaData.fieldAttrs, [\n            ...allActivesMeasures,\n        ]);\n        const config = { metaData, data: this.data };\n        await addPropertyFieldDefs(\n            this.orm,\n            metaData.resModel,\n            searchParams.context,\n            metaData.fields,\n            new Set([...metaData.rowGroupBys, ...metaData.colGroupBys])\n        );\n        return this._loadData(config);\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     */\n    sortRows(sortedColumn) {\n        if (this.race.getCurrentProm()) {\n            return; // we are currently reloaded the table\n        }\n\n        const config = { metaData: this.metaData, data: this.data };\n        this._sortRows(sortedColumn, config);\n\n        this.notify();\n    }\n    /**\n     * Toggle the active state for a given measure, then reload the data\n     * if this turns out to be necessary.\n     *\n     * @param {string} fieldName\n     * @returns {Promise}\n     */\n    async toggleMeasure(fieldName) {\n        const metaData = this._buildMetaData();\n        this.nextActiveMeasures = this.nextActiveMeasures || metaData.activeMeasures;\n        metaData.activeMeasures = this.nextActiveMeasures;\n        const index = metaData.activeMeasures.indexOf(fieldName);\n        if (index !== -1) {\n            // in this case, we already have all data in memory, no need to\n            // actually reload a lesser amount of information (but still, we need\n            // to wait in case there is a pending load)\n            metaData.activeMeasures.splice(index, 1);\n            await Promise.resolve(this.race.getCurrentProm());\n            this.metaData = metaData;\n        } else {\n            metaData.activeMeasures.push(fieldName);\n            const config = { metaData, data: this.data };\n            await this._loadData(config);\n            this.useSampleModel = false;\n        }\n        this.nextActiveMeasures = null;\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add labels/values in the provided groupTree. A new leaf is created in\n     * the groupTree with a root object corresponding to the group with given\n     * labels/values.\n     *\n     * @protected\n     * @param {Object} groupTree, either this.data.rowGroupTree or this.data.colGroupTree\n     * @param {string[]} labels\n     * @param {Array} values\n     */\n    _addGroup(groupTree, labels, values) {\n        let tree = groupTree;\n        // we assume here that the group with value value.slice(value.length - 2) has already been added.\n        values.slice(0, values.length - 1).forEach(function (value) {\n            tree = tree.directSubTrees.get(value);\n        });\n        const value = values[values.length - 1];\n        if (tree.directSubTrees.has(value)) {\n            return;\n        }\n        tree.directSubTrees.set(value, {\n            root: {\n                labels: labels,\n                values: values,\n            },\n            directSubTrees: new Map(),\n        });\n    }\n    /**\n     * Return a copy of this.metaData, extended with optional params. This is useful\n     * for async methods that need to modify this.metaData, but it can't be done in\n     * place directly for the model to be concurrency proof (so they work on a\n     * copy and commit it at the end).\n     *\n     * @protected\n     * @param {Object} params\n     * @returns {Object}\n     */\n    _buildMetaData(params) {\n        const metaData = Object.assign({}, this.metaData, params);\n        metaData.activeMeasures = [...metaData.activeMeasures];\n        metaData.colGroupBys = [...metaData.colGroupBys];\n        metaData.rowGroupBys = [...metaData.rowGroupBys];\n        metaData.expandedColGroupBys = [...metaData.expandedColGroupBys];\n        metaData.expandedRowGroupBys = [...metaData.expandedRowGroupBys];\n        metaData.customGroupBys = new Map([...metaData.customGroupBys]);\n        // shallow copy sortedColumn because we never modify groupId in place\n        metaData.sortedColumn = metaData.sortedColumn ? { ...metaData.sortedColumn } : null;\n        if (this.searchParams.comparison) {\n            const domains = this.searchParams.comparison.domains.slice().reverse();\n            metaData.domains = domains.map((d) => d.arrayRepr);\n            metaData.origins = domains.map((d) => d.description);\n        } else {\n            metaData.domains = [this.searchParams.domain];\n            metaData.origins = [\"\"];\n        }\n        Object.defineProperty(metaData, \"fullColGroupBys\", {\n            get() {\n                return metaData.colGroupBys.concat(metaData.expandedColGroupBys);\n            },\n        });\n        Object.defineProperty(metaData, \"fullRowGroupBys\", {\n            get() {\n                return metaData.rowGroupBys.concat(metaData.expandedRowGroupBys);\n            },\n        });\n        return metaData;\n    }\n    /**\n     * Expand a group by using groupBy to split it.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {'row'|'col'} type\n     * @param {Config} config\n     */\n    async _expandGroup(groupId, type, config) {\n        const { metaData } = config;\n        const group = {\n            rowValues: groupId[0],\n            colValues: groupId[1],\n            type: type,\n        };\n        const groupValues = type === \"row\" ? groupId[0] : groupId[1];\n        const groupBys = type === \"row\" ? metaData.fullRowGroupBys : metaData.fullColGroupBys;\n        if (groupValues.length >= groupBys.length) {\n            throw new Error(\"Cannot expand group\");\n        }\n        const groupBy = groupBys[groupValues.length];\n        let leftDivisors;\n        let rightDivisors;\n        if (group.type === \"row\") {\n            leftDivisors = [[groupBy]];\n            rightDivisors = sections(metaData.fullColGroupBys);\n        } else {\n            leftDivisors = sections(metaData.fullRowGroupBys);\n            rightDivisors = [[groupBy]];\n        }\n        const divisors = cartesian(leftDivisors, rightDivisors);\n        delete group.type;\n        await this._subdivideGroup(group, divisors, config);\n    }\n    /**\n     * Find a group with given values in the provided groupTree, either\n     * this.rowGrouptree or this.data.colGroupTree.\n     *\n     * @protected\n     * @param {Object} groupTree\n     * @param {Array} values\n     * @returns {Object}\n     */\n    _findGroup(groupTree, values) {\n        let tree = groupTree;\n        values.slice(0, values.length).forEach((value) => {\n            tree = tree.directSubTrees.get(value);\n        });\n        return tree;\n    }\n    /**\n     * In case originIndex is an array of length 1, thus a single origin\n     * index, returns the given measure for a group determined by the id\n     * groupId and the origin index.\n     * If originIndexes is an array of length 2, we compute the variation\n     * of the measure values for the groups determined by groupId and the\n     * different origin indexes.\n     *\n     * @protected\n     * @param {Array[]} groupId\n     * @param {string} measure\n     * @param {number[]} originIndexes\n     * @param {Config} config\n     * @returns {number}\n     */\n    _getCellValue(groupId, measure, originIndexes, config) {\n        var key = JSON.stringify(groupId);\n        if (!config.data.measurements[key]) {\n            return;\n        }\n        var values = originIndexes.map((originIndex) => {\n            return config.data.measurements[key][originIndex][measure];\n        });\n        if (originIndexes.length > 1) {\n            return computeVariation(values[1], values[0]);\n        } else {\n            return values[0];\n        }\n    }\n    /**\n     * @protected\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @returns {string[]}\n     */\n    _getGroupBySpecs(rowGroupBy, colGroupBy) {\n        const set = rowGroupBy.concat(colGroupBy).reduce((acc, gb) => {\n            acc.add(this._normalize(gb));\n            return acc;\n        }, new Set());\n        return [...set];\n    }\n    /**\n     * Returns a domain representation of a group\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array} group.colValues\n     * @param {Array} group.rowValues\n     * @param {number} group.originIndex\n     * @param {Config} config\n     * @returns {Array[]}\n     */\n    _getGroupDomain(group, config) {\n        const { data } = config;\n        var key = JSON.stringify([group.rowValues, group.colValues]);\n        return data.groupDomains[key][group.originIndex];\n    }\n    /**\n     * Returns the group sanitized labels.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @param {Config} config\n     * @returns {string[]}\n     */\n    _getGroupLabels(group, groupBys, config) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeLabel(group[groupBy], groupBy, config);\n        });\n    }\n    /**\n     * Returns a promise that returns the annotated read_group results\n     * corresponding to a partition of the given group obtained using the given\n     * rowGroupBy and colGroupBy.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} rowGroupBy\n     * @param {string[]} colGroupBy\n     * @param {Object} params\n     */\n    async _getGroupSubdivision(group, rowGroupBy, colGroupBy, params) {\n        const groupBy = this._getGroupBySpecs(rowGroupBy, colGroupBy);\n        const subGroups = await this._getSubGroups(groupBy, params);\n        return {\n            group,\n            subGroups,\n            rowGroupBy: rowGroupBy,\n            colGroupBy: colGroupBy,\n        };\n    }\n\n    /**\n     * Returns the group sanitized values.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {string[]} groupBys\n     * @returns {Array}\n     */\n    _getGroupValues(group, groupBys) {\n        return groupBys.map((gb) => {\n            const groupBy = this._normalize(gb);\n            return this._sanitizeValue(group[groupBy]);\n        });\n    }\n    /**\n     * Returns the leaf counts of each group inside the given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @returns {Object} keys are group ids\n     */\n    _getLeafCounts(tree) {\n        const leafCounts = {};\n        let leafCount;\n        if (!tree.directSubTrees.size) {\n            leafCount = 1;\n        } else {\n            leafCount = [...tree.directSubTrees.values()].reduce((acc, subTree) => {\n                const subLeafCounts = this._getLeafCounts(subTree);\n                Object.assign(leafCounts, subLeafCounts);\n                return acc + leafCounts[JSON.stringify(subTree.root.values)];\n            }, 0);\n        }\n\n        leafCounts[JSON.stringify(tree.root.values)] = leafCount;\n        return leafCounts;\n    }\n    /**\n     * Returns the group sanitized measure values for the measures in\n     * this.metaData.activeMeasures (that migth contain '__count', not really a fieldName).\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Config} config\n     * @returns {Array}\n     */\n    _getMeasurements(group, config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((measurements, measureName) => {\n            var measurement = group[measureName];\n            if (measurement instanceof Array) {\n                // case field is many2one and used as measure and groupBy simultaneously\n                measurement = 1;\n            }\n            if (\n                metaData.measures[measureName].type === \"boolean\" &&\n                measurement instanceof Boolean\n            ) {\n                measurement = measurement ? 1 : 0;\n            }\n            if (metaData.origins.length > 1 && !measurement) {\n                measurement = 0;\n            }\n            measurements[measureName] = measurement;\n            return measurements;\n        }, {});\n    }\n    /**\n     * Returns a description of the measures row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which measure cells must be generated\n     * @returns {Object[]}\n     */\n    _getMeasuresRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const measureRow = [];\n\n        columns.forEach((column) => {\n            this.metaData.activeMeasures.forEach((measureName) => {\n                const measureCell = {\n                    groupId: column.groupId,\n                    height: 1,\n                    measure: measureName,\n                    title: this.metaData.measures[measureName].string,\n                    width: 2 * this.metaData.origins.length - 1,\n                };\n                if (\n                    sortedColumn.measure === measureName &&\n                    JSON.stringify(sortedColumn.groupId) === JSON.stringify(column.groupId) // FIXME\n                ) {\n                    measureCell.order = sortedColumn.order;\n                }\n                measureRow.push(measureCell);\n            });\n        });\n\n        return measureRow;\n    }\n    /**\n     * Returns the list of measure specs associated with metaData.activeMeasures, i.e.\n     * a measure 'fieldName' becomes 'fieldName:aggregator' where\n     * aggregator is the value specified on the field 'fieldName' for\n     * the key aggregator.\n     *\n     * @protected\n     * @param {Config} config\n     * @return {string[]}\n     */\n    _getMeasureSpecs(config) {\n        const { metaData } = config;\n        return metaData.activeMeasures.reduce((acc, measure) => {\n            if (measure === \"__count\") {\n                acc.push(measure);\n                return acc;\n            }\n            const field = this.metaData.fields[measure];\n            if (field.type === \"many2one\") {\n                field.aggregator = \"count_distinct\";\n            }\n            if (field.aggregator === undefined) {\n                throw new Error(\n                    \"No aggregate function has been provided for the measure '\" + measure + \"'\"\n                );\n            }\n            acc.push(measure + \":\" + field.aggregator);\n            return acc;\n        }, []);\n    }\n    /**\n     * Make sure that the labels of different many2one values are distinguished\n     * by numbering them if necessary.\n     *\n     * @protected\n     * @param {Array} label\n     * @param {string} fieldName\n     * @param {Config} config\n     * @returns {string}\n     */\n    _getNumberedLabel(label, fieldName, config) {\n        const { data } = config;\n        const id = label[0];\n        const name = label[1];\n        data.numbering[fieldName] = data.numbering[fieldName] || {};\n        data.numbering[fieldName][name] = data.numbering[fieldName][name] || {};\n        const numbers = data.numbering[fieldName][name];\n        numbers[id] = numbers[id] || Object.keys(numbers).length + 1;\n        return name + (numbers[id] > 1 ? \"  (\" + numbers[id] + \")\" : \"\");\n    }\n    /**\n     * Returns a description of the origins row of the pivot table\n     *\n     * @protected\n     * @param {Object[]} columns for which origin cells must be generated\n     * @returns {Object[]}\n     */\n    _getOriginsRow(columns) {\n        const sortedColumn = this.metaData.sortedColumn || {};\n        const originRow = [];\n\n        columns.forEach((column) => {\n            const groupId = column.groupId;\n            const measure = column.measure;\n            const isSorted =\n                sortedColumn.measure === measure &&\n                JSON.stringify(sortedColumn.groupId) === JSON.stringify(groupId); // FIXME\n            const isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1];\n            const isSortedByVariation = isSorted && sortedColumn.originIndexes[1];\n\n            this.metaData.origins.forEach((origin, originIndex) => {\n                const originCell = {\n                    groupId: groupId,\n                    height: 1,\n                    measure: measure,\n                    originIndexes: [originIndex],\n                    title: origin,\n                    width: 1,\n                };\n                if (isSortedByOrigin && sortedColumn.originIndexes[0] === originIndex) {\n                    originCell.order = sortedColumn.order;\n                }\n                originRow.push(originCell);\n\n                if (originIndex > 0) {\n                    const variationCell = {\n                        groupId: groupId,\n                        height: 1,\n                        measure: measure,\n                        originIndexes: [originIndex - 1, originIndex],\n                        title: _t(\"Variation\"),\n                        width: 1,\n                    };\n                    if (isSortedByVariation && sortedColumn.originIndexes[1] === originIndex) {\n                        variationCell.order = sortedColumn.order;\n                    }\n                    originRow.push(variationCell);\n                }\n            });\n        });\n\n        return originRow;\n    }\n    /**\n     * @protected\n     * @param {string[]} groupBy\n     * @param {Object} params\n     * @returns {Promise<Object[]>}\n     */\n    async _getSubGroups(groupBy, params) {\n        const { resModel, groupDomain, measureSpecs, kwargs, mapping } = params;\n        const key = JSON.stringify(groupBy);\n        if (!mapping[key]) {\n            mapping[key] = this.orm.readGroup(resModel, groupDomain, measureSpecs, groupBy, kwargs);\n        }\n        return mapping[key];\n    }\n    /**\n     * Returns the list of header rows of the pivot table: the col group rows\n     * (depending on the col groupbys), the measures row and optionnaly the\n     * origins row (if there are more than one origins).\n     *\n     * @protected\n     * @returns {Object[]}\n     */\n    _getTableHeaders() {\n        const colGroupBys = this.metaData.fullColGroupBys;\n        const height = colGroupBys.length + 1;\n        const measureCount = this.metaData.activeMeasures.length;\n        const originCount = this.metaData.origins.length;\n        const leafCounts = this._getLeafCounts(this.data.colGroupTree);\n        let headers = [];\n        const measureColumns = []; // used to generate the measure cells\n\n        // 1) generate col group rows (total row + one row for each col groupby)\n        const colGroupRows = new Array(height).fill(0).map(() => []);\n        // blank top left cell\n        colGroupRows[0].push({\n            height: height + 1 + (originCount > 1 ? 1 : 0), // + measures rows [+ origins row]\n            title: \"\",\n            width: 1,\n        });\n\n        // col groupby cells with group values\n        /**\n         * Recursive function that generates the header cells corresponding to\n         * the groups of a given tree.\n         *\n         * @param {Object} tree\n         */\n        function generateTreeHeaders(tree, fields) {\n            const group = tree.root;\n            const rowIndex = group.values.length;\n            const row = colGroupRows[rowIndex];\n            const groupId = [[], group.values];\n            const isLeaf = !tree.directSubTrees.size;\n            const leafCount = leafCounts[JSON.stringify(tree.root.values)];\n            const cell = {\n                groupId: groupId,\n                height: isLeaf ? colGroupBys.length + 1 - rowIndex : 1,\n                isLeaf: isLeaf,\n                isFolded: isLeaf && colGroupBys.length > group.values.length,\n                label:\n                    rowIndex === 0\n                        ? undefined\n                        : fields[colGroupBys[rowIndex - 1].split(\":\")[0]].string,\n                title: group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\"),\n                width: leafCount * measureCount * (2 * originCount - 1),\n            };\n            row.push(cell);\n            if (isLeaf) {\n                measureColumns.push(cell);\n            }\n\n            [...tree.directSubTrees.values()].forEach((subTree) => {\n                generateTreeHeaders(subTree, fields);\n            });\n        }\n\n        generateTreeHeaders(this.data.colGroupTree, this.metaData.fields);\n        // blank top right cell for 'Total' group (if there is more that one leaf)\n        if (leafCounts[JSON.stringify(this.data.colGroupTree.root.values)] > 1) {\n            var groupId = [[], []];\n            var totalTopRightCell = {\n                groupId: groupId,\n                height: height,\n                title: \"\",\n                width: measureCount * (2 * originCount - 1),\n            };\n            colGroupRows[0].push(totalTopRightCell);\n            measureColumns.push(totalTopRightCell);\n        }\n        headers = headers.concat(colGroupRows);\n\n        // 2) generate measures row\n        var measuresRow = this._getMeasuresRow(measureColumns);\n        headers.push(measuresRow);\n\n        // 3) generate origins row if more than one origin\n        if (originCount > 1) {\n            headers.push(this._getOriginsRow(measuresRow));\n        }\n\n        return headers;\n    }\n    /**\n     * Returns the list of body rows of the pivot table for a given tree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object[]} columns\n     * @returns {Object[]}\n     */\n    _getTableRows(tree, columns) {\n        let rows = [];\n        const group = tree.root;\n        const rowGroupId = [group.values, []];\n        const title = group.labels.length ? group.labels[group.labels.length - 1] : _t(\"Total\");\n        const indent = group.labels.length;\n        const isLeaf = !tree.directSubTrees.size;\n        const rowGroupBys = this.metaData.fullRowGroupBys;\n\n        const subGroupMeasurements = columns.map((column) => {\n            const colGroupId = column.groupId;\n            const groupIntersectionId = [rowGroupId[0], colGroupId[1]];\n            const measure = column.measure;\n            const originIndexes = column.originIndexes || [0];\n\n            const value = this._getCellValue(groupIntersectionId, measure, originIndexes, {\n                data: this.data,\n            });\n\n            const measurement = {\n                groupId: groupIntersectionId,\n                originIndexes: originIndexes,\n                measure: measure,\n                value: value,\n                isBold: !groupIntersectionId[0].length || !groupIntersectionId[1].length,\n            };\n            return measurement;\n        });\n\n        rows.push({\n            title: title,\n            label:\n                indent === 0\n                    ? undefined\n                    : this.metaData.fields[rowGroupBys[indent - 1].split(\":\")[0]].string,\n            groupId: rowGroupId,\n            indent: indent,\n            isLeaf: isLeaf,\n            isFolded: isLeaf && rowGroupBys.length > group.values.length,\n            subGroupMeasurements: subGroupMeasurements,\n        });\n\n        const subTreeKeys = tree.sortedKeys || [...tree.directSubTrees.keys()];\n        subTreeKeys.forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            rows = rows.concat(this._getTableRows(subTree, columns));\n        });\n\n        return rows;\n    }\n    /**\n     * returns the height of a given groupTree\n     *\n     * @protected\n     * @param {Object} tree, a groupTree\n     * @returns {number}\n     */\n    _getTreeHeight(tree) {\n        const subTreeHeights = [...tree.directSubTrees.values()].map(\n            this._getTreeHeight.bind(this)\n        );\n        return Math.max(0, Math.max.apply(null, subTreeHeights)) + 1;\n    }\n    /**\n     * @protected\n     * @param {Data} data\n     * @returns {boolean} true iff there's no data in the table\n     */\n    _hasData(data) {\n        return (data.counts[JSON.stringify([[], []])] || []).some((count) => {\n            return count > 0;\n        });\n    }\n    /**\n     * Initialize/Reinitialize data.rowGroupTree, colGroupTree, measurements,\n     * counts and subdivide the group 'Total' as many times it is necessary.\n     * A first subdivision with no groupBy (divisors.slice(0, 1)) is made in\n     * order to see if there is data in the intersection of the group 'Total'\n     * and the various origins. In case there is none, non supplementary rpc\n     * will be done (see the code of subdivideGroup).\n     *\n     * @protected\n     * @param {Config} config\n     */\n    async _loadData(config, prune = true) {\n        config.data = {}; // data will be completely recomputed\n        const { data, metaData } = config;\n        data.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: new Map() };\n        data.measurements = {};\n        data.counts = {};\n        data.groupDomains = {};\n        data.numbering = {};\n        const key = JSON.stringify([[], []]);\n        data.groupDomains[key] = metaData.domains.slice(0);\n\n        const group = { rowValues: [], colValues: [] };\n        const leftDivisors = sections(metaData.fullRowGroupBys);\n        const rightDivisors = sections(metaData.fullColGroupBys);\n        const divisors = cartesian(leftDivisors, rightDivisors);\n\n        await this._subdivideGroup(group, divisors.slice(0, 1), config);\n        await this._subdivideGroup(group, divisors.slice(1), config);\n\n        // keep folded groups folded after the reload if the structure of the table is the same\n        if (prune && this._hasData(data) && this._hasData(this.data)) {\n            if (\n                symmetricalDifference(metaData.rowGroupBys, this.metaData.rowGroupBys).length === 0\n            ) {\n                this._pruneTree(data.rowGroupTree, this.data.rowGroupTree);\n            }\n            if (\n                symmetricalDifference(metaData.colGroupBys, this.metaData.colGroupBys).length === 0\n            ) {\n                this._pruneTree(data.colGroupTree, this.data.colGroupTree);\n            }\n        }\n\n        this.data = config.data;\n        this.metaData = config.metaData;\n    }\n    /**\n     * @protected\n     * @param {string} gb\n     * @returns {string}\n     */\n    _normalize(gb) {\n        const [fieldName, interval] = gb.split(\":\");\n        const field = this.metaData.fields[fieldName];\n        if ([\"date\", \"datetime\"].includes(field.type)) {\n            return `${fieldName}:${interval || \"month\"}`;\n        } else {\n            return fieldName;\n        }\n    }\n    /**\n     * Extract the information in the read_group results (groupSubdivisions)\n     * and develop this.data.rowGroupTree, colGroupTree, measurements, counts, and\n     * groupDomains.\n     * If a column needs to be sorted, the rowGroupTree corresponding to the\n     * group is sorted.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Object[]} groupSubdivisions\n     * @param {Config} config\n     */\n    _prepareData(group, groupSubdivisions, config) {\n        const { data, metaData } = config;\n        const groupRowValues = group.rowValues;\n        let groupRowLabels = [];\n        let rowSubTree = data.rowGroupTree;\n        let root;\n        if (groupRowValues.length) {\n            // we should have labels information on hand! regretful!\n            rowSubTree = this._findGroup(data.rowGroupTree, groupRowValues);\n            root = rowSubTree.root;\n            groupRowLabels = root.labels;\n        }\n\n        const groupColValues = group.colValues;\n        let groupColLabels = [];\n        if (groupColValues.length) {\n            root = this._findGroup(data.colGroupTree, groupColValues).root;\n            groupColLabels = root.labels;\n        }\n\n        groupSubdivisions.forEach((groupSubdivision) => {\n            groupSubdivision.subGroups.forEach((subGroup) => {\n                const rowValues = groupRowValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.rowGroupBy)\n                );\n                const rowLabels = groupRowLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.rowGroupBy, config)\n                );\n\n                const colValues = groupColValues.concat(\n                    this._getGroupValues(subGroup, groupSubdivision.colGroupBy)\n                );\n                const colLabels = groupColLabels.concat(\n                    this._getGroupLabels(subGroup, groupSubdivision.colGroupBy, config)\n                );\n\n                if (!colValues.length && rowValues.length) {\n                    this._addGroup(data.rowGroupTree, rowLabels, rowValues);\n                }\n                if (colValues.length && !rowValues.length) {\n                    this._addGroup(data.colGroupTree, colLabels, colValues);\n                }\n\n                const key = JSON.stringify([rowValues, colValues]);\n                const originIndex = groupSubdivision.group.originIndex;\n\n                if (!(key in data.measurements)) {\n                    data.measurements[key] = metaData.origins.map(() => {\n                        return this._getMeasurements({}, config);\n                    });\n                }\n                data.measurements[key][originIndex] = this._getMeasurements(subGroup, config);\n\n                if (!(key in data.counts)) {\n                    data.counts[key] = metaData.origins.map(function () {\n                        return 0;\n                    });\n                }\n                data.counts[key][originIndex] = subGroup.__count;\n\n                if (!(key in data.groupDomains)) {\n                    data.groupDomains[key] = metaData.origins.map(function () {\n                        return Domain.FALSE.toList();\n                    });\n                }\n                // if __domain is not defined this means that we are in the\n                // case where\n                // groupSubdivision.rowGroupBy = groupSubdivision.rowGroupBy = []\n                if (subGroup.__domain) {\n                    data.groupDomains[key][originIndex] = subGroup.__domain;\n                }\n            });\n        });\n\n        if (metaData.sortedColumn) {\n            this._sortRows(metaData.sortedColumn, config);\n        }\n    }\n    /**\n     * Make any group in tree a leaf if it was a leaf in oldTree.\n     *\n     * @protected\n     * @param {Object} tree\n     * @param {Object} oldTree\n     */\n    _pruneTree(tree, oldTree) {\n        if (!oldTree.directSubTrees.size) {\n            tree.directSubTrees.clear();\n            delete tree.sortedKeys;\n            return;\n        }\n        [...tree.directSubTrees.keys()].forEach((subTreeKey) => {\n            const subTree = tree.directSubTrees.get(subTreeKey);\n            if (!oldTree.directSubTrees.has(subTreeKey)) {\n                subTree.directSubTrees.clear();\n                delete subTree.sortedKeys;\n            } else {\n                const oldSubTree = oldTree.directSubTrees.get(subTreeKey);\n                this._pruneTree(subTree, oldSubTree);\n            }\n        });\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    /**\n     * Extract from a groupBy value a label.\n     *\n     * @protected\n     * @param {any} value\n     * @param {string} groupBy\n     * @param {Config} config\n     * @returns {string}\n     */\n    _sanitizeLabel(value, groupBy, config) {\n        const { metaData } = config;\n        const fieldName = groupBy.split(\":\")[0];\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"boolean\"\n        ) {\n            return value === undefined ? _t(\"None\") : value ? _t(\"Yes\") : _t(\"No\");\n        }\n        if (value === false) {\n            return this._getEmptyGroupLabel(fieldName);\n        }\n        if (value instanceof Array) {\n            return this._getNumberedLabel(value, fieldName, config);\n        }\n        if (\n            fieldName &&\n            metaData.fields[fieldName] &&\n            metaData.fields[fieldName].type === \"selection\"\n        ) {\n            const selected = metaData.fields[fieldName].selection.find((o) => o[0] === value);\n            return selected ? selected[1] : value; // selected should be truthy normally ?!\n        }\n        return value;\n    }\n    /**\n     * Extract from a groupBy value the raw value of that groupBy (discarding\n     * a label if any)\n     *\n     * @protected\n     * @param {any} value\n     * @returns {any}\n     */\n    _sanitizeValue(value) {\n        if (value instanceof Array) {\n            return value[0];\n        }\n        return value;\n    }\n    /**\n     * Get all partitions of a given group using the provided list of divisors\n     * and enrich the objects of this.data.rowGroupTree, colGroupTree,\n     * measurements, counts.\n     *\n     * @protected\n     * @param {Object} group\n     * @param {Array[]} divisors\n     * @param {Config} config\n     */\n    async _subdivideGroup(group, divisors, config) {\n        const { data, metaData } = config;\n        const key = JSON.stringify([group.rowValues, group.colValues]);\n\n        const proms = metaData.origins.reduce((acc, origin, originIndex) => {\n            // if no information on group content is available, we fetch data.\n            // if group is known to be empty for the given origin,\n            // we don't need to fetch data for that origin.\n            if (!data.counts[key] || data.counts[key][originIndex] > 0) {\n                const subGroup = {\n                    rowValues: group.rowValues,\n                    colValues: group.colValues,\n                    originIndex: originIndex,\n                };\n                const groupDomain = this._getGroupDomain(subGroup, config);\n                const measureSpecs = this._getMeasureSpecs(config);\n                const resModel = config.metaData.resModel;\n                const kwargs = { lazy: false, context: this.searchParams.context };\n                const mapping = {};\n                divisors.forEach((divisor) => {\n                    acc.push(\n                        this._getGroupSubdivision(subGroup, divisor[0], divisor[1], {\n                            resModel,\n                            groupDomain,\n                            measureSpecs,\n                            kwargs,\n                            mapping,\n                        })\n                    );\n                });\n            }\n            return acc;\n        }, []);\n        const groupSubdivisions = await this.keepLast.add(Promise.all(proms));\n        if (groupSubdivisions.length) {\n            this._prepareData(group, groupSubdivisions, config);\n        }\n    }\n    /**\n     * Sort the rows, depending on the values of a given column.  This is an\n     * in-memory sort.\n     *\n     * @protected\n     * @param {Object} sortedColumn\n     * @param {number[]} sortedColumn.groupId\n     * @param {Config} config\n     */\n    _sortRows(sortedColumn, config) {\n        const metaData = config.metaData || this.metaData;\n        const data = config.data || this.data;\n        const colGroupValues = sortedColumn.groupId[1];\n        sortedColumn.originIndexes = sortedColumn.originIndexes || [0];\n        metaData.sortedColumn = sortedColumn;\n\n        const sortFunction = (tree) => {\n            return (subTreeKey) => {\n                const subTree = tree.directSubTrees.get(subTreeKey);\n                const groupIntersectionId = [subTree.root.values, colGroupValues];\n                const value =\n                    this._getCellValue(\n                        groupIntersectionId,\n                        sortedColumn.measure,\n                        sortedColumn.originIndexes,\n                        { data }\n                    ) || 0;\n                return sortedColumn.order === \"asc\" ? value : -value;\n            };\n        };\n\n        this._sortTree(sortFunction, data.rowGroupTree);\n    }\n    /**\n     * Sort recursively the subTrees of tree using sortFunction.\n     * In the end each node of the tree has its direct children sorted\n     * according to the criterion reprensented by sortFunction.\n     *\n     * @protected\n     * @param {Function} sortFunction\n     * @param {Object} tree\n     */\n    _sortTree(sortFunction, tree) {\n        tree.sortedKeys = sortBy([...tree.directSubTrees.keys()], sortFunction(tree));\n        [...tree.directSubTrees.values()].forEach((subTree) => {\n            this._sortTree(sortFunction, subTree);\n        });\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { registry } from \"@web/core/registry\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\nimport { PivotHeader } from \"@web/views/pivot/pivot_header\";\n\nimport { Component, onWillUpdateProps, useRef } from \"@odoo/owl\";\nimport { download } from \"@web/core/network/download\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst formatters = registry.category(\"formatters\");\n\nexport class PivotRenderer extends Component {\n    static template = \"web.PivotRenderer\";\n    static components = { Dropdown, DropdownItem, CheckBox, PivotHeader, ReportViewMeasures };\n    static props = [\"model\", \"buttonTemplate\"];\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.model = this.props.model;\n        this.table = this.model.getTable();\n        this.l10n = localization;\n        this.tableRef = useRef(\"table\");\n\n        onWillUpdateProps(this.onWillUpdateProps);\n    }\n    onWillUpdateProps() {\n        this.table = this.model.getTable();\n    }\n    /**\n     * Get the formatted value of the cell.\n     *\n     * @private\n     * @param {Object} cell\n     * @returns {string} Formatted value\n     */\n    getFormattedValue(cell) {\n        const field = this.model.metaData.measures[cell.measure];\n        let formatType = this.model.metaData.widgets[cell.measure];\n        if (!formatType) {\n            const fieldType = field.type;\n            formatType = [\"many2one\", \"reference\"].includes(fieldType) ? \"integer\" : fieldType;\n        }\n        const formatter = formatters.get(formatType);\n        return formatter(cell.value, field);\n    }\n    /**\n     * Get the formatted variation of a cell.\n     *\n     * @private\n     * @param {Object} cell\n     * @returns {string} Formatted variation\n     */\n    getFormattedVariation(cell) {\n        if (isNaN(cell.value)) {\n            return \"-\";\n        }\n        return formatPercentage(cell.value, this.model.metaData.fields[cell.measure]);\n    }\n\n    getHeaderProps({ cell, isXAxis = false, isInHead = false }) {\n        const type = isXAxis ? \"col\" : \"row\";\n        return {\n            cell,\n            isXAxis,\n            isInHead,\n            customGroupBys: this.model.metaData.customGroupBys,\n            onItemSelected: (payload) => this.onGroupBySelected(type, payload),\n            onAddCustomGroupBy: (fieldName) =>\n                this.onAddCustomGroupBy(type, cell.groupId, fieldName),\n            onClick: () => this.onHeaderClick(cell, type),\n        };\n    }\n\n    //----------------------------------------------------------------------\n    // Handlers\n    //----------------------------------------------------------------------\n\n    /**\n     * Handle the adding of a custom groupby (inside the view, not the searchview).\n     *\n     * @param {\"col\"|\"row\"} type\n     * @param {Array[]} groupId\n     * @param {string} fieldName\n     */\n    onAddCustomGroupBy(type, groupId, fieldName) {\n        this.model.addGroupBy({ groupId, fieldName, custom: true, type });\n    }\n\n    /**\n     * Handle the selection of a groupby dropdown item.\n     *\n     * @param {\"col\"|\"row\"} type\n     * @param {Object} payload\n     */\n    onGroupBySelected(type, payload) {\n        this.model.addGroupBy({ ...payload, type });\n    }\n    /**\n     * Handle a click on a header cell.\n     *\n     * @param {Object} cell\n     * @param {string} type col or row\n     */\n    onHeaderClick(cell, type) {\n        if (cell.isLeaf && cell.isFolded) {\n            this.model.expandGroup(cell.groupId, type);\n        } else if (!cell.isLeaf) {\n            this.model.closeGroup(cell.groupId, type);\n        }\n    }\n    /**\n     * Handle a click on a measure cell.\n     *\n     * @param {Object} cell\n     */\n    onMeasureClick(cell) {\n        this.model.sortRows({\n            groupId: cell.groupId,\n            measure: cell.measure,\n            order: (cell.order || \"desc\") === \"asc\" ? \"desc\" : \"asc\",\n            originIndexes: cell.originIndexes,\n        });\n    }\n    /**\n     * Hover the column in which the mouse is.\n     *\n     * @param {MouseEvent} ev\n     */\n    onMouseEnter(ev) {\n        var index = [...ev.currentTarget.parentNode.children].indexOf(ev.currentTarget);\n        if (ev.currentTarget.tagName === \"TH\") {\n            if (\n                !ev.currentTarget.classList.contains(\"o_pivot_origin_row\") &&\n                this.model.metaData.origins.length === 2\n            ) {\n                index = 3 * index; // two origins + comparison column\n            }\n            index += 1; // row groupbys column\n        }\n        this.tableRef.el\n            .querySelectorAll(\"td:nth-child(\" + (index + 1) + \")\")\n            .forEach((elt) => elt.classList.add(\"o_cell_hover\"));\n    }\n    /**\n     * Remove the hover on the columns.\n     */\n    onMouseLeave() {\n        this.tableRef.el\n            .querySelectorAll(\".o_cell_hover\")\n            .forEach((elt) => elt.classList.remove(\"o_cell_hover\"));\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Exports the current pivot table data in a xls file. For this, we have to\n     * serialize the current state, then call the server /web/pivot/export_xlsx.\n     * Force a reload before exporting to ensure to export up-to-date data.\n     */\n    onDownloadButtonClicked() {\n        if (this.model.getTableWidth() > 16384) {\n            throw new Error(\n                _t(\n                    \"For Excel compatibility, data cannot be exported if there are more than 16384 columns.\\n\\nTip: try to flip axis, filter further or reduce the number of measures.\"\n                )\n            );\n        }\n        const table = this.model.exportData();\n        download({\n            url: \"/web/pivot/export_xlsx\",\n            data: { data: JSON.stringify(table) },\n        });\n    }\n    /**\n     * Expands all groups\n     */\n    onExpandButtonClicked() {\n        this.model.expandAll();\n    }\n    /**\n     * Flips axis\n     */\n    onFlipButtonClicked() {\n        this.model.flip();\n    }\n    /**\n     * Toggles the given measure\n     *\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.toggleMeasure(measure);\n    }\n    /**\n     * Execute the action to open the view on the current model.\n     *\n     * @param {Array} domain\n     * @param {Array} views\n     * @param {Object} context\n     */\n    openView(domain, views, context) {\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: this.model.metaData.title,\n            res_model: this.model.metaData.resModel,\n            views: views,\n            view_mode: \"list\",\n            target: \"current\",\n            context,\n            domain,\n        });\n    }\n    /**\n     * @param {CustomEvent} ev\n     */\n    onOpenView(cell) {\n        if (cell.value === undefined || this.model.metaData.disableLinking) {\n            return;\n        }\n\n        const context = Object.assign({}, this.model.searchParams.context);\n        Object.keys(context).forEach((x) => {\n            if (x === \"group_by\" || x.startsWith(\"search_default_\")) {\n                delete context[x];\n            }\n        });\n\n        // retrieve form and list view ids from the action\n        const { views = [] } = this.env.config;\n        this.views = [\"list\", \"form\"].map((viewType) => {\n            const view = views.find((view) => view[1] === viewType);\n            return [view ? view[0] : false, viewType];\n        });\n\n        const group = {\n            rowValues: cell.groupId[0],\n            colValues: cell.groupId[1],\n            originIndex: cell.originIndexes[0],\n        };\n        this.openView(this.model.getGroupDomain(group), this.views, context);\n    }\n}\n", "import { SearchModel } from \"@web/search/search_model\";\n\nexport class PivotSearchModel extends SearchModel {\n    _getIrFilterDescription() {\n        this.preparingIrFilterDescription = true;\n        const result = super._getIrFilterDescription(...arguments);\n        this.preparingIrFilterDescription = false;\n        return result;\n    }\n\n    _getSearchItemGroupBys(activeItem) {\n        const { searchItemId } = activeItem;\n        const { context, type } = this.searchItems[searchItemId];\n        if (\n            !this.preparingIrFilterDescription &&\n            type === \"favorite\" &&\n            context.pivot_row_groupby\n        ) {\n            return context.pivot_row_groupby;\n        }\n        return super._getSearchItemGroupBys(...arguments);\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { PivotArchParser } from \"@web/views/pivot/pivot_arch_parser\";\nimport { PivotController } from \"./pivot_controller\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\nimport { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\nimport { PivotSearchModel } from \"./pivot_search_model\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const pivotView = {\n    type: \"pivot\",\n    Controller: PivotController,\n    Renderer: PivotRenderer,\n    Model: PivotModel,\n    ArchParser: PivotArchParser,\n    SearchModel: PivotSearchModel,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"comparison\", \"favorite\"],\n    buttonTemplate: \"web.PivotView.Buttons\",\n\n    props: (genericProps, view) => {\n        const modelParams = {};\n        if (genericProps.state) {\n            modelParams.data = genericProps.state.data;\n            modelParams.metaData = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n\n            // parse arch\n            const archInfo = new view.ArchParser().parse(arch);\n\n            if (!archInfo.activeMeasures.length || archInfo.displayQuantity) {\n                archInfo.activeMeasures.unshift(\"__count\");\n            }\n\n            modelParams.metaData = {\n                activeMeasures: archInfo.activeMeasures,\n                colGroupBys: archInfo.colGroupBys,\n                defaultOrder: archInfo.defaultOrder,\n                disableLinking: Boolean(archInfo.disableLinking),\n                fields: fields,\n                fieldAttrs: archInfo.fieldAttrs,\n                resModel: resModel,\n                rowGroupBys: archInfo.rowGroupBys,\n                title: archInfo.title || _t(\"Untitled\"),\n                widgets: archInfo.widgets,\n            };\n        }\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            modelParams,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nviewRegistry.add(\"pivot\", pivotView);\n", "import { visitXML } from \"@web/core/utils/xml\";\nimport { Field } from \"@web/views/fields/field\";\n\nexport class ActivityArchParser {\n    parse(xmlDoc, models, modelName) {\n        const jsClass = xmlDoc.getAttribute(\"js_class\");\n        const title = xmlDoc.getAttribute(\"string\");\n\n        const fieldNodes = {};\n        const templateDocs = {};\n        const fieldNextIds = {};\n\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n\n            if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(\n                    node,\n                    models,\n                    modelName,\n                    \"activity\",\n                    jsClass\n                );\n                if (!(fieldInfo.name in fieldNextIds)) {\n                    fieldNextIds[fieldInfo.name] = 0;\n                }\n                const fieldId = `${fieldInfo.name}_${fieldNextIds[fieldInfo.name]++}`;\n                fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n            }\n\n            // Keep track of last update so images can be reloaded when they may have changed.\n            if (node.tagName === \"img\") {\n                const attSrc = node.getAttribute(\"t-att-src\");\n                if (\n                    attSrc &&\n                    /\\bactivity_image\\b/.test(attSrc) &&\n                    !Object.values(fieldNodes).some((f) => f.name === \"write_date\")\n                ) {\n                    fieldNodes.write_date_0 = { name: \"write_date\", type: \"datetime\" };\n                }\n            }\n        });\n        return {\n            fieldNodes,\n            templateDocs,\n            title,\n        };\n    }\n}\n", "import { ActivityListPopover } from \"@mail/core/web/activity_list_popover\";\nimport { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\nimport { formatDate } from \"@web/core/l10n/dates\";\n\nexport class ActivityCell extends Component {\n    static components = {\n        Avatar,\n    };\n    static props = {\n        activityIds: {\n            type: Array,\n            elements: Number,\n        },\n        attachmentsInfo: {\n            optional: true,\n            type: Object,\n        },\n        activityTypeId: Number,\n        reportingDate: String,\n        countByState: Object,\n        reloadFunc: Function,\n        resId: Number,\n        resModel: String,\n        userAssignedIds: Array,\n    };\n    static template = \"mail.ActivityCell\";\n\n    setup() {\n        this.popover = usePopover(ActivityListPopover, { position: \"bottom-start\" });\n        this.contentRef = useRef(\"content\");\n    }\n\n    get reportingDateFormatted() {\n        return formatDate(luxon.DateTime.fromISO(this.props.reportingDate));\n    }\n\n    get ongoingActivityCount() {\n        return (\n            (this.props.countByState?.planned ?? 0) +\n            (this.props.countByState?.today ?? 0) +\n            (this.props.countByState?.overdue ?? 0)\n        );\n    }\n\n    get totalActivityCount() {\n        return this.ongoingActivityCount + (this.props.countByState?.done ?? 0);\n    }\n\n    onClick() {\n        if (this.popover.isOpen) {\n            this.popover.close();\n        } else {\n            this.popover.open(this.contentRef.el, {\n                activityIds: this.props.activityIds,\n                defaultActivityTypeId: this.props.activityTypeId,\n                onActivityChanged: () => {\n                    this.props.reloadFunc();\n                    this.popover.close();\n                },\n                resId: this.props.resId,\n                resModel: this.props.resModel,\n            });\n        }\n    }\n}\n", "import { createElement, extractAttributes } from \"@web/core/utils/xml\";\nimport { toInterpolatedStringExpression, ViewCompiler } from \"@web/views/view_compiler\";\nimport { toStringExpression } from \"@web/views/utils\";\n\nexport class ActivityCompiler extends ViewCompiler {\n    /**\n     * @override\n     */\n    compileField(el, params) {\n        let compiled;\n        if (el.hasAttribute(\"widget\")) {\n            compiled = super.compileField(el, params);\n        } else {\n            // fields without a specified widget are rendered as simple spans in activity records\n            compiled = createElement(\"div\", {\n                \"t-out\": `record[\"${el.getAttribute(\"name\")}\"].value`,\n            });\n        }\n        const classNames = [];\n        const { bold, display, muted } = extractAttributes(el, [\"bold\", \"display\", \"muted\"]);\n        if (display === \"right\") {\n            classNames.push(\"float-end\");\n        }\n        if (display === \"full\") {\n            classNames.push(\"d-block\", \"text-truncate\");\n        } else {\n            classNames.push(\"d-inline-block\");\n        }\n        if (bold) {\n            classNames.push(\"fw-bold\");\n        }\n        if (muted) {\n            classNames.push(\"text-muted\");\n        }\n        if (classNames.length > 0) {\n            const clsFormatted = el.hasAttribute(\"widget\")\n                ? toStringExpression(classNames.join(\" \"))\n                : classNames.join(\" \");\n            compiled.setAttribute(\"class\", clsFormatted);\n        }\n\n        const attrs = {};\n        for (const attr of el.attributes) {\n            attrs[attr.name] = attr.value;\n        }\n\n        if (el.hasAttribute(\"widget\")) {\n            const attrsParts = Object.entries(attrs).map(([key, value]) => {\n                if (key.startsWith(\"t-attf-\")) {\n                    key = key.slice(7);\n                    value = toInterpolatedStringExpression(value);\n                } else if (key.startsWith(\"t-att-\")) {\n                    key = key.slice(6);\n                    value = `\"\" + (${value})`;\n                } else if (key.startsWith(\"t-att\")) {\n                    throw new Error(\"t-att on <field> nodes is not supported\");\n                } else if (!key.startsWith(\"t-\")) {\n                    value = toStringExpression(value);\n                }\n                return `'${key}':${value}`;\n            });\n            compiled.setAttribute(\"attrs\", `{${attrsParts.join(\",\")}}`);\n        }\n\n        for (const attr in attrs) {\n            if (attr.startsWith(\"t-\") && !attr.startsWith(\"t-att\")) {\n                compiled.setAttribute(attr, attrs[attr]);\n            }\n        }\n\n        return compiled;\n    }\n}\n\nActivityCompiler.OWL_DIRECTIVE_WHITELIST = [\n    ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,\n    \"t-name\",\n    \"t-esc\",\n    \"t-out\",\n    \"t-set\",\n    \"t-value\",\n    \"t-if\",\n    \"t-else\",\n    \"t-elif\",\n    \"t-foreach\",\n    \"t-as\",\n    \"t-key\",\n    \"t-att.*\",\n    \"t-call\",\n    \"t-translation\",\n];\n", "import { _t } from \"@web/core/l10n/translation\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useModel } from \"@web/model/model\";\nimport { extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { Layout } from \"@web/search/layout\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\n\nexport class ActivityController extends Component {\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        archInfo: Object,\n    };\n    static template = \"mail.ActivityController\";\n\n    setup() {\n        this.model = useState(useModel(this.props.Model, this.modelParams));\n\n        this.dialog = useService(\"dialog\");\n        this.action = useService(\"action\");\n        this.store = useService(\"mail.store\");\n        this.ui = useState(useService(\"ui\"));\n        usePager(() => {\n            const { count, hasLimitedCount, limit, offset } = this.model.root;\n            return {\n                offset: offset,\n                limit: limit,\n                total: count,\n                onUpdate: async (params) => {\n                    // Ensure that only (active) records with at least one activity, \"done\" (archived) or not, are fetched.\n                    // We don't use active_test=false in the context because otherwise we would also get archived records.\n                    params.domain = [...(this.model.originalDomain || []), [\"activity_ids.active\", \"in\", [true, false]]];\n                    await Promise.all([\n                        this.model.root.load(params),\n                        this.model.fetchActivityData(params),\n                    ]);\n                },\n                updateTotal: hasLimitedCount ? () => this.model.root.fetchCount() : undefined,\n            };\n        });\n    }\n\n    get modelParams() {\n        const { archInfo, resModel } = this.props;\n        const { activeFields, fields } = extractFieldsFromArchInfo(archInfo, this.props.fields);\n        return {\n            config: {\n                activeFields,\n                resModel,\n                fields,\n            },\n        };\n    }\n\n    getSearchProps() {\n        const { comparision, context, domain, groupBy, orderBy } = this.env.searchModel;\n        return { comparision, context, domain, groupBy, orderBy };\n    }\n\n    scheduleActivity() {\n        this.dialog.add(SelectCreateDialog, {\n            resModel: this.props.resModel,\n            searchViewId: this.env.searchModel.searchViewId,\n            domain: this.model.originalDomain,\n            title: _t(\"Search: %s\", this.props.archInfo.title),\n            multiSelect: false,\n            context: this.props.context,\n            noCreate: this.props.context?.create === false,\n            onSelected: async (resIds) => {\n                await this.store.scheduleActivity(this.props.resModel, resIds);\n            },\n        }, {\n            onClose: () => this.model.load(this.getSearchProps())\n        });\n    }\n\n    openActivityFormView(resId, activityTypeId) {\n        this.action.doAction(\n            {\n                type: \"ir.actions.act_window\",\n                res_model: \"mail.activity\",\n                views: [[false, \"form\"]],\n                view_mode: \"form\",\n                view_type: \"form\",\n                res_id: false,\n                target: \"new\",\n                context: {\n                    default_res_id: resId,\n                    default_res_model: this.props.resModel,\n                    default_activity_type_id: activityTypeId,\n                },\n            },\n            {\n                onClose: () => this.model.load(this.getSearchProps()),\n            }\n        );\n    }\n\n    sendMailTemplate(templateID, activityTypeID) {\n        const groupedActivities = this.model.activityData.grouped_activities;\n        const resIds = [];\n        for (const resId in groupedActivities) {\n            const activityByType = groupedActivities[resId];\n            const activity = activityByType[activityTypeID];\n            if (activity) {\n                resIds.push(parseInt(resId));\n            }\n        }\n        this.model.orm.call(this.props.resModel, \"activity_send_mail\", [resIds, templateID], {});\n    }\n\n    async openRecord(record, mode) {\n        const activeIds = this.model.root.records.map((datapoint) => datapoint.resId);\n        this.props.selectRecord(record.resId, { activeIds, mode });\n    }\n\n    get rendererProps() {\n        return {\n            activityTypes: this.model.activityData.activity_types,\n            activityResIds: this.model.activityData.activity_res_ids,\n            fields: this.model.root.fields,\n            records: this.model.root.records,\n            resModel: this.props.resModel,\n            archInfo: this.props.archInfo,\n            groupedActivities: this.model.activityData.grouped_activities,\n            scheduleActivity: this.scheduleActivity.bind(this),\n            onReloadData: () => this.model.load(this.getSearchProps()),\n            onEmptyCell: this.openActivityFormView.bind(this),\n            onSendMailTemplate: this.sendMailTemplate.bind(this),\n            openRecord: this.openRecord.bind(this),\n        };\n    }\n}\n", "import { RelationalModel } from \"@web/model/relational_model/relational_model\";\n\nexport class ActivityModel extends RelationalModel {\n    static DEFAULT_LIMIT = 100;\n\n    async load(params = {}) {\n        this.originalDomain = params.domain ? [...params.domain] : [];\n        // Ensure that only (active) records with at least one activity, \"done\" (archived) or not, are fetched.\n        // We don't use active_test=false in the context because otherwise we would also get archived records.\n        params.domain = [...(params.domain || []), [\"activity_ids.active\", \"in\", [true, false]]];\n        if (params && \"groupBy\" in params) {\n            params.groupBy = [];\n        }\n        await Promise.all([this.fetchActivityData(params), super.load(params)]);\n    }\n\n    async fetchActivityData(params) {\n        this.activityData = await this.orm.call(\"mail.activity\", \"get_activity_data\", [], {\n            res_model: this.config.resModel,\n            domain: params.domain || this.env.searchModel._domain,\n            limit: params.limit || this.initialLimit,\n            offset: params.offset || 0,\n            fetch_done: true,\n        });\n    }\n}\n", "import { ActivityCompiler } from \"@mail/views/web/activity/activity_compiler\";\n\nimport { Component } from \"@odoo/owl\";\n\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { Field } from \"@web/views/fields/field\";\nimport {\n    getFormattedRecord,\n    getImageSrcFromRecordInfo,\n    isHtmlEmpty,\n} from \"@web/views/kanban/kanban_record\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\n\nexport class ActivityRecord extends Component {\n    static components = {\n        Field,\n    };\n    static props = {\n        archInfo: { type: Object },\n        openRecord: { type: Function },\n        record: { type: Object },\n    };\n    static template = \"mail.ActivityRecord\";\n\n    setup() {\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n        this.widget = {\n            deletable: false,\n            editable: false,\n            isHtmlEmpty,\n        };\n        const { templateDocs } = this.props.archInfo;\n        const templates = useViewCompiler(ActivityCompiler, templateDocs);\n        this.recordTemplate = templates[\"activity-box\"];\n    }\n\n    getRenderingContext() {\n        const { record } = this.props;\n        return {\n            record: getFormattedRecord(record),\n            activity_image: (...args) => getImageSrcFromRecordInfo(record, ...args),\n            user_context: user.context,\n            widget: this.widget,\n            luxon,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n        };\n    }\n}\n", "import { MailColumnProgress } from \"@mail/core/web/mail_column_progress\";\nimport { ActivityCell } from \"@mail/views/web/activity/activity_cell\";\nimport { ActivityRecord } from \"@mail/views/web/activity/activity_record\";\n\nimport { Component, useState } from \"@odoo/owl\";\n\nimport { browser } from \"@web/core/browser/browser\";\nimport { CheckBox } from \"@web/core/checkbox/checkbox\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nexport class ActivityRenderer extends Component {\n    static components = {\n        ActivityCell,\n        ActivityRecord,\n        ColumnProgress: MailColumnProgress,\n        Dropdown,\n        DropdownItem,\n        CheckBox,\n    };\n    static props = {\n        activityTypes: { type: Object },\n        activityResIds: { type: Array },\n        fields: { type: Object },\n        resModel: { type: String },\n        records: { type: Array },\n        archInfo: { type: Object },\n        groupedActivities: { type: Object },\n        scheduleActivity: { type: Function },\n        onReloadData: { type: Function },\n        onEmptyCell: { type: Function },\n        onSendMailTemplate: { type: Function },\n        openRecord: { type: Function },\n    };\n    static template = \"mail.ActivityRenderer\";\n\n    setup() {\n        this.activeFilter = useState({\n            progressValue: {\n                active: null,\n            },\n            activityTypeId: null,\n            resIds: new Set(Object.keys(this.props.groupedActivities)),\n        });\n\n        this.storageKey = [\"activity_columns\", this.props.resModel, this.env.config.viewId];\n        this.setupStorageActiveColumns();\n    }\n\n    getGroupInfo(activityType) {\n        const types = {\n            done: {\n                color: \"secondary\",\n                inProgressBar: false,\n                label: _t(\"done\"), // activity_mixin.activity_state has no done state, so we add it manually here\n                value: 0,\n            },\n            planned: {\n                color: \"success\",\n                inProgressBar: true,\n                value: 0,\n            },\n            today: {\n                color: \"warning\",\n                inProgressBar: true,\n                value: 0,\n            },\n            overdue: {\n                color: \"danger\",\n                inProgressBar: true,\n                value: 0,\n            },\n        };\n        for (const [type, label] of this.props.fields.activity_state.selection) {\n            types[type].label = label;\n        }\n        const typeId = activityType.id;\n        const isColumnFiltered = this.activeFilter.activityTypeId === activityType.id;\n        const progressValue = isColumnFiltered ? this.activeFilter.progressValue : { active: null };\n\n        let totalCountWithoutDone = 0;\n        for (const activities of Object.values(this.props.groupedActivities)) {\n            if (typeId in activities) {\n                for (const [state, stateCount] of Object.entries(\n                    activities[typeId].count_by_state\n                )) {\n                    types[state].value += stateCount;\n                    if (state !== \"done\") {\n                        totalCountWithoutDone += stateCount;\n                    }\n                }\n            }\n        }\n\n        const progressBar = {\n            bars: [],\n            activeBar: isColumnFiltered ? this.activeFilter.progressValue.active : null,\n        };\n        for (const [value, count] of Object.entries(types)) {\n            if (count.inProgressBar) {\n                progressBar.bars.push({\n                    count: count.value,\n                    value,\n                    string: types[value].label,\n                    color: count.color,\n                });\n            }\n        }\n\n        const ongoingActivityCount = types.overdue.value + types.today.value + types.planned.value;\n        const ongoingAndDoneCount = ongoingActivityCount + types.done.value;\n        const labelAggregate = `${types.overdue.label} + ${types.today.label} + ${types.planned.label}`;\n        const aggregateOn =\n            ongoingAndDoneCount && this.isTypeDisplayDone(typeId)\n                ? {\n                      title: `${types.done.label} + ${labelAggregate}`,\n                      value: ongoingAndDoneCount,\n                  }\n                : undefined;\n        return {\n            aggregate: {\n                title: labelAggregate,\n                value: isColumnFiltered ? types[progressValue.active].value : ongoingActivityCount,\n            },\n            aggregateOn: aggregateOn,\n            data: {\n                count: totalCountWithoutDone,\n                filterProgressValue: (name) => this.onSetProgressBarState(typeId, name),\n                progressBar,\n                progressValue,\n            },\n        };\n    }\n\n    getRecord(resId) {\n        return this.props.records.find((r) => r.resId === resId);\n    }\n\n    isTypeDisplayDone(typeId) {\n        return this.props.activityTypes.find((a) => a.id === typeId).keep_done;\n    }\n\n    onSetProgressBarState(typeId, bar) {\n        const name = bar.value;\n        if (this.activeFilter.progressValue.active === name) {\n            this.activeFilter.progressValue.active = null;\n            this.activeFilter.activityTypeId = null;\n            this.activeFilter.resIds = new Set(Object.keys(this.props.groupedActivities));\n        } else {\n            this.activeFilter.progressValue.active = name;\n            this.activeFilter.activityTypeId = typeId;\n            this.activeFilter.resIds = new Set(\n                Object.entries(this.props.groupedActivities)\n                    .filter(\n                        ([, resIds]) => typeId in resIds && name in resIds[typeId].count_by_state\n                    )\n                    .map(([key]) => parseInt(key))\n            );\n        }\n    }\n\n    get activeColumns() {\n        return this.props.activityTypes.filter(\n            (activityType) => this.storageActiveColumns[activityType.id]\n        );\n    }\n\n    setupStorageActiveColumns() {\n        const storageActiveColumnsList = browser.localStorage.getItem(this.storageKey)?.split(\",\");\n\n        this.storageActiveColumns = useState({});\n        for (const activityType of this.props.activityTypes) {\n            if (storageActiveColumnsList) {\n                this.storageActiveColumns[activityType.id] = storageActiveColumnsList.includes(\n                    activityType.id.toString()\n                );\n            } else {\n                this.storageActiveColumns[activityType.id] = true;\n            }\n        }\n    }\n\n    toggleDisplayColumn(typeId) {\n        this.storageActiveColumns[typeId] = !this.storageActiveColumns[typeId];\n        browser.localStorage.setItem(\n            this.storageKey.join(\",\"),\n            Object.keys(this.storageActiveColumns).filter(\n                (activityType) => this.storageActiveColumns[activityType]\n            )\n        );\n    }\n}\n", "import { ActivityArchParser } from \"@mail/views/web/activity/activity_arch_parser\";\nimport { ActivityController } from \"@mail/views/web/activity/activity_controller\";\nimport { ActivityModel } from \"@mail/views/web/activity/activity_model\";\nimport { ActivityRenderer } from \"@mail/views/web/activity/activity_renderer\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport const activityView = {\n    type: \"activity\",\n    searchMenuTypes: [\"filter\", \"favorite\"],\n    Controller: ActivityController,\n    Renderer: ActivityRenderer,\n    ArchParser: ActivityArchParser,\n    Model: ActivityModel,\n    props: (genericProps, view) => {\n        const { arch, relatedModels, resModel } = genericProps;\n        const archInfo = new view.ArchParser().parse(arch, relatedModels, resModel);\n        return {\n            ...genericProps,\n            archInfo,\n            Model: view.Model,\n            Renderer: view.Renderer,\n        };\n    },\n};\nregistry.category(\"views\").add(\"activity\", activityView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\n\nexport const forecastGraphView = {\n    ...graphView,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_graph\", forecastGraphView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { ForecastSearchModel } from \"@crm/views/forecast_search_model\";\n\nexport const forecastPivotView = {\n    ...pivotView,\n    SearchModel: ForecastSearchModel,\n};\n\nregistry.category(\"views\").add(\"forecast_pivot\", forecastPivotView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { graphView } from \"@web/views/graph/graph_view\";\n\nexport class StockForecastedGraphRenderer extends GraphRenderer {\n    static template = \"stock.ForecastedGraphRenderer\";\n};\n\nexport const StockForecastedGraphView = {\n    ...graphView,\n    Renderer: StockForecastedGraphRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_forecasted_graph\", StockForecastedGraphView);\n", "/** @odoo-module */\n\nimport { patch } from \"@web/core/utils/patch\";\nimport { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\npatch(PivotRenderer.prototype, {\n    setup() {\n        super.setup();\n        this.root = useRef(\"root\");\n        if (this.env.isSmall) {\n            useEffect(() => {\n                if (this.root.el) {\n                    const tooltipElems = this.root.el.querySelectorAll(\"*[data-tooltip]\");\n                    for (const el of tooltipElems) {\n                        el.removeAttribute(\"data-tooltip\");\n                        el.removeAttribute(\"data-tooltip-position\");\n                    }\n                }\n            });\n        }\n    },\n\n    getPadding(cell) {\n        if (this.env.isSmall) {\n            return 5 + cell.indent * 5;\n        }\n        return super.getPadding(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { unique } from \"@web/core/utils/arrays\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\n\nexport class MapArchParser {\n    parse(arch) {\n        const archInfo = {\n            fieldNames: [],\n            fieldNamesMarkerPopup: [],\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"map\":\n                    this.visitMap(node, archInfo);\n                    break;\n                case \"field\":\n                    this.visitField(node, archInfo);\n                    break;\n            }\n        });\n\n        archInfo.fieldNames = unique(archInfo.fieldNames);\n        archInfo.fieldNamesMarkerPopup = unique(archInfo.fieldNamesMarkerPopup);\n\n        return archInfo;\n    }\n\n    visitMap(node, archInfo) {\n        archInfo.resPartnerField = node.getAttribute(\"res_partner\");\n        archInfo.fieldNames.push(archInfo.resPartnerField);\n\n        if (node.hasAttribute(\"limit\")) {\n            archInfo.limit = parseInt(node.getAttribute(\"limit\"), 10);\n        }\n        if (node.hasAttribute(\"panel_title\")) {\n            archInfo.panelTitle = node.getAttribute(\"panel_title\");\n        }\n        if (node.hasAttribute(\"routing\")) {\n            archInfo.routing = exprToBoolean(node.getAttribute(\"routing\"));\n        }\n        if (node.hasAttribute(\"hide_title\")) {\n            archInfo.hideTitle = exprToBoolean(node.getAttribute(\"hide_title\"));\n        }\n        if (node.hasAttribute(\"hide_address\")) {\n            archInfo.hideAddress = exprToBoolean(node.getAttribute(\"hide_address\"));\n        }\n        if (node.hasAttribute(\"hide_name\")) {\n            archInfo.hideName = exprToBoolean(node.getAttribute(\"hide_name\"));\n        }\n        if (!archInfo.hideName) {\n            archInfo.fieldNames.push(\"display_name\");\n        }\n        if (node.hasAttribute(\"default_order\")) {\n            archInfo.defaultOrder = {\n                name: node.getAttribute(\"default_order\"),\n                asc: true,\n            };\n        }\n        if (node.hasAttribute(\"allow_resequence\")) {\n            archInfo.allowResequence = exprToBoolean(node.getAttribute(\"allow_resequence\"));\n        }\n    }\n    visitField(node, params) {\n        params.fieldNames.push(node.getAttribute(\"name\"));\n        params.fieldNamesMarkerPopup.push({\n            fieldName: node.getAttribute(\"name\"),\n            string: node.getAttribute(\"string\"),\n        });\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { loadJS, loadCSS } from \"@web/core/assets\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { Layout } from \"@web/search/layout\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, onWillUnmount, onWillStart } from \"@odoo/owl\";\n\nexport class MapController extends Component {\n    static template = \"web_map.MapView\";\n    static components = {\n        Layout,\n        SearchBar,\n        CogMenu,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.action = useService(\"action\");\n\n        /** @type {typeof MapModel} */\n        const Model = this.props.Model;\n        const model = useModelWithSampleData(Model, this.props.modelParams);\n        this.model = model;\n\n        onWillUnmount(() => {\n            this.model.stopFetchingCoordinates();\n        });\n\n        useSetupAction({\n            getLocalState: () => {\n                return this.model.metaData;\n            },\n        });\n\n        onWillStart(() =>\n            Promise.all([\n                loadJS(\"/web_map/static/lib/leaflet/leaflet.js\"),\n                loadCSS(\"/web_map/static/lib/leaflet/leaflet.css\"),\n            ])\n        );\n\n        usePager(() => {\n            return {\n                offset: this.model.metaData.offset,\n                limit: this.model.metaData.limit,\n                total: this.model.data.count,\n                onUpdate: ({ offset, limit }) => this.model.load({ offset, limit }),\n            };\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    /**\n     * @returns {any}\n     */\n    get rendererProps() {\n        return {\n            model: this.model,\n            onMarkerClick: this.openRecords.bind(this),\n        };\n    }\n\n    /**\n     * Redirects to views when clicked on open button in marker popup.\n     *\n     * @param {number[]} ids\n     */\n    openRecords(ids) {\n        if (ids.length > 1) {\n            this.action.doAction({\n                type: \"ir.actions.act_window\",\n                name: this.env.config.getDisplayName() || _t(\"Untitled\"),\n                views: [\n                    [false, \"list\"],\n                    [false, \"form\"],\n                ],\n                res_model: this.props.resModel,\n                domain: [[\"id\", \"in\", ids]],\n            });\n        } else {\n            this.action.switchView(\"form\", { resId: ids[0] });\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Model } from \"@web/model/model\";\nimport { session } from \"@web/session\";\nimport { resequence } from \"@web/model/relational_model/utils\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { formatDateTime, parseDate, parseDateTime } from \"@web/core/l10n/dates\";\nimport { KeepLast } from \"@web/core/utils/concurrency\";\n\nconst DATE_GROUP_FORMATS = {\n    year: \"yyyy\",\n    quarter: \"'Q'q yyyy\",\n    month: \"MMMM yyyy\",\n    week: \"'W'WW yyyy\",\n    day: \"dd MMM yyyy\",\n};\n\nexport class MapModel extends Model {\n    setup(params, { notification, http }) {\n        this.notification = notification;\n        this.http = http;\n\n        this.metaData = {\n            ...params,\n            mapBoxToken: session.map_box_token || \"\",\n        };\n\n        this.data = {\n            count: 0,\n            fetchingCoordinates: false,\n            groupByKey: false,\n            isGrouped: false,\n            numberOfLocatedRecords: 0,\n            partners: {},\n            partnerToCache: [],\n            recordGroups: [],\n            records: [],\n            routes: [],\n            routingError: null,\n            shouldUpdatePosition: true,\n            useMapBoxAPI: !!this.metaData.mapBoxToken,\n        };\n\n        this.coordinateFetchingTimeoutHandle = undefined;\n        this.shouldFetchCoordinates = false;\n        this.keepLast = new KeepLast();\n    }\n    /**\n     * @param {any} params\n     * @returns {Promise<void>}\n     */\n    async load(params) {\n        if (this.coordinateFetchingTimeoutHandle !== undefined) {\n            this.stopFetchingCoordinates();\n        }\n        const metaData = {\n            ...this.metaData,\n            ...params,\n        };\n\n        // remove the properties fields from the group by\n        metaData.groupBy = (metaData.groupBy || []).filter((groupBy) => {\n            // properties fields are in the form `[propert_field_name].[property_entry_key]`\n            const [fieldName] = groupBy.split(\".\");\n            const field = metaData.fields[fieldName];\n            return field?.type !== \"properties\";\n        });\n\n        this.data = await this._fetchData(metaData);\n        this.metaData = metaData;\n\n        this.notify();\n    }\n    /**\n     * Tells the model to stop fetching coordinates.\n     * In OSM mode, the model starts to fetch coordinates once every second after the\n     * model has loaded.\n     * This fetching has to be done every second if we don't want to be banned from OSM.\n     * There are typically two cases when we need to stop fetching:\n     * - when component is about to be unmounted because the request is bound to\n     *   the component and it will crash if we do so.\n     * - when calling the `load` method as it will start fetching new coordinates.\n     */\n    stopFetchingCoordinates() {\n        browser.clearTimeout(this.coordinateFetchingTimeoutHandle);\n        this.coordinateFetchingTimeoutHandle = undefined;\n        this.shouldFetchCoordinates = false;\n    }\n\n    get canResequence() {\n        return (\n            this.metaData.defaultOrder &&\n            !this.metaData.fields[this.metaData.defaultOrder.name].readonly &&\n            this.metaData.fields[this.metaData.defaultOrder.name].type === \"integer\" &&\n            this.metaData.allowResequence &&\n            !this.metaData.groupBy?.length\n        );\n    }\n\n    /**\n     * Resequence the records in `this.data.records` such that the record with the id\n     * `movedRecordId` is moved after the record with the id `targetRecordId`\n     * @param {Number} movedRecordId\n     * @param {Number} targetRecordId\n     */\n    async resequence(movedId, targetId) {\n        const fieldName = this.metaData.defaultOrder.name;\n        const asc = this.metaData.defaultOrder.asc;\n        const resequenceProm = resequence({\n            records: this.data.records,\n            resModel: this.metaData.resModel,\n            movedId,\n            targetId,\n            fieldName,\n            asc,\n            context: this.metaData.context,\n            orm: this.orm,\n        });\n        // the resequence method modifies this.data.records before the resequence backend call\n        // we need to notify after the synchronous record change\n        this.notify();\n        const resequencedRecords = await resequenceProm;\n        if (resequencedRecords) {\n            for (const resequencedRecord of resequencedRecords) {\n                const record = this.data.records.find((r) => r.id === resequencedRecord.id);\n                record[fieldName] = resequencedRecord[fieldName];\n            }\n            this.notify();\n            await this._updatePartnerCoordinate(this.metaData, this.data);\n        }\n    }\n\n    //----------------------------------------------------------------------\n    // Protected\n    //----------------------------------------------------------------------\n\n    /**\n     * Adds the corresponding partner to a record.\n     *\n     * @protected\n     */\n    _addPartnerToRecord(metaData, data) {\n        for (const record of data.records) {\n            if (metaData.resModel === \"res.partner\" && metaData.resPartnerField === \"id\") {\n                record.partner = data.partners[record.id];\n            } else {\n                record.partner = data.partners[record[metaData.resPartnerField].id];\n            }\n            data.numberOfLocatedRecords++;\n        }\n    }\n\n    /**\n     * The partner's coordinates should be between -90 <= latitude <= 90 and -180 <= longitude <= 180.\n     *\n     * @protected\n     * @param {Object} partner\n     * @param {number} partner.partner_latitude latitude of the partner\n     * @param {number} partner.partner_longitude longitude of the partner\n     * @returns {boolean}\n     */\n    _checkCoordinatesValidity(partner) {\n        if (\n            partner.partner_latitude &&\n            partner.partner_longitude &&\n            partner.partner_latitude >= -90 &&\n            partner.partner_latitude <= 90 &&\n            partner.partner_longitude >= -180 &&\n            partner.partner_longitude <= 180\n        ) {\n            return true;\n        }\n        return false;\n    }\n\n    /**\n     * Handles the case of an empty map.\n     * Handles the case where the model is res_partner.\n     * Fetches the records according to the model given in the arch.\n     * If the records has no partner_id field it is sliced from the array.\n     *\n     * @protected\n     * @params {any} metaData\n     * @return {Promise<any>}\n     */\n    async _fetchData(metaData) {\n        const data = {\n            count: 0,\n            fetchingCoordinates: false,\n            groupByKey: metaData.groupBy.length ? metaData.groupBy[0] : false,\n            isGrouped: metaData.groupBy.length > 0,\n            numberOfLocatedRecords: 0,\n            partners: {},\n            partnerToCache: [],\n            recordGroups: [],\n            records: [],\n            routes: [],\n            routingError: null,\n            shouldUpdatePosition: true,\n            useMapBoxAPI: !!metaData.mapBoxToken,\n        };\n\n        //case of empty map\n        if (!metaData.resPartnerField) {\n            data.recordGroups = [];\n            data.records = [];\n            data.routes = [];\n            return this.keepLast.add(Promise.resolve(data));\n        }\n        const results = await this.keepLast.add(this._fetchRecordData(metaData, data));\n\n        const datetimeFields = metaData.fieldNames.filter(\n            (name) => metaData.fields[name].type == \"datetime\"\n        );\n        for (const record of results.records) {\n            // convert date fields from UTC to local timezone\n            for (const field of datetimeFields) {\n                if (record[field]) {\n                    const dateUTC = luxon.DateTime.fromFormat(\n                        record[field],\n                        \"yyyy-MM-dd HH:mm:ss\",\n                        { zone: \"UTC\" }\n                    );\n                    record[field] = formatDateTime(dateUTC, { format: \"yyyy-MM-dd HH:mm:ss\" });\n                }\n            }\n        }\n\n        data.records = results.records;\n        data.count = results.length;\n        if (data.isGrouped) {\n            data.recordGroups = await this._getRecordGroups(metaData, data);\n        } else {\n            data.recordGroups = [];\n        }\n\n        if (metaData.resModel === \"res.partner\" && metaData.resPartnerField === \"id\") {\n            for (const record of data.records) {\n                if (!data.partners[record.id]) {\n                    data.partners[record.id] = { ...record };\n                }\n            }\n        } else {\n            for (const record of data.records) {\n                const partner = record[metaData.resPartnerField];\n                if (partner && !data.partners[partner.id]) {\n                    data.partners[partner.id] = partner;\n                }\n            }\n        }\n        this._addPartnerToRecord(metaData, data);\n        await this._updatePartnerCoordinate(metaData, data);\n        return data;\n    }\n\n    _getRecordSpecification(metaData, data) {\n        const fieldNames = data.groupByKey\n            ? metaData.fieldNames.concat(data.groupByKey.split(\":\")[0])\n            : metaData.fieldNames;\n        const specification = {};\n        const fieldsToAdd = {\n            contact_address_complete: {},\n            partner_latitude: {},\n            partner_longitude: {},\n        };\n        for (const fieldName of fieldNames) {\n            specification[fieldName] = {};\n            if (metaData.resPartnerField === \"id\") {\n                Object.assign(specification, fieldsToAdd);\n            } else if (\n                [\"many2one\", \"one2many\", \"many2many\"].includes(metaData.fields[fieldName].type)\n            ) {\n                specification[fieldName].fields = { display_name: {} };\n                if (fieldName === metaData.resPartnerField) {\n                    Object.assign(specification[fieldName].fields, fieldsToAdd);\n                }\n            }\n        }\n        return specification;\n    }\n\n    /**\n     * Fetch the records for a given model.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    _fetchRecordData(metaData, data) {\n        const specification = this._getRecordSpecification(metaData, data);\n        const orderBy = [];\n        if (metaData.defaultOrder) {\n            orderBy.push(metaData.defaultOrder.name);\n            if (metaData.defaultOrder.asc) {\n                orderBy.push(\"ASC\");\n            }\n        }\n        return this.orm.webSearchRead(metaData.resModel, metaData.domain, {\n            specification,\n            limit: metaData.limit,\n            offset: metaData.offset,\n            order: orderBy.join(\" \"),\n            context: metaData.context,\n        });\n    }\n\n    /**\n     * This function convert the addresses to coordinates using the mapbox API.\n     *\n     * @protected\n     * @param {Object} record this object contains the record fetched from the database.\n     * @returns {Promise} result.query contains the query the the api received\n     *      result.features contains results in descendant order of relevance\n     */\n    _fetchCoordinatesFromAddressMB(metaData, data, record) {\n        const address = encodeURIComponent(record.contact_address_complete);\n        const token = metaData.mapBoxToken;\n        const encodedUrl = `https://api.mapbox.com/geocoding/v5/mapbox.places/${address}.json?access_token=${token}&cachebuster=1552314159970&autocomplete=true`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * This function convert the addresses to coordinates using the openStreetMap api.\n     *\n     * @protected\n     * @param {Object} record this object contains the record fetched from the database.\n     * @returns {Promise} result is an array that contains the result in descendant order of relevance\n     *      result[i].lat is the latitude of the converted address\n     *      result[i].lon is the longitude of the converted address\n     *      result[i].importance is a number that the relevance of the result the closer the number is to one the best it is.\n     */\n    _fetchCoordinatesFromAddressOSM(metaData, data, record) {\n        const address = encodeURIComponent(record.contact_address_complete.replace(\"/\", \" \"));\n        const encodedUrl = `https://nominatim.openstreetmap.org/search?q=${address}&format=jsonv2`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * Fetch the route from the mapbox api.\n     *\n     * @protected\n     * @returns {Promise}\n     *      results.geometry.legs[i] contains one leg (i.e: the trip between two markers).\n     *      results.geometry.legs[i].steps contains the sets of coordinates to follow to reach a point from an other.\n     *      results.geometry.legs[i].distance: the distance in meters to reach the destination\n     *      results.geometry.legs[i].duration the duration of the leg\n     *      results.geometry.coordinates contains the sets of coordinates to go from the first to the last marker without the notion of waypoint\n     */\n    _fetchRoute(metaData, data) {\n        const coordinatesParam = data.records\n            .filter((record) => record.partner.partner_latitude && record.partner.partner_longitude)\n            .map(({ partner }) => `${partner.partner_longitude},${partner.partner_latitude}`);\n        const address = encodeURIComponent(coordinatesParam.join(\";\"));\n        const token = metaData.mapBoxToken;\n        const encodedUrl = `https://api.mapbox.com/directions/v5/mapbox/driving/${address}?access_token=${token}&steps=true&geometries=geojson`;\n        return this.http.get(encodedUrl);\n    }\n\n    /**\n     * Converts a MapBox error message into a custom translatable one.\n     *\n     * @protected\n     * @param {string} message\n     */\n    _getErrorMessage(message) {\n        const ERROR_MESSAGES = {\n            \"Too many coordinates; maximum number of coordinates is 25\": _t(\n                \"Too many routing points (maximum 25)\"\n            ),\n            \"Route exceeds maximum distance limitation\": _t(\n                \"Some routing points are too far apart\"\n            ),\n            \"Too Many Requests\": _t(\"Too many requests, try again in a few minutes\"),\n        };\n        return ERROR_MESSAGES[message];\n    }\n\n    _getEmptyGroupLabel(fieldName) {\n        return _t(\"None\");\n    }\n\n    /**\n     * @protected\n     * @returns {Object} the fetched records grouped by the groupBy field.\n     */\n    async _getRecordGroups(metaData, data) {\n        const [fieldName, subGroup] = data.groupByKey.split(\":\");\n        const fieldType = metaData.fields[fieldName].type;\n        const groups = {};\n        function addToGroup(id, name, record) {\n            if (!groups[id]) {\n                groups[id] = {\n                    name,\n                    records: [],\n                };\n            }\n            groups[id].records.push(record);\n        }\n        for (const record of data.records) {\n            const value = record[fieldName];\n            let id, name;\n            if ([\"one2many\", \"many2many\"].includes(fieldType)) {\n                if (value.length) {\n                    for (const r of value) {\n                        addToGroup(r.id, r.display_name, record);\n                    }\n                } else {\n                    id = name = this._getEmptyGroupLabel(fieldName);\n                    addToGroup(id, name, record);\n                }\n            } else {\n                if ([\"date\", \"datetime\"].includes(fieldType) && value) {\n                    const date = fieldType === \"date\" ? parseDate(value) : parseDateTime(value);\n                    id = name = date.toFormat(DATE_GROUP_FORMATS[subGroup]);\n                } else if (fieldType === \"boolean\") {\n                    id = name = value ? _t(\"Yes\") : _t(\"No\");\n                } else if (fieldType === \"selection\") {\n                    const selected = metaData.fields[fieldName].selection.find(\n                        (o) => o[0] === value\n                    );\n                    id = name = selected ? selected[1] : value;\n                } else if (fieldType === \"many2one\" && value) {\n                    id = value.id;\n                    name = value.display_name;\n                } else {\n                    id = value;\n                    name = value;\n                }\n                if (!id && !name) {\n                    id = name = this._getEmptyGroupLabel(fieldName);\n                }\n                addToGroup(id, name, record);\n            }\n        }\n        return groups;\n    }\n\n    /**\n     * Handles the case where the selected api is MapBox.\n     * Iterates on all the partners and fetches their coordinates when they're not set.\n     *\n     * @protected\n     * @return {Promise} if there's more than 2 located records and the routing option is activated it returns a promise that fetches the route\n     *      resultResult is an object that contains the computed route\n     *      or if either of these conditions are not respected it returns an empty promise\n     */\n    _maxBoxAPI(metaData, data) {\n        const promises = [];\n        for (const partner of Object.values(data.partners)) {\n            if (\n                partner.contact_address_complete &&\n                (!partner.partner_latitude || !partner.partner_longitude)\n            ) {\n                promises.push(\n                    this._fetchCoordinatesFromAddressMB(metaData, data, partner).then(\n                        (coordinates) => {\n                            if (coordinates.features.length) {\n                                partner.partner_longitude = parseFloat(\n                                    coordinates.features[0].geometry.coordinates[0]\n                                );\n                                partner.partner_latitude = parseFloat(\n                                    coordinates.features[0].geometry.coordinates[1]\n                                );\n                                data.partnerToCache.push(partner);\n                            }\n                        }\n                    )\n                );\n            } else if (!this._checkCoordinatesValidity(partner)) {\n                partner.partner_latitude = undefined;\n                partner.partner_longitude = undefined;\n            }\n        }\n        return Promise.all(promises).then(() => {\n            data.routes = [];\n            if (data.numberOfLocatedRecords > 1 && metaData.routing && !data.groupByKey) {\n                return this._fetchRoute(metaData, data).then((routeResult) => {\n                    if (routeResult.routes) {\n                        data.routes = routeResult.routes;\n                    } else {\n                        data.routingError = this._getErrorMessage(routeResult.message);\n                    }\n                });\n            } else {\n                return Promise.resolve();\n            }\n        });\n    }\n\n    /**\n     * Handles the displaying of error message according to the error.\n     *\n     * @protected\n     * @param {Object} err contains the error returned by the requests\n     * @param {number} err.status contains the status_code of the failed http request\n     */\n    _mapBoxErrorHandling(metaData, data, err) {\n        switch (err.status) {\n            case 401:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"Token invalid\"),\n                        type: \"danger\",\n                    }\n                );\n                break;\n            case 403:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"Unauthorized connection\"),\n                        type: \"danger\",\n                    }\n                );\n                break;\n            case 422: // Max. addresses reached\n            case 429: // Max. requests reached\n                data.routingError = this._getErrorMessage(err.responseJSON.message);\n                break;\n            case 500:\n                this.notification.add(\n                    _t(\n                        \"The view has switched to another provider but functionalities will be limited\"\n                    ),\n                    {\n                        title: _t(\"MapBox servers unreachable\"),\n                        type: \"danger\",\n                    }\n                );\n        }\n    }\n\n    /**\n     * Notifies the fetched coordinates to server and controller.\n     *\n     * @protected\n     */\n    _notifyFetchedCoordinate(metaData, data) {\n        this._writeCoordinatesUsers(metaData, data);\n        data.shouldUpdatePosition = false;\n        this.notify();\n    }\n\n    /**\n     * Calls (without awaiting) _openStreetMapAPIAsync with a delay of 1000ms\n     * to not get banned from openstreetmap's server.\n     *\n     * Tests should patch this function to wait for coords to be fetched.\n     *\n     * @see _openStreetMapAPIAsync\n     * @protected\n     * @return {Promise}\n     */\n    _openStreetMapAPI(metaData, data) {\n        this._openStreetMapAPIAsync(metaData, data);\n        return Promise.resolve();\n    }\n    /**\n     * Handles the case where the selected api is open street map.\n     * Iterates on all the partners and fetches their coordinates when they're not set.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    _openStreetMapAPIAsync(metaData, data) {\n        // Group partners by address to reduce address list\n        const addressPartnerMap = new Map();\n        for (const partner of Object.values(data.partners)) {\n            if (\n                partner.contact_address_complete &&\n                (!partner.partner_latitude || !partner.partner_longitude)\n            ) {\n                if (!addressPartnerMap.has(partner.contact_address_complete)) {\n                    addressPartnerMap.set(partner.contact_address_complete, []);\n                }\n                addressPartnerMap.get(partner.contact_address_complete).push(partner);\n                partner.fetchingCoordinate = true;\n            } else if (!this._checkCoordinatesValidity(partner)) {\n                partner.partner_latitude = undefined;\n                partner.partner_longitude = undefined;\n            }\n        }\n\n        // `fetchingCoordinates` is used to display the \"fetching banner\"\n        // We need to check if there are coordinates to fetch before reload the\n        // view to prevent flickering\n        data.fetchingCoordinates = addressPartnerMap.size > 0;\n        this.shouldFetchCoordinates = true;\n        const fetch = async () => {\n            const partnersList = Array.from(addressPartnerMap.values());\n            for (let i = 0; i < partnersList.length; i++) {\n                await new Promise((resolve) => {\n                    this.coordinateFetchingTimeoutHandle = browser.setTimeout(\n                        resolve,\n                        this.constructor.COORDINATE_FETCH_DELAY\n                    );\n                });\n                if (!this.shouldFetchCoordinates) {\n                    return;\n                }\n                const partners = partnersList[i];\n                try {\n                    const coordinates = await this._fetchCoordinatesFromAddressOSM(\n                        metaData,\n                        data,\n                        partners[0]\n                    );\n                    if (!this.shouldFetchCoordinates) {\n                        return;\n                    }\n                    if (coordinates.length) {\n                        for (const partner of partners) {\n                            partner.partner_longitude = parseFloat(coordinates[0].lon);\n                            partner.partner_latitude = parseFloat(coordinates[0].lat);\n                            data.partnerToCache.push(partner);\n                        }\n                    }\n                    for (const partner of partners) {\n                        partner.fetchingCoordinate = false;\n                    }\n                    data.fetchingCoordinates = i < partnersList.length - 1;\n                    this._notifyFetchedCoordinate(metaData, data);\n                } catch {\n                    for (const partner of Object.values(data.partners)) {\n                        partner.fetchingCoordinate = false;\n                    }\n                    data.fetchingCoordinates = false;\n                    this.shouldFetchCoordinates = false;\n                    this.notification.add(\n                        _t(\"OpenStreetMap's request limit exceeded, try again later.\"),\n                        { type: \"danger\" }\n                    );\n                    this.notify();\n                }\n            }\n        };\n        return fetch();\n    }\n\n    /**\n     * if the token is set it uses the mapBoxApi to fetch address and route\n     * if not is uses the openstreetmap api to fetch the address.\n     *\n     * @protected\n     * @returns {Promise}\n     */\n    async _updatePartnerCoordinate(metaData, data) {\n        if (data.useMapBoxAPI) {\n            return this.keepLast\n                .add(this._maxBoxAPI(metaData, data))\n                .then(() => {\n                    this._writeCoordinatesUsers(metaData, data);\n                })\n                .catch((err) => {\n                    this._mapBoxErrorHandling(metaData, data, err);\n                    data.useMapBoxAPI = false;\n                    return this._openStreetMapAPI(metaData, data);\n                });\n        } else {\n            return this._openStreetMapAPI(metaData, data).then(() => {\n                this._writeCoordinatesUsers(metaData, data);\n            });\n        }\n    }\n    /**\n     * Writes partner_longitude and partner_latitude of the res.partner model.\n     *\n     * @protected\n     * @return {Promise}\n     */\n    async _writeCoordinatesUsers(metaData, data) {\n        const partners = data.partnerToCache;\n        data.partnerToCache = [];\n        if (partners.length) {\n            await this.orm.call(\"res.partner\", \"update_latitude_longitude\", [partners], {\n                context: metaData.context,\n            });\n        }\n    }\n}\n\nMapModel.services = [\"notification\", \"http\"];\nMapModel.COORDINATE_FETCH_DELAY = 1000;\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\n/*global L*/\n\nimport { renderToString } from \"@web/core/utils/render\";\nimport { delay } from \"@web/core/utils/concurrency\";\n\nimport {\n    Component,\n    onWillUnmount,\n    onWillUpdateProps,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useSortable } from \"@web/core/utils/sortable_owl\";\n\nconst apiTilesRouteWithToken =\n    \"https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}\";\nconst apiTilesRouteWithoutToken = \"https://a.tile.openstreetmap.org/{z}/{x}/{y}.png\";\n\nconst colors = [\n    \"#F06050\",\n    \"#6CC1ED\",\n    \"#F7CD1F\",\n    \"#814968\",\n    \"#30C381\",\n    \"#D6145F\",\n    \"#475577\",\n    \"#F4A460\",\n    \"#EB7E7F\",\n    \"#2C8397\",\n];\n\nconst mapTileAttribution = `\n    \u00a9 <a href=\"https://www.mapbox.com/about/maps/\">Mapbox</a>\n    \u00a9 <a href=\"http://www.openstreetmap.org/copyright\">OpenStreetMap</a>\n    <strong>\n        <a href=\"https://www.mapbox.com/map-feedback/\" target=\"_blank\">\n            Improve this map\n        </a>\n    </strong>`;\n\nexport class MapRenderer extends Component {\n    static template = \"web_map.MapRenderer\";\n    static props = {\n        model: Object,\n        onMarkerClick: Function,\n    };\n\n    setup() {\n        this.leafletMap = null;\n        this.markers = [];\n        this.polylines = [];\n        this.mapContainerRef = useRef(\"mapContainer\");\n        this.state = useState({\n            closedGroupIds: [],\n            expendedPinList: false,\n        });\n        this.nextId = 1;\n\n        useEffect(\n            () => {\n                this.leafletMap = L.map(this.mapContainerRef.el, {\n                    maxBounds: [L.latLng(180, -180), L.latLng(-180, 180)],\n                });\n                this.leafletMap.attributionControl.setPrefix(\n                    '<a href=\"https://leafletjs.com\" title=\"A JavaScript library for interactive maps\">Leaflet</a>'\n                );\n                L.tileLayer(this.apiTilesRoute, {\n                    attribution: mapTileAttribution,\n                    tileSize: 512,\n                    zoomOffset: -1,\n                    minZoom: 2,\n                    maxZoom: 19,\n                    id: \"mapbox/streets-v11\",\n                    accessToken: this.props.model.metaData.mapBoxToken,\n                }).addTo(this.leafletMap);\n            },\n            () => []\n        );\n        useEffect(() => {\n            this.updateMap();\n        });\n\n        this.pinListRef = useRef(\"pinList\");\n        useSortable({\n            enable: () => this.props.model.canResequence,\n            ref: this.pinListRef,\n            elements: \".o-map-renderer--pin-located\",\n            handle: \".o_row_handle\",\n            onDrop: async (params) => {\n                const rowId = parseInt(params.element.dataset.id);\n                const previousRowId = parseInt(params.previous?.dataset?.id) || null;\n                await this.props.model.resequence(rowId, previousRowId);\n            },\n        });\n\n        onWillUpdateProps(this.onWillUpdateProps);\n        onWillUnmount(this.onWillUnmount);\n    }\n    /**\n     * Update group opened/closed state.\n     */\n    async onWillUpdateProps(nextProps) {\n        if (this.props.model.data.groupByKey !== nextProps.model.data.groupByKey) {\n            this.state.closedGroupIds = [];\n        }\n    }\n    /**\n     * Remove map and the listeners on its markers and routes.\n     */\n    onWillUnmount() {\n        this.removeMarkers();\n        this.removeRoutes();\n        if (this.leafletMap) {\n            this.leafletMap.remove();\n        }\n    }\n\n    /**\n     * Return the route to the tiles api with or without access token.\n     *\n     * @returns {string}\n     */\n    get apiTilesRoute() {\n        return this.props.model.data.useMapBoxAPI\n            ? apiTilesRouteWithToken\n            : apiTilesRouteWithoutToken;\n    }\n\n    /**\n     * If there's located records, adds the corresponding marker on the map.\n     * Binds events to the created markers.\n     */\n    addMarkers() {\n        this.removeMarkers();\n\n        const markersInfo = {};\n        let records = this.props.model.data.records;\n        if (this.props.model.data.isGrouped) {\n            records = Object.entries(this.props.model.data.recordGroups)\n                .filter(([key]) => !this.state.closedGroupIds.includes(key))\n                .flatMap(([groupId, value]) => value.records.map((elem) => ({ ...elem, groupId })));\n        }\n\n        const pinInSamePlace = {};\n        for (const record of records) {\n            const partner = record.partner;\n            if (partner && partner.partner_latitude && partner.partner_longitude) {\n                const lat_long = `${partner.partner_latitude}-${partner.partner_longitude}`;\n                const group = this.props.model.data.recordGroups ? `-${record.groupId}` : \"\";\n                const key = `${lat_long}${group}`;\n                if (key in markersInfo) {\n                    markersInfo[key].record = record;\n                    markersInfo[key].ids.push(record.id);\n                } else {\n                    pinInSamePlace[lat_long] = ++pinInSamePlace[lat_long] || 0;\n                    markersInfo[key] = {\n                        record: record,\n                        ids: [record.id],\n                        pinInSamePlace: pinInSamePlace[lat_long],\n                    };\n                }\n            }\n        }\n\n        for (const markerInfo of Object.values(markersInfo)) {\n            const params = {\n                count: markerInfo.ids.length,\n                isMulti: markerInfo.ids.length > 1,\n                number: this.props.model.data.records.indexOf(markerInfo.record) + 1,\n                numbering: this.props.model.metaData.numbering,\n            };\n\n            if (this.props.model.data.isGrouped) {\n                const groupId = markerInfo.record.groupId;\n                params.color = this.getGroupColor(groupId);\n                params.number =\n                    this.props.model.data.recordGroups[groupId].records.findIndex((record) => {\n                        return record.id === markerInfo.record.id;\n                    }) + 1;\n            }\n\n            // Icon creation\n            const iconInfo = {\n                className: \"o-map-renderer--marker\",\n                html: renderToString(\"web_map.marker\", params),\n            };\n\n            const offset = markerInfo.pinInSamePlace * 0.000025;\n            // Attach marker with icon and popup\n            const marker = L.marker(\n                [\n                    markerInfo.record.partner.partner_latitude + offset,\n                    markerInfo.record.partner.partner_longitude - offset,\n                ],\n                { icon: L.divIcon(iconInfo) }\n            );\n            marker.addTo(this.leafletMap);\n            marker.on(\"click\", () => {\n                this.createMarkerPopup(markerInfo, offset);\n            });\n            this.markers.push(marker);\n        }\n    }\n    /**\n     * If there are computed routes, create polylines and add them to the map.\n     * each element of this.props.routeInfo[0].legs array represent the route between\n     * two waypoints thus each of these must be a polyline.\n     */\n    addRoutes() {\n        this.removeRoutes();\n        if (!this.props.model.data.useMapBoxAPI || !this.props.model.data.routes.length) {\n            return;\n        }\n\n        for (const leg of this.props.model.data.routes[0].legs) {\n            const latLngs = [];\n            for (const step of leg.steps) {\n                for (const coordinate of step.geometry.coordinates) {\n                    latLngs.push(L.latLng(coordinate[1], coordinate[0]));\n                }\n            }\n\n            const polyline = L.polyline(latLngs, {\n                color: \"blue\",\n                weight: 5,\n                opacity: 0.3,\n            }).addTo(this.leafletMap);\n\n            const polylines = this.polylines;\n            polyline.on(\"click\", function () {\n                for (const polyline of polylines) {\n                    polyline.setStyle({ color: \"blue\", opacity: 0.3 });\n                }\n                this.setStyle({ color: \"darkblue\", opacity: 1.0 });\n            });\n            this.polylines.push(polyline);\n        }\n    }\n    /**\n     * Create a popup for the specified marker.\n     *\n     * @param {Object} markerInfo\n     * @param {Number} latLongOffset\n     */\n    createMarkerPopup(markerInfo, latLongOffset = 0) {\n        const popupFields = this.getMarkerPopupFields(markerInfo);\n        const partner = markerInfo.record.partner;\n        const encodedAddress = encodeURIComponent(partner.contact_address_complete);\n        const popupHtml = renderToString(\"web_map.markerPopup\", {\n            fields: popupFields,\n            hasFormView: this.props.model.metaData.hasFormView,\n            url: `https://www.google.com/maps/dir/?api=1&destination=${encodedAddress}`,\n        });\n\n        const popup = L.popup({ offset: [0, -30] })\n            .setLatLng([\n                partner.partner_latitude + latLongOffset,\n                partner.partner_longitude - latLongOffset,\n            ])\n            .setContent(popupHtml)\n            .openOn(this.leafletMap);\n\n        const openBtn = popup\n            .getElement()\n            .querySelector(\"button.o-map-renderer--popup-buttons-open\");\n        if (openBtn) {\n            openBtn.onclick = () => {\n                this.props.onMarkerClick(markerInfo.ids);\n            };\n        }\n        return popup;\n    }\n    /**\n     * @param {Number} groupId\n     */\n    getGroupColor(groupId) {\n        const index = Object.keys(this.props.model.data.recordGroups).indexOf(groupId);\n        return colors[index % colors.length];\n    }\n    /**\n     * Creates an array of latLng objects if there is located records.\n     *\n     * @returns {latLngBounds|boolean} objects containing the coordinates that\n     *          allows all the records to be shown on the map or returns false\n     *          if the records does not contain any located record.\n     */\n    getLatLng() {\n        const tabLatLng = [];\n        for (const record of this.props.model.data.records) {\n            const partner = record.partner;\n            if (partner && partner.partner_latitude && partner.partner_longitude) {\n                tabLatLng.push(L.latLng(partner.partner_latitude, partner.partner_longitude));\n            }\n        }\n        if (!tabLatLng.length) {\n            return false;\n        }\n        return L.latLngBounds(tabLatLng);\n    }\n    /**\n     * Get the fields' name and value to display in the popup.\n     *\n     * @param {Object} markerInfo\n     * @returns {Object} value contains the value of the field and string\n     *                   contains the value of the xml's string attribute\n     */\n    getMarkerPopupFields(markerInfo) {\n        const record = markerInfo.record;\n        const fieldsView = [];\n        // Only display address in multi coordinates marker popup\n        if (markerInfo.ids.length > 1) {\n            if (!this.props.model.metaData.hideAddress) {\n                fieldsView.push({\n                    id: this.nextId++,\n                    value: record.partner.contact_address_complete,\n                    string: _t(\"Address\"),\n                });\n            }\n            return fieldsView;\n        }\n        if (!this.props.model.metaData.hideName) {\n            fieldsView.push({\n                id: this.nextId++,\n                value: record.display_name,\n                string: _t(\"Name\"),\n            });\n        }\n        if (!this.props.model.metaData.hideAddress) {\n            fieldsView.push({\n                id: this.nextId++,\n                value: record.partner.contact_address_complete,\n                string: _t(\"Address\"),\n            });\n        }\n        const fields = this.props.model.metaData.fields;\n        for (const field of this.props.model.metaData.fieldNamesMarkerPopup) {\n            if (record[field.fieldName]) {\n                let value = record[field.fieldName];\n                if (fields[field.fieldName].type === \"many2one\") {\n                    value = record[field.fieldName].display_name;\n                } else if ([\"one2many\", \"many2many\"].includes(fields[field.fieldName].type)) {\n                    value = record[field.fieldName]\n                        ? record[field.fieldName].map((r) => r.display_name).join(\", \")\n                        : \"\";\n                }\n                fieldsView.push({\n                    id: this.nextId++,\n                    value,\n                    string: field.string,\n                });\n            }\n        }\n        return fieldsView;\n    }\n    /**\n     * @returns {string}\n     */\n    get googleMapUrl() {\n        let url = \"https://www.google.com/maps/dir/?api=1\";\n        if (this.props.model.data.records.length) {\n            const allCoordinates = this.props.model.data.records.filter(\n                ({ partner }) => partner && partner.partner_latitude && partner.partner_longitude\n            );\n            const uniqueCoordinates = allCoordinates.reduce((coords, { partner }) => {\n                const coord = partner.partner_latitude + \",\" + partner.partner_longitude;\n                if (!coords.includes(coord)) {\n                    coords.push(coord);\n                }\n                return coords;\n            }, []);\n            if (uniqueCoordinates.length && this.props.model.metaData.routing) {\n                // When routing is enabled, make last record the destination\n                url += `&destination=${uniqueCoordinates.pop()}`;\n            }\n            if (uniqueCoordinates.length) {\n                url += `&waypoints=${uniqueCoordinates.join(\"|\")}`;\n            }\n        }\n        return url;\n    }\n    /**\n     * Remove the markers from the map and empty the markers array.\n     */\n    removeMarkers() {\n        for (const marker of this.markers) {\n            marker.off(\"click\");\n            this.leafletMap.removeLayer(marker);\n        }\n        this.markers = [];\n    }\n    /**\n     * Remove the routes from the map and empty the the polyline array.\n     */\n    removeRoutes() {\n        for (const polyline of this.polylines) {\n            polyline.off(\"click\");\n            this.leafletMap.removeLayer(polyline);\n        }\n        this.polylines = [];\n    }\n    /**\n     * Update position in the map, markers and routes.\n     */\n    updateMap() {\n        if (this.props.model.data.shouldUpdatePosition) {\n            const initialCoord = this.getLatLng();\n            if (initialCoord) {\n                this.leafletMap.flyToBounds(initialCoord, { animate: false });\n            } else {\n                this.leafletMap.fitWorld();\n            }\n            this.leafletMap.closePopup();\n        }\n        this.addMarkers();\n        this.addRoutes();\n    }\n\n    /**\n     * Center the map on a certain pin and open the popup linked to it.\n     *\n     * @param {Object} record\n     */\n    async centerAndOpenPin(record) {\n        this.state.expendedPinList = false;\n        // wait the next owl render to avoid marker popup create => destroy\n        await delay(0);\n        const popup = this.createMarkerPopup({\n            record: record,\n            ids: [record.id],\n        });\n        const px = this.leafletMap.project([\n            record.partner.partner_latitude,\n            record.partner.partner_longitude,\n        ]);\n        const popupHeight = popup.getElement().offsetHeight;\n        px.y -= popupHeight / 2;\n        const latlng = this.leafletMap.unproject(px);\n        this.leafletMap.panTo(latlng, { animate: true });\n    }\n    /**\n     * @param {Number} id\n     */\n    toggleGroup(id) {\n        if (this.state.closedGroupIds.includes(id)) {\n            const index = this.state.closedGroupIds.indexOf(id);\n            this.state.closedGroupIds.splice(index, 1);\n        } else {\n            this.state.closedGroupIds.push(id);\n        }\n    }\n\n    togglePinList() {\n        this.state.expendedPinList = !this.state.expendedPinList;\n    }\n\n    get expendedPinList() {\n        return this.env.isSmall ? this.state.expendedPinList : false;\n    }\n\n    get canDisplayPinList() {\n        return !this.env.isSmall || this.expendedPinList;\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { registry } from \"@web/core/registry\";\nimport { MapArchParser } from \"./map_arch_parser\";\nimport { MapModel } from \"./map_model\";\nimport { MapController } from \"./map_controller\";\nimport { MapRenderer } from \"./map_renderer\";\n\nexport const mapView = {\n    type: \"map\",\n    Controller: MapController,\n    Renderer: MapRenderer,\n    Model: MapModel,\n    ArchParser: MapArchParser,\n    buttonTemplate: \"web_map.MapView.Buttons\",\n\n    props: (genericProps, view, config) => {\n        let modelParams = genericProps.state;\n        if (!modelParams) {\n            const { arch, resModel, fields, context } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch);\n            const views = config.views || [];\n            modelParams = {\n                allowResequence: archInfo.allowResequence || false,\n                context: context,\n                defaultOrder: archInfo.defaultOrder,\n                fieldNames: archInfo.fieldNames,\n                fieldNamesMarkerPopup: archInfo.fieldNamesMarkerPopup,\n                fields: fields,\n                hasFormView: views.some((view) => view[1] === \"form\"),\n                hideAddress: archInfo.hideAddress || false,\n                hideName: archInfo.hideName || false,\n                hideTitle: archInfo.hideTitle || false,\n                limit: archInfo.limit || 80,\n                numbering: archInfo.routing || false,\n                offset: 0,\n                panelTitle: archInfo.panelTitle || config.getDisplayName() || _t(\"Items\"),\n                resModel: resModel,\n                resPartnerField: archInfo.resPartnerField,\n                routing: archInfo.routing || false,\n            };\n        }\n\n        return {\n            ...genericProps,\n            Model: view.Model,\n            modelParams,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"map\", mapView);\n", "import { getLocalYearAndWeek } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { evaluateExpr } from \"@web/core/py_js/py\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { getActiveActions } from \"@web/views/utils\";\n\nconst DECORATIONS = [\n    \"decoration-danger\",\n    \"decoration-info\",\n    \"decoration-secondary\",\n    \"decoration-success\",\n    \"decoration-warning\",\n];\nconst PARTS = { full: 1, half: 2, quarter: 4 };\nconst SCALES = {\n    day: {\n        // determines subcolumns\n        cellPrecisions: { full: 60, half: 30, quarter: 15 },\n        defaultPrecision: \"full\",\n        time: \"minute\",\n        unitDescription: _t(\"minutes\"),\n\n        // determines columns\n        interval: \"hour\",\n        minimalColumnWidth: 40,\n\n        // determines column groups\n        unit: \"day\",\n        groupHeaderFormatter: (date) => date.toFormat(\"dd MMMM yyyy\"),\n\n        defaultRange: { unit: \"day\", count: 3 },\n    },\n    week: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 192,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"week\",\n        groupHeaderFormatter: formatLocalWeekYear,\n\n        defaultRange: { unit: \"week\", count: 3 },\n    },\n    week_2: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 96,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"week\",\n        groupHeaderFormatter: formatLocalWeekYear,\n\n        defaultRange: { unit: \"week\", count: 6 },\n    },\n    month: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 50,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"month\",\n        groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM yyyy\" : \"MMMM yyyy\"),\n\n        defaultRange: { unit: \"month\", count: 3 },\n    },\n    month_3: {\n        cellPrecisions: { full: 24, half: 12 },\n        defaultPrecision: \"half\",\n        time: \"hour\",\n        unitDescription: _t(\"hours\"),\n\n        interval: \"day\",\n        minimalColumnWidth: 18,\n        colHeaderFormatter: (date) => date.toFormat(\"dd\"),\n\n        unit: \"month\",\n        groupHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM yyyy\" : \"MMMM yyyy\"),\n\n        defaultRange: { unit: \"month\", count: 6 },\n    },\n    year: {\n        cellPrecisions: { full: 1 },\n        defaultPrecision: \"full\",\n        time: \"month\",\n        unitDescription: _t(\"months\"),\n\n        interval: \"month\",\n        minimalColumnWidth: 60,\n        colHeaderFormatter: (date, env) => date.toFormat(env.isSmall ? \"MMM\" : \"MMMM\"),\n\n        unit: \"year\",\n        groupHeaderFormatter: (date) => date.toFormat(\"yyyy\"),\n\n        defaultRange: { unit: \"year\", count: 1 },\n    },\n};\n\n/**\n * Formats a date to a `'W'W kkkk` datetime string, in the user's locale settings.\n *\n * @param {Date|luxon.DateTime} date\n * @returns {string}\n */\nfunction formatLocalWeekYear(date) {\n    const { year, week } = getLocalYearAndWeek(date);\n    return `W${week} ${year}`;\n}\n\nfunction getPreferedScaleId(scaleId, scales) {\n    // we assume that scales is not empty\n    if (scaleId in scales) {\n        return scaleId;\n    }\n    const scaleIds = Object.keys(SCALES);\n    const index = scaleIds.findIndex((id) => id === scaleId);\n    for (let j = index - 1; j >= 0; j--) {\n        const id = scaleIds[j];\n        if (id in scales) {\n            return id;\n        }\n    }\n    for (let j = index + 1; j < scaleIds.length; j++) {\n        const id = scaleIds[j];\n        if (id in scales) {\n            return id;\n        }\n    }\n}\n\nconst RANGES = {\n    day: { scaleId: \"day\", description: _t(\"Today\") },\n    week: { scaleId: \"week\", description: _t(\"This week\") },\n    month: { scaleId: \"month\", description: _t(\"This month\") },\n    quarter: { scaleId: \"month_3\", description: _t(\"This quarter\") },\n    year: { scaleId: \"year\", description: _t(\"This year\") },\n};\n\nexport class GanttArchParser {\n    parse(arch) {\n        let infoFromRootNode;\n        const decorationFields = [];\n        const popoverArchParams = {\n            displayGenericButtons: true,\n            bodyTemplate: null,\n            footerTemplate: null,\n        };\n\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"gantt\": {\n                    infoFromRootNode = getInfoFromRootNode(node);\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\");\n                    decorationFields.push(fieldName);\n                    break;\n                }\n                case \"templates\": {\n                    const body = node.querySelector(\"[t-name=gantt-popover]\") || null;\n                    if (body) {\n                        popoverArchParams.bodyTemplate = body.cloneNode(true);\n                        popoverArchParams.bodyTemplate.removeAttribute(\"t-name\");\n                        const footer = popoverArchParams.bodyTemplate.querySelector(\"footer\");\n                        if (footer) {\n                            popoverArchParams.displayGenericButtons = false;\n                            footer.remove();\n                            const footerTemplate = new Document().createElement(\"t\");\n                            footerTemplate.append(...footer.children);\n                            popoverArchParams.footerTemplate = footerTemplate;\n                            const replace = footer.getAttribute(\"replace\");\n                            if (replace && !exprToBoolean(replace)) {\n                                popoverArchParams.displayGenericButtons = true;\n                            }\n                        }\n                    }\n                }\n            }\n        });\n\n        return {\n            ...infoFromRootNode,\n            decorationFields,\n            popoverArchParams,\n        };\n    }\n}\n\nfunction getInfoFromRootNode(rootNode) {\n    const attrs = {};\n    for (const { name, value } of rootNode.attributes) {\n        attrs[name] = value;\n    }\n\n    const { create: canCreate, delete: canDelete, edit: canEdit } = getActiveActions(rootNode);\n    const canCellCreate = exprToBoolean(attrs.cell_create, true) && canCreate;\n    const canPlan = exprToBoolean(attrs.plan, true) && canEdit;\n\n    let consolidationMaxField;\n    let consolidationMaxValue;\n    const consolidationMax = attrs.consolidation_max ? evaluateExpr(attrs.consolidation_max) : {};\n    if (Object.keys(consolidationMax).length > 0) {\n        consolidationMaxField = Object.keys(consolidationMax)[0];\n        consolidationMaxValue = consolidationMax[consolidationMaxField];\n    }\n\n    const consolidationParams = {\n        excludeField: attrs.consolidation_exclude,\n        field: attrs.consolidation,\n        maxField: consolidationMaxField,\n        maxValue: consolidationMaxValue,\n    };\n\n    const dependencyField = attrs.dependency_field || null;\n    const dependencyEnabled = !!dependencyField;\n    const dependencyInvertedField = attrs.dependency_inverted_field || null;\n\n    const allowedScales = [];\n    if (attrs.scales) {\n        for (const key of attrs.scales.split(\",\")) {\n            if (SCALES[key]) {\n                allowedScales.push(key);\n            }\n        }\n    }\n    if (allowedScales.length === 0) {\n        allowedScales.push(...Object.keys(SCALES));\n    }\n\n    // Cell precision\n    const cellPrecisions = {};\n\n    // precision = {'day': 'hour:half', 'week': 'day:half', 'month': 'day', 'year': 'month:quarter'}\n    const precisionAttrs = attrs.precision ? evaluateExpr(attrs.precision) : {};\n    for (const scaleId in SCALES) {\n        if (precisionAttrs[scaleId]) {\n            const precision = precisionAttrs[scaleId].split(\":\"); // hour:half\n            // Note that precision[0] (which is the cell interval) is not\n            // taken into account right now because it is no customizable.\n            if (\n                precision[1] &&\n                Object.keys(SCALES[scaleId].cellPrecisions).includes(precision[1])\n            ) {\n                cellPrecisions[scaleId] = precision[1];\n            }\n        }\n        cellPrecisions[scaleId] ||= SCALES[scaleId].defaultPrecision;\n    }\n\n    const scales = {};\n    for (const scaleId of allowedScales) {\n        const precision = cellPrecisions[scaleId];\n        const referenceScale = SCALES[scaleId];\n        scales[scaleId] = {\n            ...referenceScale,\n            cellPart: PARTS[precision],\n            cellTime: referenceScale.cellPrecisions[precision],\n            id: scaleId,\n            unitDescription: referenceScale.unitDescription.toString(),\n        };\n        // protect SCALES content\n        delete scales[scaleId].cellPrecisions;\n    }\n\n    const ranges = {};\n    for (const rangeId in RANGES) {\n        const referenceRange = RANGES[rangeId];\n        ranges[rangeId] = {\n            ...referenceRange,\n            id: rangeId,\n            scaleId: getPreferedScaleId(referenceRange.scaleId, scales),\n            description: referenceRange.description.toString(),\n        };\n    }\n\n    let pillDecorations = null;\n    for (const decoration of DECORATIONS) {\n        if (decoration in attrs) {\n            if (!pillDecorations) {\n                pillDecorations = {};\n            }\n            pillDecorations[decoration] = attrs[decoration];\n        }\n    }\n\n    return {\n        canCellCreate,\n        canCreate,\n        canDelete,\n        canEdit,\n        canPlan,\n        colorField: attrs.color,\n        computePillDisplayName: !!attrs.pill_label,\n        consolidationParams,\n        createAction: attrs.on_create || null,\n        dateStartField: attrs.date_start,\n        dateStopField: attrs.date_stop,\n        defaultGroupBy: attrs.default_group_by ? attrs.default_group_by.split(\",\") : [],\n        defaultRange: attrs.default_range,\n        defaultScale: attrs.default_scale || \"month\",\n        dependencyEnabled,\n        dependencyField,\n        dependencyInvertedField,\n        disableDrag: exprToBoolean(attrs.disable_drag_drop),\n        displayMode: attrs.display_mode || \"dense\",\n        displayTotalRow: exprToBoolean(attrs.total_row),\n        displayUnavailability: exprToBoolean(attrs.display_unavailability),\n        formViewId: attrs.form_view_id ? parseInt(attrs.form_view_id, 10) : false,\n        offset: attrs.offset,\n        pagerLimit: attrs.groups_limit ? parseInt(attrs.groups_limit, 10) : null,\n        pillDecorations,\n        progressBarFields: attrs.progress_bar ? attrs.progress_bar.split(\",\") : null,\n        progressField: attrs.progress || null,\n        ranges,\n        scales,\n        string: attrs.string || _t(\"Gantt View\").toString(),\n        thumbnails: attrs.thumbnails ? evaluateExpr(attrs.thumbnails) : {},\n    };\n}\n", "import { ViewCompiler } from \"@web/views/view_compiler\";\n\nexport class GanttCompiler extends ViewCompiler {}\nGanttCompiler.OWL_DIRECTIVE_WHITELIST = [\n    ...ViewCompiler.OWL_DIRECTIVE_WHITELIST,\n    \"t-name\",\n    \"t-esc\",\n    \"t-out\",\n    \"t-set\",\n    \"t-value\",\n    \"t-if\",\n    \"t-else\",\n    \"t-elif\",\n    \"t-foreach\",\n    \"t-as\",\n    \"t-key\",\n    \"t-att.*\",\n    \"t-call\",\n    \"t-translation\",\n];\n", "import { Component, onWillRender, useEffect, useRef } from \"@odoo/owl\";\n\n/**\n * @typedef {\"error\" | \"warning\"} ConnectorAlert\n * @typedef {`__connector__${number | \"new\"}`} ConnectorId\n * @typedef {import(\"./gantt_renderer\").Point} Point\n *\n * @typedef ConnectorProps\n * @property {ConnectorId} id\n * @property {ConnectorAlert | null} alert\n * @property {boolean} highlighted\n * @property {boolean} displayButtons\n * @property {Point | () => Point | null} sourcePoint\n * @property {Point | () => Point | null} targetPoint\n *\n * @typedef {Object} PathInfo\n * @property {Point} sourceControlPoint\n * @property {Point} targetControlPoint\n * @property {Point} removeButtonPosition\n *\n * @typedef Point\n * @property {number} [x]\n * @property {number} [y]\n */\n\n/**\n * Gets the stroke's rgba css string corresponding to the provided parameters for both the stroke and its\n * hovered state.\n *\n * @param {number} r [0, 255]\n * @param {number} g [0, 255]\n * @param {number} b [0, 255]\n * @return {{ stroke: string, hoveredStroke: string }} the css colors.\n */\nexport function getStrokeAndHoveredStrokeColor(r, g, b) {\n    return {\n        color: `rgba(${r},${g},${b},0.5)`,\n        highlightedColor: `rgba(${r},${g},${b},1)`,\n    };\n}\n\nexport const COLORS = {\n    default: getStrokeAndHoveredStrokeColor(143, 143, 143),\n    error: getStrokeAndHoveredStrokeColor(211, 65, 59),\n    warning: getStrokeAndHoveredStrokeColor(236, 151, 31),\n    outline: getStrokeAndHoveredStrokeColor(255, 255, 255),\n};\n\n/** @extends {Component<{ reactive: ConnectorProps }, any>} */\nexport class GanttConnector extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                id: String,\n                alert: {\n                    type: [{ value: \"error\" }, { value: \"warning\" }, { value: null }],\n                    optional: true,\n                },\n                highlighted: { type: Boolean, optional: true },\n                displayButtons: { type: Boolean, optional: true },\n                sourcePoint: [\n                    { value: null },\n                    Function,\n                    { type: Object, shape: { left: Number, top: Number } },\n                ],\n                targetPoint: [\n                    { value: null },\n                    Function,\n                    { type: Object, shape: { left: Number, top: Number } },\n                ],\n            },\n        },\n        onLeftButtonClick: { type: Function, optional: true },\n        onRemoveButtonClick: { type: Function, optional: true },\n        onRightButtonClick: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        highlighted: false,\n        displayButtons: false,\n    };\n    static template = \"web_gantt.GanttConnector\";\n\n    rootRef = useRef(\"root\");\n    style = {\n        hoverEaseWidth: 10,\n        slackness: 0.9,\n        stroke: { width: 2 },\n        outlineStroke: { width: 1 },\n    };\n\n    get alert() {\n        return this.props.reactive.alert;\n    }\n\n    get displayButtons() {\n        return this.props.reactive.displayButtons;\n    }\n\n    get highlighted() {\n        return this.props.reactive.highlighted;\n    }\n\n    get id() {\n        return this.props.reactive.id;\n    }\n\n    get isNew() {\n        return this.id.endsWith(\"new\");\n    }\n\n    get sourcePoint() {\n        return this.props.reactive.sourcePoint;\n    }\n\n    get targetPoint() {\n        return this.props.reactive.targetPoint;\n    }\n\n    setup() {\n        onWillRender(this.onWillRender);\n\n        useEffect(\n            (el, sourceLeft, sourceTop, targetLeft, targetTop) => {\n                if (!el) {\n                    return;\n                }\n                const { sourceControlPoint, targetControlPoint, removeButtonPosition } =\n                    this.getPathInfo(\n                        { left: sourceLeft, top: sourceTop },\n                        { left: targetLeft, top: targetTop },\n                        this.style.slackness\n                    );\n\n                const drawingCommands = [\n                    `M`,\n                    `${sourceLeft},${sourceTop}`,\n                    `C`,\n                    `${sourceControlPoint.left},${sourceControlPoint.top}`,\n                    `${targetControlPoint.left},${targetControlPoint.top}`,\n                    `${targetLeft},${targetTop}`,\n                ].join(\" \");\n\n                const paths = el.querySelectorAll(\n                    \".o_connector_stroke, .o_connector_stroke_hover_ease\"\n                );\n                for (const path of paths) {\n                    path.setAttribute(\"d\", drawingCommands);\n                }\n\n                const svgButtons = el.querySelector(\".o_connector_stroke_buttons\");\n                if (svgButtons) {\n                    svgButtons.setAttribute(\"x\", removeButtonPosition.left - 24);\n                    svgButtons.setAttribute(\"y\", removeButtonPosition.top - 8);\n                }\n            },\n            () => this.getEffectDependencies()\n        );\n    }\n\n    /**\n     * Refreshes the connector properties from the props.\n     *\n     * @param {ConnectorProps} props\n     */\n    computeStyle({ alert, highlighted }) {\n        const key = highlighted ? \"highlightedColor\" : \"color\";\n        const strokeType = alert || \"default\";\n        this.style = {\n            hoverEaseWidth: 10,\n            slackness: 0.9,\n            stroke: {\n                color: COLORS[strokeType][key],\n                width: 2,\n            },\n            outlineStroke: {\n                color: COLORS.outline[key],\n                width: 1,\n            },\n        };\n    }\n\n    getEffectDependencies() {\n        let sourcePoint = this.sourcePoint || { left: 0, top: 0 };\n        if (typeof sourcePoint === \"function\") {\n            sourcePoint = sourcePoint();\n        }\n        let targetPoint = this.targetPoint || { left: 0, top: 0 };\n        if (typeof targetPoint === \"function\") {\n            targetPoint = targetPoint();\n        }\n        const { x, y } = this.rootRef.el?.getBoundingClientRect() || { x: 0, y: 0 };\n\n        return [\n            this.rootRef.el,\n            sourcePoint.left - x,\n            sourcePoint.top - y,\n            targetPoint.left - x,\n            targetPoint.top - y,\n            this.displayButtons,\n        ];\n    }\n\n    /**\n     * Returns the linear interpolation for a point to be found somewhere on the line startingPoint, endingPoint.\n     *\n     * @param {Point} startingPoint\n     * @param {Point} endingPoint\n     * @param {number} lambda\n     * @returns {Point}\n     */\n    getLinearInterpolation(startingPoint, endingPoint, lambda = 0.5) {\n        return {\n            left: lambda * startingPoint.left + (1 - lambda) * endingPoint.left,\n            top: lambda * startingPoint.top + (1 - lambda) * endingPoint.top,\n        };\n    }\n\n    /**\n     * Returns the parameters of both the single Bezier curve as well as is decomposition into two beziers curves\n     * (which allows to get the middle position of the single Bezier curve) for the provided source, target and\n     * slackness (0 being a straight line).\n     *\n     * @param {Point} sourcePoint\n     * @param {Point} targetPoint\n     * @param {number} slackness [0, 1]\n     * @returns {PathInfo}\n     */\n    getPathInfo(sourcePoint, targetPoint, slackness) {\n        // If the source is on the left of the target, we need to invert the control points.\n        const xDelta = targetPoint.left - sourcePoint.left;\n        const yDelta = targetPoint.top - sourcePoint.top;\n        const directionFactor = Math.sign(xDelta);\n\n        // What follows can be seen as magic numbers. And those are indeed such numbers as they have been determined\n        // by observing their shape while creating short and long connectors. These seems to allow keeping the same\n        // kind of shape amongst short and long connectors.\n        const xInc = 100 + (Math.abs(xDelta) * slackness) / 10;\n        const yInc =\n            Math.abs(yDelta) < 16 && directionFactor === -1 ? 15 - 0.001 * xDelta * slackness : 0;\n\n        const b = {\n            left: sourcePoint.left + xInc,\n            top: sourcePoint.top + yInc,\n        };\n\n        // Prevent having the air pin effect when in creation and having target on the left of the source\n        const c = {\n            left: targetPoint.left + (this.isNew && directionFactor === -1 ? xInc : -xInc),\n            top: targetPoint.top + yInc,\n        };\n\n        const e = this.getLinearInterpolation(sourcePoint, b);\n        const f = this.getLinearInterpolation(b, c);\n        const g = this.getLinearInterpolation(c, targetPoint);\n        const h = this.getLinearInterpolation(e, f);\n        const i = this.getLinearInterpolation(f, g);\n        const j = this.getLinearInterpolation(h, i);\n\n        return {\n            sourceControlPoint: b,\n            targetControlPoint: c,\n            removeButtonPosition: j,\n        };\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onLeftButtonClick() {\n        if (this.props.onLeftButtonClick) {\n            this.props.onLeftButtonClick();\n        }\n    }\n\n    onRemoveButtonClick() {\n        if (this.props.onRemoveButtonClick) {\n            this.props.onRemoveButtonClick();\n        }\n    }\n\n    onRightButtonClick() {\n        if (this.props.onRightButtonClick) {\n            this.props.onRightButtonClick();\n        }\n    }\n\n    onWillRender() {\n        const key = this.highlighted ? \"highlightedColor\" : \"color\";\n        this.style.stroke.color = COLORS[this.alert || \"default\"][key];\n        this.style.outlineStroke.color = COLORS.outline[key];\n    }\n}\n", "import { _t } from \"@web/core/l10n/translation\";\nimport { Component, onWillUnmount, useEffect, useRef, useSubEnv } from \"@odoo/owl\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { Layout } from \"@web/search/layout\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { usePager } from \"@web/search/pager_hook\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { CallbackRecorder, useSetupAction } from \"@web/search/action_hook\";\n\nexport class GanttController extends Component {\n    static components = {\n        CogMenu,\n        Layout,\n        SearchBar,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        modelParams: Object,\n        scrollPosition: { type: Object, optional: true },\n    };\n    static template = \"web_gantt.GanttController\";\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.orm = useService(\"orm\");\n\n        useSubEnv({\n            getCurrentFocusDateCallBackRecorder: new CallbackRecorder(),\n        });\n\n        const rootRef = useRef(\"root\");\n\n        this.model = useModelWithSampleData(this.props.Model, this.props.modelParams);\n        useSetupAction({\n            rootRef,\n            getLocalState: () => {\n                return { metaData: this.model.metaData, displayParams: this.model.displayParams };\n            },\n        });\n\n        onWillUnmount(() => this.closeDialog?.());\n\n        usePager(() => {\n            const { groupedBy, pagerLimit, pagerOffset } = this.model.metaData;\n            const { count } = this.model.data;\n            if (pagerLimit !== null && groupedBy.length) {\n                return {\n                    offset: pagerOffset,\n                    limit: pagerLimit,\n                    total: count,\n                    onUpdate: async ({ offset, limit }) => {\n                        await this.model.updatePagerParams({ offset, limit });\n                    },\n                };\n            }\n        });\n\n        useEffect(\n            (showNoContentHelp) => {\n                if (showNoContentHelp) {\n                    const realRows = [\n                        ...rootRef.el.querySelectorAll(\n                            \".o_gantt_row_header:not(.o_sample_data_disabled)\"\n                        ),\n                    ];\n                    // interactive rows created in extensions (fromServer undefined)\n                    const headerContainerWidth =\n                        rootRef.el.querySelector(\".o_gantt_header_groups\").clientHeight +\n                        rootRef.el.querySelector(\".o_gantt_header_columns\").clientHeight;\n\n                    const offset = realRows.reduce(\n                        (current, el) => current + el.clientHeight,\n                        headerContainerWidth\n                    );\n\n                    const noContentHelperEl = rootRef.el.querySelector(\".o_view_nocontent\");\n                    noContentHelperEl.style.top = `${offset}px`;\n                }\n            },\n            () => [this.showNoContentHelp]\n        );\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get className() {\n        if (this.env.isSmall) {\n            const classList = (this.props.className || \"\").split(\" \");\n            classList.push(\"o_action_delegate_scroll\");\n            return classList.join(\" \");\n        }\n        return this.props.className;\n    }\n\n    get showNoContentHelp() {\n        return this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Record<string, any>} [context]\n     */\n    create(context) {\n        const { createAction } = this.model.metaData;\n        if (createAction) {\n            this.actionService.doAction(createAction, {\n                additionalContext: context,\n                onClose: () => {\n                    this.model.fetchData();\n                },\n            });\n        } else {\n            this.openDialog({ context });\n        }\n    }\n\n    /**\n     * Opens dialog to add/edit/view a record\n     *\n     * @param {Record<string, any>} props FormViewDialog props\n     * @param {Record<string, any>} [options={}]\n     */\n    openDialog(props, options = {}) {\n        const { canDelete, canEdit, resModel, formViewId: viewId } = this.model.metaData;\n\n        const title = props.title || (props.resId ? _t(\"Open\") : _t(\"Create\"));\n\n        let removeRecord;\n        if (canDelete && props.resId) {\n            removeRecord = () => {\n                return new Promise((resolve) => {\n                    this.dialogService.add(ConfirmationDialog, {\n                        body: _t(\"Are you sure to delete this record?\"),\n                        confirm: async () => {\n                            await this.orm.unlink(resModel, [props.resId]);\n                            resolve();\n                        },\n                        cancel: () => {},\n                    });\n                });\n            };\n        }\n\n        this.closeDialog = this.dialogService.add(\n            FormViewDialog,\n            {\n                title,\n                resModel,\n                viewId,\n                resId: props.resId,\n                size: props.size,\n                mode: canEdit ? \"edit\" : \"readonly\",\n                context: props.context,\n                removeRecord,\n            },\n            {\n                ...options,\n                onClose: () => {\n                    this.closeDialog = null;\n                    this.model.fetchData();\n                },\n            }\n        );\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    onAddClicked() {\n        const { scale } = this.model.metaData;\n        const focusDate = this.getCurrentFocusDate();\n        const start = focusDate.startOf(scale.unit);\n        const stop = focusDate.endOf(scale.unit).plus({ millisecond: 1 });\n        const context = this.model.getDialogContext({ start, stop, withDefault: true });\n        this.create(context);\n    }\n\n    getCurrentFocusDate() {\n        const { callbacks } = this.env.getCurrentFocusDateCallBackRecorder;\n        if (callbacks.length) {\n            return callbacks[0]();\n        }\n        return this.model.metaData.focusDate;\n    }\n}\n", "import { onWillUnmount, status, useComponent, useEffect, useEnv } from \"@odoo/owl\";\nimport { getEndOfLocalWeek, getStartOfLocalWeek } from \"@web/core/l10n/dates\";\nimport { makePopover, usePopover } from \"@web/core/popover/popover_hook\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder_owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { GanttPopoverInDialog } from \"./gantt_popover_in_dialog\";\n\n/** @typedef {luxon.DateTime} DateTime */\n\n/**\n * @param {number} target\n * @param {number[]} values\n * @returns {number}\n */\nfunction closest(target, values) {\n    return values.reduce(\n        (prev, val) => (Math.abs(val - target) < Math.abs(prev - target) ? val : prev),\n        Infinity\n    );\n}\n\n/**\n * Adds a time diff to a date keeping the same value even if the offset changed\n * during the manipulation. This is typically needed with timezones using DayLight\n * Saving offset changes.\n *\n * @example dateAddFixedOffset(luxon.DateTime.local(), { hour: 1 });\n * @param {DateTime} date\n * @param {Record<string, number>} plusParams\n */\nexport function dateAddFixedOffset(date, plusParams) {\n    const shouldApplyOffset = Object.keys(plusParams).some((key) =>\n        /^(hour|minute|second)s?$/i.test(key)\n    );\n    const result = date.plus(plusParams);\n    if (shouldApplyOffset) {\n        const initialOffset = date.offset;\n        const diff = initialOffset - result.offset;\n        if (diff) {\n            const adjusted = result.plus({ minute: diff });\n            return adjusted.offset === initialOffset ? result : adjusted;\n        }\n    }\n    return result;\n}\n\nexport function diffColumn(col1, col2, unit) {\n    return col2.diff(col1, unit).values[`${unit}s`];\n}\n\nexport function getRangeFromDate(rangeId, date) {\n    const startDate = localStartOf(date, rangeId);\n    const stopDate = startDate.plus({ [rangeId]: 1 }).minus({ day: 1 });\n    return { focusDate: date, startDate, stopDate, rangeId };\n}\n\nexport function localStartOf(date, unit) {\n    return unit === \"week\" ? getStartOfLocalWeek(date) : date.startOf(unit);\n}\n\nexport function localEndOf(date, unit) {\n    return unit === \"week\" ? getEndOfLocalWeek(date) : date.endOf(unit);\n}\n\n/**\n * @param {number} cellPart\n * @param {(0 | 1)[]} subSlotUnavailabilities\n * @param {boolean} isToday\n * @returns {string | null}\n */\nexport function getCellColor(cellPart, subSlotUnavailabilities, isToday) {\n    const sum = subSlotUnavailabilities.reduce((acc, d) => acc + d);\n    if (!sum) {\n        return null;\n    }\n    switch (cellPart) {\n        case sum: {\n            return `background-color:${getCellPartColor(sum, isToday)}`;\n        }\n        case 2: {\n            const [c0, c1] = subSlotUnavailabilities.map((d) => getCellPartColor(d, isToday));\n            return `background:linear-gradient(90deg,${c0}49%,${c1}50%)`;\n        }\n        case 4: {\n            const [c0, c1, c2, c3] = subSlotUnavailabilities.map((d) =>\n                getCellPartColor(d, isToday)\n            );\n            return `background:linear-gradient(90deg,${c0}24%,${c1}25%,${c1}49%,${c2}50%,${c2}74%,${c3}75%)`;\n        }\n    }\n}\n\n/**\n * @param {0 | 1} availability\n * @param {boolean} isToday\n * @returns {string}\n */\nexport function getCellPartColor(availability, isToday) {\n    if (availability) {\n        return \"var(--Gantt__DayOff-background-color)\";\n    } else if (isToday) {\n        return \"var(--Gantt__DayOffToday-background-color)\";\n    } else {\n        return \"var(--Gantt__Day-background-color)\";\n    }\n}\n\n/**\n * @param {number | [number, string]} value\n * @returns {number}\n */\nexport function getColorIndex(value) {\n    if (typeof value === \"number\") {\n        return Math.round(value) % NB_GANTT_RECORD_COLORS;\n    } else if (Array.isArray(value)) {\n        return value[0] % NB_GANTT_RECORD_COLORS;\n    }\n    return 0;\n}\n\n/**\n * Intervals are supposed to intersect (intersection duration >= 1 milliseconds)\n *\n * @param {[DateTime, DateTime]} interval\n * @param {[DateTime, DateTime]} otherInterval\n * @returns {[DateTime, DateTime]}\n */\nexport function getIntersection(interval, otherInterval) {\n    const [start, end] = interval;\n    const [otherStart, otherEnd] = otherInterval;\n    return [start >= otherStart ? start : otherStart, end <= otherEnd ? end : otherEnd];\n}\n\n/**\n * Computes intersection of a closed interval with a union of closed intervals ordered and disjoint\n * = a union of intersections\n *\n * @param {[DateTime, DateTime]} interval\n * @param {[DateTime, DateTime]} intervals\n * @returns {[DateTime, DateTime][]}\n */\nexport function getUnionOfIntersections(interval, intervals) {\n    const [start, end] = interval;\n    const intersecting = intervals.filter((otherInterval) => {\n        const [otheStart, otherEnd] = otherInterval;\n        return otherEnd > start && end > otheStart;\n    });\n    const len = intersecting.length;\n    if (len === 0) {\n        return [];\n    }\n    const union = [];\n    const first = getIntersection(interval, intersecting[0]);\n    union.push(first);\n    if (len >= 2) {\n        const last = getIntersection(interval, intersecting[len - 1]);\n        union.push(...intersecting.slice(1, len - 1), last);\n    }\n    return union;\n}\n\n/**\n * @param {Object} params\n * @param {Ref<HTMLElement>} params.ref\n * @param {string} params.selector\n * @param {string} params.related\n * @param {string} params.className\n */\nexport function useMultiHover({ ref, selector, related, className }) {\n    /**\n     * @param {HTMLElement} el\n     */\n    const findSiblings = (el) =>\n        ref.el.querySelectorAll(\n            related\n                .map((attr) => `[${attr}='${el.getAttribute(attr).replace(/'/g, \"\\\\'\")}']`)\n                .join(\"\")\n        );\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    const onPointerEnter = (ev) => {\n        for (const sibling of findSiblings(ev.target)) {\n            sibling.classList.add(...classList);\n            classedEls.add(sibling);\n        }\n    };\n\n    /**\n     * @param {PointerEvent} ev\n     */\n    const onPointerLeave = (ev) => {\n        for (const sibling of findSiblings(ev.target)) {\n            sibling.classList.remove(...classList);\n            classedEls.delete(sibling);\n        }\n    };\n\n    const classList = className.split(/\\s+/g);\n    const classedEls = new Set();\n\n    useEffect(\n        (...targets) => {\n            if (targets.length) {\n                for (const target of targets) {\n                    target.addEventListener(\"pointerenter\", onPointerEnter);\n                    target.addEventListener(\"pointerleave\", onPointerLeave);\n                }\n                return () => {\n                    for (const el of classedEls) {\n                        el.classList.remove(...classList);\n                    }\n                    classedEls.clear();\n                    for (const target of targets) {\n                        target.removeEventListener(\"pointerenter\", onPointerEnter);\n                        target.removeEventListener(\"pointerleave\", onPointerLeave);\n                    }\n                };\n            }\n        },\n        () => [...ref.el.querySelectorAll(selector)]\n    );\n}\n\nconst NB_GANTT_RECORD_COLORS = 12;\n\nfunction getElementCenter(el) {\n    const { x, y, width, height } = el.getBoundingClientRect();\n    return {\n        x: x + width / 2,\n        y: y + height / 2,\n    };\n}\n\n// Resizable hook handles\n\nconst HANDLE_CLASS_START = \"o_handle_start\";\nconst HANDLE_CLASS_END = \"o_handle_end\";\nconst handles = {\n    start: document.createElement(\"div\"),\n    end: document.createElement(\"div\"),\n};\n\n// Draggable hooks\n\nexport const useGanttConnectorDraggable = makeDraggableHook({\n    name: \"useGanttConnectorDraggable\",\n    acceptedParams: {\n        parentWrapper: [String],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.parentWrapper = params.parentWrapper;\n        ctx.followCursor = false;\n    },\n    onDragStart: ({ ctx, addStyle }) => {\n        const { current } = ctx;\n        const parent = current.element.closest(ctx.parentWrapper);\n        if (!parent) {\n            return;\n        }\n        for (const otherParent of ctx.ref.el.querySelectorAll(ctx.parentWrapper)) {\n            if (otherParent !== parent) {\n                addStyle(otherParent, { pointerEvents: \"auto\" });\n            }\n        }\n        return { sourcePill: parent, ...current.connectorCenter };\n    },\n    onDrag: ({ ctx }) => {\n        ctx.current.connectorCenter = getElementCenter(ctx.current.element);\n        return pick(ctx.current, \"connectorCenter\");\n    },\n    onDragEnd: ({ ctx }) => pick(ctx.current, \"element\"),\n    onDrop: ({ ctx, target }) => {\n        const { current } = ctx;\n        const parent = current.element.closest(ctx.parentWrapper);\n        const targetParent = target.closest(ctx.parentWrapper);\n        if (!targetParent || targetParent === parent) {\n            return;\n        }\n        return { target: targetParent };\n    },\n    onWillStartDrag: ({ ctx }) => {\n        ctx.current.connectorCenter = getElementCenter(ctx.current.element);\n    },\n});\n\nfunction getCoordinate(style, name) {\n    return +style.getPropertyValue(name).slice(1);\n}\n\nfunction getColumnStart(style) {\n    return getCoordinate(style, \"grid-column-start\");\n}\n\nfunction getColumnEnd(style) {\n    return getCoordinate(style, \"grid-column-end\");\n}\n\nexport const useGanttDraggable = makeDraggableHook({\n    name: \"useGanttDraggable\",\n    acceptedParams: {\n        cells: [String, Function],\n        cellDragClassName: [String, Function],\n        ghostClassName: [String, Function],\n        hoveredCell: [Object],\n        addStickyCoordinates: [Function],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.cellSelector = params.cells;\n        ctx.ghostClassName = params.ghostClassName;\n        ctx.cellDragClassName = params.cellDragClassName;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.addStickyCoordinates = params.addStickyCoordinates;\n    },\n    onDragStart({ ctx }) {\n        const { current, ghostClassName } = ctx;\n        current.element.before(current.placeHolder);\n        if (ghostClassName) {\n            current.placeHolder.classList.add(ghostClassName);\n        }\n        return { pill: current.element };\n    },\n    onDrag({ ctx, addStyle }) {\n        const { cellSelector, current, hoveredCell } = ctx;\n        let { el: cell, part } = hoveredCell;\n\n        const isDifferentCell = cell !== current.cell.el;\n        const isDifferentPart = part !== current.cell.part;\n\n        if (cell && !cell.matches(cellSelector)) {\n            cell = null; // Not a cell\n        }\n\n        current.cell.el = cell;\n        current.cell.part = part;\n\n        if (cell) {\n            // Recompute cell style if in a different cell\n            if (isDifferentCell) {\n                const style = getComputedStyle(cell);\n                current.cell.gridRow = style.getPropertyValue(\"grid-row\");\n                current.cell.gridColumnStart = getColumnStart(style) + current.gridColumnOffset;\n            }\n            // Assign new grid coordinates if in different cell or different cell part\n            if (isDifferentCell || isDifferentPart) {\n                const { pillSpan } = current;\n                const { gridRow, gridColumnStart: start } = current.cell;\n                const gridColumnStart = clamp(start + part, 1, current.maxGridColumnStart);\n                const gridColumnEnd = gridColumnStart + pillSpan;\n\n                addStyle(current.cellGhost, {\n                    gridRow,\n                    gridColumn: `c${gridColumnStart} / c${gridColumnEnd}`,\n                });\n\n                const [gridRowStart, gridRowEnd] = /r(\\d+) \\/ r(\\d+)/g.exec(gridRow).slice(1);\n                ctx.addStickyCoordinates(\n                    [gridRowStart, gridRowEnd],\n                    [gridColumnStart, gridColumnEnd]\n                );\n                current.cell.col = gridColumnStart;\n            }\n        } else {\n            current.cell.col = null;\n        }\n\n        // Attach or remove cell ghost\n        if (isDifferentCell) {\n            if (cell) {\n                cell.after(current.cellGhost);\n            } else {\n                current.cellGhost.remove();\n            }\n        }\n\n        return { pill: current.element };\n    },\n    onDragEnd({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onDrop({ ctx }) {\n        const { cell, element, initialCol } = ctx.current;\n        if (cell.col !== null) {\n            return {\n                pill: element,\n                cell: cell.el,\n                diff: cell.col - initialCol,\n            };\n        }\n    },\n    onWillStartDrag({ ctx, addCleanup, addClass }) {\n        const { current } = ctx;\n        const { el: cell, part } = ctx.hoveredCell;\n\n        current.placeHolder = current.element.cloneNode(true);\n        current.cellGhost = document.createElement(\"div\");\n        current.cellGhost.className = ctx.cellDragClassName;\n        current.cell = { el: null, index: null, part: 0 };\n\n        const gridStyle = getComputedStyle(cell.parentElement);\n        const pillStyle = getComputedStyle(current.element);\n        const cellStyle = getComputedStyle(cell);\n\n        const gridTemplateColumns = gridStyle.getPropertyValue(\"grid-template-columns\");\n        const pGridColumnStart = getColumnStart(pillStyle);\n        const pGridColumnEnd = getColumnEnd(pillStyle);\n        const cGridColumnStart = getColumnStart(cellStyle) + part;\n\n        let highestGridCol;\n        for (const e of gridTemplateColumns.split(/\\s+/).reverse()) {\n            const res = /\\[c(\\d+)\\]/g.exec(e);\n            if (res) {\n                highestGridCol = +res[1];\n                break;\n            }\n        }\n\n        const pillSpan = pGridColumnEnd - pGridColumnStart;\n\n        current.initialCol = pGridColumnStart;\n        current.maxGridColumnStart = highestGridCol - pillSpan;\n        current.gridColumnOffset = pGridColumnStart - cGridColumnStart;\n        current.pillSpan = pillSpan;\n\n        addClass(ctx.ref.el, \"pe-auto\");\n        addCleanup(() => {\n            current.placeHolder.remove();\n            current.cellGhost.remove();\n        });\n    },\n});\n\nexport const useGanttUndraggable = makeDraggableHook({\n    name: \"useGanttUndraggable\",\n    onDragStart({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onDragEnd({ ctx }) {\n        return { pill: ctx.current.element };\n    },\n    onWillStartDrag({ ctx, addCleanup, addClass, addStyle, getRect }) {\n        const { x, y, width, height } = getRect(ctx.current.element);\n        ctx.current.container = document.createElement(\"div\");\n\n        addClass(ctx.ref.el, \"pe-auto\");\n        addStyle(ctx.current.container, {\n            position: \"fixed\",\n            left: `${x}px`,\n            top: `${y}px`,\n            width: `${width}px`,\n            height: `${height}px`,\n        });\n\n        ctx.current.element.after(ctx.current.container);\n        addCleanup(() => ctx.current.container.remove());\n    },\n});\n\nexport const useGanttResizable = makeDraggableHook({\n    name: \"useGanttResizable\",\n    requiredParams: [\"handles\"],\n    acceptedParams: {\n        innerPills: [String, Function],\n        handles: [String, Function],\n        hoveredCell: [Object],\n        rtl: [Boolean, Function],\n        cells: [String, Function],\n        precision: [Number, Function],\n        showHandles: [Function],\n    },\n    onComputeParams({ ctx, params, addCleanup, addEffectCleanup, getRect }) {\n        const onElementPointerEnter = (ev) => {\n            if (ctx.dragging || ctx.willDrag) {\n                return;\n            }\n\n            const pill = ev.target;\n            const innerPill = pill.querySelector(params.innerPills);\n\n            const pillRect = getRect(innerPill);\n\n            for (const el of Object.values(handles)) {\n                el.style.height = `${pillRect.height}px`;\n            }\n\n            const showHandles = params.showHandles ? params.showHandles(pill) : {};\n            if (\"start\" in showHandles && !showHandles.start) {\n                handles.start.remove();\n            } else {\n                innerPill.appendChild(handles.start);\n            }\n            if (\"end\" in showHandles && !showHandles.end) {\n                handles.end.remove();\n            } else {\n                innerPill.appendChild(handles.end);\n            }\n        };\n\n        const onElementPointerLeave = () => {\n            const remove = () => Object.values(handles).forEach((h) => h.remove());\n            if (ctx.dragging || ctx.current.element) {\n                addCleanup(remove);\n            } else {\n                remove();\n            }\n        };\n\n        ctx.cellSelector = params.cells;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.precision = params.precision;\n        ctx.rtl = params.rtl;\n\n        for (const el of ctx.ref.el.querySelectorAll(params.elements)) {\n            el.addEventListener(\"pointerenter\", onElementPointerEnter);\n            el.addEventListener(\"pointerleave\", onElementPointerLeave);\n            addEffectCleanup(() => {\n                el.removeEventListener(\"pointerenter\", onElementPointerEnter);\n                el.removeEventListener(\"pointerleave\", onElementPointerLeave);\n            });\n        }\n\n        handles.start.className = `${params.handles} ${HANDLE_CLASS_START}`;\n        handles.start.style.cursor = `${params.rtl ? \"e\" : \"w\"}-resize`;\n\n        handles.end.className = `${params.handles} ${HANDLE_CLASS_END}`;\n        handles.end.style.cursor = `${params.rtl ? \"w\" : \"e\"}-resize`;\n\n        // Override \"full\" and \"element\" selectors: we want the draggable feature\n        // to apply to the handles\n        ctx.pillSelector = ctx.elementSelector;\n        ctx.fullSelector = ctx.elementSelector = `.${params.handles}`;\n\n        // Force the handles to stay in place\n        ctx.followCursor = false;\n    },\n    onDragStart({ ctx, addStyle }) {\n        addStyle(ctx.current.pill, { zIndex: 15 });\n        return { pill: ctx.current.pill };\n    },\n    onDrag({ ctx, addStyle, getRect }) {\n        const { cellSelector, current, hoveredCell, pointer, precision, rtl, ref } = ctx;\n        let { el: cell, part } = hoveredCell;\n\n        const point = [pointer.x, current.initialPosition.y];\n        if (!cell) {\n            let rect;\n            cell = document.elementsFromPoint(...point).find((el) => el.matches(cellSelector));\n            if (!cell) {\n                const cells = Array.from(ref.el.querySelectorAll(\".o_gantt_cells .o_gantt_cell\"));\n                if (pointer.x < current.initialPosition.x) {\n                    cell = rtl ? cells.at(-1) : cells[0];\n                } else {\n                    cell = rtl ? cells[0] : cells.at(-1);\n                }\n                rect = getRect(cell);\n                point[0] = rtl ? rect.right - 1 : rect.left + 1;\n            } else {\n                rect = getRect(cell);\n            }\n            const x = Math.floor(rect.x);\n            const width = Math.floor(rect.width);\n            part = Math.floor((point[0] - x) / (width / precision));\n        }\n\n        const cellStyle = getComputedStyle(cell);\n        const cGridColStart = getColumnStart(cellStyle);\n\n        const { x, width } = getRect(cell);\n        const coef = ((rtl ? -1 : 1) * width) / precision;\n        const startBorder = (rtl ? x + width : x) + part * coef;\n        const endBorder = startBorder + coef;\n\n        const theClosest = closest(point[0], [startBorder, endBorder]);\n\n        let diff =\n            cGridColStart +\n            part +\n            (theClosest === startBorder ? 0 : 1) -\n            (current.isStart ? current.firstCol : current.lastCol);\n\n        if (diff === current.lastDiff) {\n            return;\n        }\n\n        if (current.isStart) {\n            diff = Math.min(diff, current.initialDiff - 1);\n            addStyle(current.pill, { \"grid-column-start\": `c${current.firstCol + diff}` });\n        } else {\n            diff = Math.max(diff, 1 - current.initialDiff);\n            addStyle(current.pill, { \"grid-column-end\": `c${current.lastCol + diff}` });\n        }\n        current.lastDiff = diff;\n\n        const isLeftHandle = rtl ? !current.isStart : current.isStart;\n        const grabbedHandle = isLeftHandle ? \"left\" : \"right\";\n        diff = current.isStart ? -diff : diff;\n        return { pill: current.pill, grabbedHandle, diff };\n    },\n    onDragEnd({ ctx }) {\n        const { current, pillSelector } = ctx;\n        const pill = current.element.closest(pillSelector);\n        return { pill };\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n\n        if (!current.lastDiff) {\n            return;\n        }\n\n        const direction = current.isStart ? \"start\" : \"end\";\n        return { pill: current.pill, diff: current.lastDiff, direction };\n    },\n    onWillStartDrag({ ctx, addClass }) {\n        const { current, pillSelector } = ctx;\n\n        const pill = ctx.current.element.closest(pillSelector);\n        current.pill = pill;\n\n        const pillStyle = getComputedStyle(pill);\n        current.firstCol = getColumnStart(pillStyle);\n        current.lastCol = getColumnEnd(pillStyle);\n        current.initialDiff = current.lastCol - current.firstCol;\n\n        ctx.cursor = getComputedStyle(current.element).cursor;\n\n        current.isStart = current.element.classList.contains(HANDLE_CLASS_START);\n\n        addClass(ctx.ref.el, \"pe-auto\");\n    },\n});\n\nfunction getCellsOnRow(refEl, rowId) {\n    return refEl.querySelectorAll(\n        `.o_gantt_cell:not(.o_gantt_group)[data-row-id='${CSS.escape(rowId)}']`\n    );\n}\n\nfunction getMinMax(a, b) {\n    return a <= b ? [a, b] : [b, a];\n}\n\nexport const useGanttSelectable = makeDraggableHook({\n    name: \"useGanttSelectable\",\n    acceptedParams: {\n        hoveredCell: [Object],\n        rtl: [Boolean, Function],\n    },\n    onComputeParams({ ctx, params }) {\n        ctx.followCursor = false;\n        ctx.hoveredCell = params.hoveredCell;\n        ctx.rtl = params.rtl;\n    },\n    onDrag({ ctx, addClass, getRect, removeClass }) {\n        const { current, hoveredCell, pointer, ref, rtl } = ctx;\n        let { el: cell } = hoveredCell;\n        if (!cell) {\n            const point = [pointer.x, current.initialPosition.y];\n            cell = document.elementsFromPoint(...point).find((el) => el.matches(\".o_gantt_cell\"));\n            if (!cell) {\n                const cells = Array.from(ref.el.querySelectorAll(\".o_gantt_cells .o_gantt_cell\"));\n                if (pointer.x < current.initialPosition.x) {\n                    cell = rtl ? cells.at(-1) : cells[0];\n                } else {\n                    cell = rtl ? cells[0] : cells.at(-1);\n                }\n            }\n        }\n        const col = +cell.dataset.col;\n        const lastSelectedCol = current.lastSelectedCol;\n        current.lastSelectedCol = col;\n        if (lastSelectedCol === col) {\n            return;\n        }\n        const [startCol, stopCol] = getMinMax(current.initialCol, col);\n        for (const cell of getCellsOnRow(ref.el, current.rowId)) {\n            const cellCol = +cell.dataset.col;\n            if (cellCol < startCol || cellCol > stopCol) {\n                removeClass(cell, \"o_drag_hover\");\n            } else {\n                addClass(cell, \"o_drag_hover\");\n            }\n        }\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n        const { rowId, initialCol, lastSelectedCol } = current;\n        const [startCol, stopCol] = getMinMax(initialCol, lastSelectedCol);\n        return { rowId, startCol, stopCol };\n    },\n    onWillStartDrag({ ctx, addClass }) {\n        const { current, hoveredCell, ref } = ctx;\n        const { el: cell } = hoveredCell;\n        current.rowId = cell.dataset.rowId;\n        current.initialCol = +cell.dataset.col;\n        addClass(ref.el, \"pe-auto\");\n        addClass(cell, \"pe-auto\");\n    },\n});\n\n/**\n * Same as usePopover, but replaces the popover by a dialog when display size is small.\n *\n * @param {typeof import(\"@odoo/owl\").Component} component\n * @param {import(\"@web/core/popover/popover_service\").PopoverServiceAddOptions} [options]\n * @returns {import(\"@web/core/popover/popover_hook\").PopoverHookReturnType}\n */\nexport function useGanttResponsivePopover(dialogTitle, component, options = {}) {\n    const dialogService = useService(\"dialog\");\n    const env = useEnv();\n    const owner = useComponent();\n    const popover = usePopover(component, options);\n    const onClose = () => {\n        if (status(owner) !== \"destroyed\") {\n            options.onClose?.();\n        }\n    };\n    const dialogAddFn = (_, comp, props, options) => dialogService.add(comp, props, options);\n    const popoverInDialog = makePopover(dialogAddFn, GanttPopoverInDialog, { onClose });\n    const ganttReponsivePopover = {\n        open: (target, props) => {\n            if (env.isSmall) {\n                popoverInDialog.open(target, {\n                    component: component,\n                    componentProps: props,\n                    dialogTitle,\n                });\n            } else {\n                popover.open(target, props);\n            }\n        },\n        close: () => {\n            popover.close();\n            popoverInDialog.close();\n        },\n        get isOpen() {\n            return popover.isOpen || popoverInDialog.isOpen;\n        },\n    };\n    onWillUnmount(ganttReponsivePopover.close);\n    return ganttReponsivePopover;\n}\n", "import { registry } from \"@web/core/registry\";\n\nfunction _mockGetGanttData(_, { model, kwargs }) {\n    const lazy = !kwargs.limit && !kwargs.offset && kwargs.groupby.length === 1;\n    const { groups, length } = this.mockWebReadGroup(model, {\n        ...kwargs,\n        lazy,\n        fields: [\"__record_ids:array_agg(id)\"],\n    });\n\n    const recordIds = [];\n    for (const group of groups) {\n        recordIds.push(...(group.__record_ids || []));\n    }\n\n    const { records } = this.mockWebSearchReadUnity(model, [], {\n        domain: [[\"id\", \"in\", recordIds]],\n        context: kwargs.context,\n        specification: kwargs.read_specification,\n    });\n\n    const unavailabilities = {};\n    for (const fieldName of kwargs.unavailability_fields || []) {\n        unavailabilities[fieldName] = {};\n    }\n\n    const progress_bars = {};\n    for (const fieldName of kwargs.progress_bar_fields || []) {\n        progress_bars[fieldName] = {};\n    }\n\n    return { groups, length, records, unavailabilities, progress_bars };\n}\n\nregistry.category(\"mock_server\").add(\"get_gantt_data\", _mockGetGanttData);\n", "import { browser } from \"@web/core/browser/browser\";\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    deserializeDate,\n    deserializeDateTime,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { x2ManyCommands } from \"@web/core/orm_service\";\nimport { registry } from \"@web/core/registry\";\nimport { groupBy, unique } from \"@web/core/utils/arrays\";\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { sprintf } from \"@web/core/utils/strings\";\nimport { Model } from \"@web/model/model\";\nimport { formatFloatTime, formatPercentage } from \"@web/views/fields/formatters\";\nimport { getRangeFromDate, localStartOf } from \"./gantt_helpers\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {luxon.DateTime} DateTime\n * @typedef {`[{${string}}]`} RowId\n * @typedef {import(\"./gantt_arch_parser\").Scale} Scale\n * @typedef {import(\"./gantt_arch_parser\").ScaleId} ScaleId\n *\n * @typedef ConsolidationParams\n * @property {string} excludeField\n * @property {string} field\n * @property {string} [maxField]\n * @property {string} [maxValue]\n *\n * @typedef Data\n * @property {Record<string, any>[]} records\n * @property {Row[]} rows\n *\n * @typedef Field\n * @property {string} name\n * @property {string} type\n * @property {[any, string][]} [selection]\n *\n * @typedef MetaData\n * @property {ConsolidationParams} consolidationParams\n * @property {string} dateStartField\n * @property {string} dateStopField\n * @property {string[]} decorationFields\n * @property {ScaleId} defaultScale\n * @property {string} dependencyField\n * @property {boolean} dynamicRange\n * @property {Record<string, Field>} fields\n * @property {DateTime} focusDate\n * @property {number | false} formViewId\n * @property {string[]} groupedBy\n * @property {Element | null} popoverTemplate\n * @property {string} resModel\n * @property {Scale} scale\n * @property {Scale[]} scales\n * @property {DateTime} startDate\n * @property {DateTime} stopDate\n *\n * @typedef ProgressBar\n * @property {number} value_formatted\n * @property {number} max_value_formatted\n * @property {number} ratio\n * @property {string} warning\n *\n * @typedef Row\n * @property {RowId} id\n * @property {boolean} consolidate\n * @property {boolean} fromServer\n * @property {string[]} groupedBy\n * @property {string} groupedByField\n * @property {number} groupLevel\n * @property {string} name\n * @property {number[]} recordIds\n * @property {ProgressBar} [progressBar]\n * @property {number | false} resId\n * @property {Row[]} [rows]\n */\n\nfunction firstColumnBefore(date, unit) {\n    return localStartOf(date, unit);\n}\n\nfunction firstColumnAfter(date, unit) {\n    const start = localStartOf(date, unit);\n    if (date.equals(start)) {\n        return date;\n    }\n    return start.plus({ [unit]: 1 });\n}\n\n/**\n * @param {Record<string, Field>} fields\n * @param {Record<string, any>} values\n */\nexport function parseServerValues(fields, values) {\n    /** @type {Record<string, any>} */\n    const parsedValues = {};\n    if (!values) {\n        return parsedValues;\n    }\n    for (const fieldName in values) {\n        const field = fields[fieldName];\n        const value = values[fieldName];\n        switch (field.type) {\n            case \"date\": {\n                parsedValues[fieldName] = value ? deserializeDate(value) : false;\n                break;\n            }\n            case \"datetime\": {\n                parsedValues[fieldName] = value ? deserializeDateTime(value) : false;\n                break;\n            }\n            case \"selection\": {\n                if (value === false) {\n                    // process selection: convert false to 0, if 0 is a valid key\n                    const hasKey0 = field.selection.some((option) => option[0] === 0);\n                    parsedValues[fieldName] = hasKey0 ? 0 : value;\n                } else {\n                    parsedValues[fieldName] = value;\n                }\n                break;\n            }\n            case \"many2one\": {\n                parsedValues[fieldName] = value ? [value.id, value.display_name] : false;\n                break;\n            }\n            default: {\n                parsedValues[fieldName] = value;\n            }\n        }\n    }\n    return parsedValues;\n}\n\nexport class GanttModel extends Model {\n    static services = [\"notification\"];\n\n    setup(params, services) {\n        this.notification = services.notification;\n\n        /** @type {Data} */\n        this.data = {};\n        /** @type {MetaData} */\n        this.metaData = params.metaData;\n        this.displayParams = params.displayParams;\n\n        this.searchParams = null;\n\n        /** @type {Set<RowId>} */\n        this.closedRows = new Set();\n\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        /** @type {MetaData | null} */\n        this._nextMetaData = null;\n    }\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    async load(searchParams) {\n        this.searchParams = searchParams;\n\n        const metaData = this._buildMetaData();\n\n        const params = {\n            groupedBy: this._getGroupedBy(metaData, searchParams),\n            pagerOffset: 0,\n        };\n\n        if (!metaData.scale || !metaData.startDate || !metaData.stopDate) {\n            Object.assign(\n                params,\n                this._getInitialRangeParams(this._buildMetaData(params), searchParams)\n            );\n        }\n\n        await this._fetchData(this._buildMetaData(params));\n    }\n\n    //-------------------------------------------------------------------------\n    // Public\n    //-------------------------------------------------------------------------\n\n    collapseRows() {\n        const collapse = (rows) => {\n            for (const row of rows) {\n                this.closedRows.add(row.id);\n                if (row.rows) {\n                    collapse(row.rows);\n                }\n            }\n        };\n        collapse(this.data.rows);\n        this.notify();\n    }\n\n    /**\n     * Create a copy of a task with defaults determined by schedule.\n     *\n     * @param {number} id\n     * @param {Record<string, any>} schedule\n     * @param {(result: any) => any} [callback]\n     */\n    copy(id, schedule, callback) {\n        const { resModel } = this.metaData;\n        const { context } = this.searchParams;\n        const data = this._scheduleToData(schedule);\n        return this.mutex.exec(async () => {\n            const result = await this.orm.call(resModel, \"copy\", [[id]], {\n                context,\n                default: data,\n            });\n            if (callback) {\n                callback(result[0]);\n            }\n            this.fetchData();\n        });\n    }\n\n    /**\n     * Adds a dependency between masterId and slaveId (slaveId depends\n     * on masterId).\n     *\n     * @param {number} masterId\n     * @param {number} slaveId\n     */\n    async createDependency(masterId, slaveId) {\n        const { dependencyField, resModel } = this.metaData;\n        const writeCommand = {\n            [dependencyField]: [x2ManyCommands.link(masterId)],\n        };\n        await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));\n        await this.fetchData();\n    }\n\n    dateStartFieldIsDate(metaData = this.metaData) {\n        return metaData?.fields[metaData.dateStartField].type === \"date\";\n    }\n\n    dateStopFieldIsDate(metaData = this.metaData) {\n        return metaData?.fields[metaData.dateStopField].type === \"date\";\n    }\n\n    expandRows() {\n        this.closedRows.clear();\n        this.notify();\n    }\n\n    async fetchData(params) {\n        await this._fetchData(this._buildMetaData(params));\n        this.useSampleModel = false;\n        this.notify();\n    }\n\n    /**\n     * @param {Object} params\n     * @param {RowId} [params.rowId]\n     * @param {DateTime} [params.start]\n     * @param {DateTime} [params.stop]\n     * @param {boolean} [params.withDefault]\n     * @returns {Record<string, any>}\n     */\n    getDialogContext(params) {\n        /** @type {Record<string, any>} */\n        const context = { ...this.getSchedule(params) };\n\n        if (params.withDefault) {\n            for (const k in context) {\n                context[sprintf(\"default_%s\", k)] = context[k];\n            }\n        }\n\n        return Object.assign({}, this.searchParams.context, context);\n    }\n\n    /**\n     * @param {Object} params\n     * @param {RowId} [params.rowId]\n     * @param {DateTime} [params.start]\n     * @param {DateTime} [params.stop]\n     * @returns {Record<string, any>}\n     */\n    getSchedule({ rowId, start, stop } = {}) {\n        const { dateStartField, dateStopField, fields, groupedBy } = this.metaData;\n\n        /** @type {Record<string, any>} */\n        const schedule = {};\n\n        if (start) {\n            schedule[dateStartField] = this.dateStartFieldIsDate()\n                ? serializeDate(start)\n                : serializeDateTime(start);\n        }\n        if (stop && dateStartField !== dateStopField) {\n            schedule[dateStopField] = this.dateStopFieldIsDate()\n                ? serializeDate(stop)\n                : serializeDateTime(stop);\n        }\n        if (rowId) {\n            const group = Object.assign({}, ...JSON.parse(rowId));\n            for (const fieldName of groupedBy) {\n                if (fieldName in group) {\n                    const value = group[fieldName];\n                    if (Array.isArray(value)) {\n                        const { type } = fields[fieldName];\n                        schedule[fieldName] = type === \"many2many\" ? [value[0]] : value[0];\n                    } else {\n                        schedule[fieldName] = value;\n                    }\n                }\n            }\n        }\n\n        return schedule;\n    }\n\n    /**\n     * @override\n     * @returns {boolean}\n     */\n    hasData() {\n        return Boolean(this.data.records.length);\n    }\n\n    /**\n     * @param {RowId} rowId\n     * @returns {boolean}\n     */\n    isClosed(rowId) {\n        return this.closedRows.has(rowId);\n    }\n\n    /**\n     * Removes the dependency between masterId and slaveId (slaveId is no\n     * more dependent on masterId).\n     *\n     * @param {number} masterId\n     * @param {number} slaveId\n     */\n    async removeDependency(masterId, slaveId) {\n        const { dependencyField, resModel } = this.metaData;\n        const writeCommand = {\n            [dependencyField]: [x2ManyCommands.unlink(masterId)],\n        };\n        await this.mutex.exec(() => this.orm.write(resModel, [slaveId], writeCommand));\n        await this.fetchData();\n    }\n\n    /**\n     * Removes from 'data' the fields holding the same value as the records targetted\n     * by 'ids'.\n     *\n     * @template {Record<string, any>} T\n     * @param {T} data\n     * @param {number[]} ids\n     * @returns {Partial<T>}\n     */\n    removeRedundantData(data, ids) {\n        const records = this.data.records.filter((rec) => ids.includes(rec.id));\n        if (!records.length) {\n            return data;\n        }\n\n        /**\n         *\n         * @param {Record<string, any>} record\n         * @param {Field} field\n         */\n        const isSameValue = (record, { name, type }) => {\n            const recordValue = record[name];\n            let newValue = data[name];\n            if (Array.isArray(newValue)) {\n                [newValue] = newValue;\n            }\n            if (Array.isArray(recordValue)) {\n                if (type === \"many2many\") {\n                    return recordValue.includes(newValue);\n                } else {\n                    return recordValue[0] === newValue;\n                }\n            } else if (type === \"date\") {\n                return serializeDate(recordValue) === newValue;\n            } else if (type === \"datetime\") {\n                return serializeDateTime(recordValue) === newValue;\n            } else {\n                return recordValue === newValue;\n            }\n        };\n\n        /** @type {Partial<T>} */\n        const trimmed = { ...data };\n\n        for (const fieldName in data) {\n            const field = this.metaData.fields[fieldName];\n            if (records.every((rec) => isSameValue(rec, field))) {\n                // All the records already have the given value.\n                delete trimmed[fieldName];\n            }\n        }\n\n        return trimmed;\n    }\n\n    /**\n     * Reschedule a task to the given schedule.\n     *\n     * @param {number | number[]} ids\n     * @param {Record<string, any>} schedule\n     * @param {(result: any) => any} [callback]\n     */\n    async reschedule(ids, schedule, callback) {\n        if (!Array.isArray(ids)) {\n            ids = [ids];\n        }\n        const allData = this._scheduleToData(schedule);\n        const data = this.removeRedundantData(allData, ids);\n        const context = this._getRescheduleContext();\n        return this.mutex.exec(async () => {\n            try {\n                const result = await this._reschedule(ids, data, context);\n                if (callback) {\n                    await callback(result);\n                }\n            } finally {\n                this.fetchData();\n            }\n        });\n    }\n\n    async _reschedule(ids, data, context) {\n        return this.orm.write(this.metaData.resModel, ids, data, {\n            context,\n        });\n    }\n\n    toggleHighlightPlannedFilter(ids) {}\n\n    /**\n     * Reschedule masterId or slaveId according to the direction\n     *\n     * @param {\"forward\" | \"backward\"} direction\n     * @param {number} masterId\n     * @param {number} slaveId\n     * @returns {Promise<any>}\n     */\n    async rescheduleAccordingToDependency(\n        direction,\n        masterId,\n        slaveId,\n        rescheduleAccordingToDependencyCallback\n    ) {\n        const {\n            dateStartField,\n            dateStopField,\n            dependencyField,\n            dependencyInvertedField,\n            resModel,\n        } = this.metaData;\n\n        return await this.mutex.exec(async () => {\n            try {\n                const result = await this.orm.call(resModel, \"web_gantt_reschedule\", [\n                    direction,\n                    masterId,\n                    slaveId,\n                    dependencyField,\n                    dependencyInvertedField,\n                    dateStartField,\n                    dateStopField,\n                ]);\n                if (rescheduleAccordingToDependencyCallback) {\n                    await rescheduleAccordingToDependencyCallback(result);\n                }\n            } finally {\n                this.fetchData();\n            }\n        });\n    }\n\n    /**\n     * @param {string} rowId\n     */\n    toggleRow(rowId) {\n        if (this.isClosed(rowId)) {\n            this.closedRows.delete(rowId);\n        } else {\n            this.closedRows.add(rowId);\n        }\n        this.notify();\n    }\n\n    async toggleDisplayMode() {\n        this.displayParams.displayMode =\n            this.displayParams.displayMode === \"dense\" ? \"sparse\" : \"dense\";\n        this.notify();\n    }\n\n    async updatePagerParams({ limit, offset }) {\n        await this.fetchData({ pagerLimit: limit, pagerOffset: offset });\n    }\n\n    //-------------------------------------------------------------------------\n    // Protected\n    //-------------------------------------------------------------------------\n\n    /**\n     * Return a copy of this.metaData or of the last copy, extended with optional\n     * params. This is useful for async methods that need to modify this.metaData,\n     * but it can't be done in place directly for the model to be concurrency\n     * proof (so they work on a copy and commit it at the end).\n     *\n     * @protected\n     * @param {Object} params\n     * @param {DateTime} [params.focusDate]\n     * @param {DateTime} [params.startDate]\n     * @param {DateTime} [params.stopDate]\n     * @param {string[]} [params.groupedBy]\n     * @param {ScaleId} [params.scaleId]\n     * @returns {MetaData}\n     */\n    _buildMetaData(params = {}) {\n        this._nextMetaData = { ...(this._nextMetaData || this.metaData) };\n\n        if (params.groupedBy) {\n            this._nextMetaData.groupedBy = params.groupedBy;\n        }\n        if (params.scaleId) {\n            browser.localStorage.setItem(this._getLocalStorageKey(), params.scaleId);\n            this._nextMetaData.scale = { ...this._nextMetaData.scales[params.scaleId] };\n        }\n        if (params.focusDate) {\n            this._nextMetaData.focusDate = params.focusDate;\n        }\n        if (params.startDate) {\n            this._nextMetaData.startDate = params.startDate;\n        }\n        if (params.stopDate) {\n            this._nextMetaData.stopDate = params.stopDate;\n        }\n        if (params.rangeId) {\n            this._nextMetaData.rangeId = params.rangeId;\n        }\n\n        if (\"pagerLimit\" in params) {\n            this._nextMetaData.pagerLimit = params.pagerLimit;\n        }\n        if (\"pagerOffset\" in params) {\n            this._nextMetaData.pagerOffset = params.pagerOffset;\n        }\n\n        if (\"scaleId\" in params || \"startDate\" in params || \"stopDate\" in params) {\n            // we assume that scale, startDate, and stopDate are already set in this._nextMetaData\n\n            let exchange = false;\n            if (this._nextMetaData.startDate > this._nextMetaData.stopDate) {\n                exchange = true;\n                const temp = this._nextMetaData.startDate;\n                this._nextMetaData.startDate = this._nextMetaData.stopDate;\n                this._nextMetaData.stopDate = temp;\n            }\n            const { interval } = this._nextMetaData.scale;\n\n            const rightLimit = this._nextMetaData.startDate.plus({ year: 10, day: -1 });\n            if (this._nextMetaData.stopDate > rightLimit) {\n                if (exchange) {\n                    this._nextMetaData.startDate = this._nextMetaData.stopDate.minus({\n                        year: 10,\n                        day: -1,\n                    });\n                } else {\n                    this._nextMetaData.stopDate = this._nextMetaData.startDate.plus({\n                        year: 10,\n                        day: -1,\n                    });\n                }\n            }\n            this._nextMetaData.globalStart = firstColumnBefore(\n                this._nextMetaData.startDate,\n                interval\n            );\n            this._nextMetaData.globalStop = firstColumnAfter(\n                this._nextMetaData.stopDate.plus({ day: 1 }),\n                interval\n            );\n\n            if (params.currentFocusDate) {\n                this._nextMetaData.focusDate = params.currentFocusDate;\n                if (this._nextMetaData.focusDate < this._nextMetaData.startDate) {\n                    this._nextMetaData.focusDate = this._nextMetaData.startDate;\n                } else if (this._nextMetaData.stopDate < this._nextMetaData.focusDate) {\n                    this._nextMetaData.focusDate = this._nextMetaData.stopDate;\n                }\n            }\n        }\n\n        return this._nextMetaData;\n    }\n\n    /**\n     * Fetches records to display (and groups if necessary).\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Object} [additionalContext]\n     */\n    async _fetchData(metaData, additionalContext) {\n        const { globalStart, globalStop, groupedBy, pagerLimit, pagerOffset, resModel, scale } =\n            metaData;\n        const context = {\n            ...this.searchParams.context,\n            group_by: groupedBy,\n            ...additionalContext,\n        };\n        const domain = this._getDomain(metaData);\n        const fields = this._getFields(metaData);\n        const specification = {};\n        for (const fieldName of fields) {\n            specification[fieldName] = {};\n            if (metaData.fields[fieldName].type === \"many2one\") {\n                specification[fieldName].fields = { display_name: {} };\n            }\n        }\n\n        const { length, groups, records, progress_bars, unavailabilities } =\n            await this.keepLast.add(\n                this.orm.call(resModel, \"get_gantt_data\", [], {\n                    domain,\n                    groupby: groupedBy,\n                    read_specification: specification,\n                    scale: scale.unit,\n                    start_date: serializeDateTime(globalStart),\n                    stop_date: serializeDateTime(globalStop),\n                    unavailability_fields: this._getUnavailabilityFields(metaData),\n                    progress_bar_fields: this._getProgressBarFields(metaData),\n                    context,\n                    limit: pagerLimit,\n                    offset: pagerOffset,\n                })\n            );\n\n        groups.forEach((g) => (g.fromServer = true));\n\n        const data = { count: length };\n\n        data.records = this._parseServerData(metaData, records);\n        data.rows = this._generateRows(metaData, {\n            groupedBy,\n            groups,\n            parentGroup: [],\n        });\n        data.unavailabilities = this._processUnavailabilities(unavailabilities);\n        data.progressBars = this._processProgressBars(progress_bars);\n\n        await this.keepLast.add(this._fetchDataPostProcess(metaData, data));\n\n        this.data = data;\n        this.metaData = metaData;\n        this._nextMetaData = null;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Data} data\n     */\n    async _fetchDataPostProcess(metaData, data) {}\n\n    /**\n     * Remove date in groupedBy field\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {string[]} groupedBy\n     * @returns {string[]}\n     */\n    _filterDateIngroupedBy(metaData, groupedBy) {\n        return groupedBy.filter((gb) => {\n            const [fieldName] = gb.split(\":\");\n            const { type } = metaData.fields[fieldName];\n            return ![\"date\", \"datetime\"].includes(type);\n        });\n    }\n\n    /**\n     * @protected\n     * @param {number} floatVal\n     * @param {string}\n     */\n    _formatTime(floatVal) {\n        const timeStr = formatFloatTime(floatVal, { noLeadingZeroHour: true });\n        const [hourStr, minuteStr] = timeStr.split(\":\");\n        const hour = parseInt(hourStr, 10);\n        const minute = parseInt(minuteStr, 10);\n        return minute ? _t(\"%(hour)sh%(minute)s\", { hour, minute }) : _t(\"%sh\", hour);\n    }\n\n    /**\n     * Process groups to generate a recursive structure according\n     * to groupedBy fields. Note that there might be empty groups (filled by\n     * read_goup with group_expand) that also need to be processed.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Object} params\n     * @param {Object[]} params.groups\n     * @param {string[]} params.groupedBy\n     * @param {Object[]} params.parentGroup\n     * @returns {Row[]}\n     */\n    _generateRows(metaData, params) {\n        const groupedBy = params.groupedBy;\n        const groups = params.groups;\n        const groupLevel = metaData.groupedBy.length - groupedBy.length;\n        const parentGroup = params.parentGroup;\n\n        if (!groupedBy.length || !groups.length) {\n            const recordIds = [];\n            for (const g of groups) {\n                recordIds.push(...(g.__record_ids || []));\n            }\n            const part = parentGroup.at(-1);\n            const [[parentGroupedField, value]] = part ? Object.entries(part) : [[]];\n            return [\n                {\n                    groupLevel,\n                    id: JSON.stringify([...parentGroup, {}]),\n                    name: \"\",\n                    recordIds: unique(recordIds),\n                    parentGroupedField,\n                    parentResId: Array.isArray(value) ? value[0] : value,\n                    __extra__: true,\n                },\n            ];\n        }\n\n        /** @type {Row[]} */\n        const rows = [];\n\n        // Some groups might be empty (thanks to expand_groups), so we can't\n        // simply group the data, we need to keep all returned groups\n        const groupedByField = groupedBy[0];\n        const currentLevelGroups = groupBy(groups, (g) => {\n            if (g[groupedByField] === undefined) {\n                // we want to group the groups with undefined values for groupedByField with the ones\n                // with false value for the same field.\n                // we also want to be sure that stringification keeps groupedByField:\n                // JSON.stringify({ key: undefined }) === \"{}\"\n                // see construction of id below.\n                g[groupedByField] = false;\n            }\n            return g[groupedByField];\n        });\n        const { maxField } = metaData.consolidationParams;\n        const consolidate = groupLevel === 0 && groupedByField === maxField;\n        const generateSubRow = maxField ? true : groupedBy.length > 1;\n        for (const key in currentLevelGroups) {\n            const subGroups = currentLevelGroups[key];\n            const value = subGroups[0][groupedByField];\n            const part = {};\n            part[groupedByField] = value;\n            const fakeGroup = [...parentGroup, part];\n            const id = JSON.stringify(fakeGroup);\n            const resId = Array.isArray(value) ? value[0] : value; // not really a resId\n            const fromServer = subGroups.some((g) => g.fromServer);\n            const recordIds = [];\n            for (const g of subGroups) {\n                recordIds.push(...(g.__record_ids || []));\n            }\n            const row = {\n                consolidate,\n                fromServer,\n                groupedBy,\n                groupedByField,\n                groupLevel,\n                id,\n                name: this._getRowName(metaData, groupedByField, value),\n                resId, // not really a resId\n                recordIds: unique(recordIds),\n            };\n            if (generateSubRow) {\n                row.rows = this._generateRows(metaData, {\n                    ...params,\n                    groupedBy: groupedBy.slice(1),\n                    groups: subGroups,\n                    parentGroup: fakeGroup,\n                });\n            }\n            if (resId === false) {\n                rows.unshift(row);\n            } else {\n                rows.push(row);\n            }\n        }\n\n        return rows;\n    }\n\n    /**\n     * Get domain of records to display in the gantt view.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {any[]}\n     */\n    _getDomain(metaData) {\n        const { dateStartField, dateStopField, globalStart, globalStop } = metaData;\n        const domain = Domain.and([\n            this.searchParams.domain,\n            [\n                \"&\",\n                [\n                    dateStartField,\n                    \"<\",\n                    this.dateStopFieldIsDate(metaData)\n                        ? serializeDate(globalStop)\n                        : serializeDateTime(globalStop),\n                ],\n                [\n                    dateStopField,\n                    this.dateStartFieldIsDate(metaData) ? \">=\" : \">\",\n                    this.dateStartFieldIsDate(metaData)\n                        ? serializeDate(globalStart)\n                        : serializeDateTime(globalStart),\n                ],\n            ],\n        ]);\n        return domain.toList();\n    }\n\n    /**\n     * Format field value to display purpose.\n     *\n     * @protected\n     * @param {any} value\n     * @param {Object} field\n     * @returns {string} formatted field value\n     */\n    _getFieldFormattedValue(value, field) {\n        if (field.type === \"boolean\") {\n            return value ? \"True\" : \"False\";\n        } else if (!value) {\n            return _t(\"Undefined %s\", field.string);\n        } else if (field.type === \"many2many\") {\n            return value[1];\n        }\n        const formatter = registry.category(\"formatters\").get(field.type);\n        return formatter(value, field);\n    }\n\n    /**\n     * Get all the fields needed.\n     *\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {string[]}\n     */\n    _getFields(metaData) {\n        const fields = new Set([\n            \"display_name\",\n            metaData.dateStartField,\n            metaData.dateStopField,\n            ...metaData.groupedBy,\n            ...metaData.decorationFields,\n        ]);\n        if (metaData.colorField) {\n            fields.add(metaData.colorField);\n        }\n        if (metaData.consolidationParams.field) {\n            fields.add(metaData.consolidationParams.field);\n        }\n        if (metaData.consolidationParams.excludeField) {\n            fields.add(metaData.consolidationParams.excludeField);\n        }\n        if (metaData.dependencyField) {\n            fields.add(metaData.dependencyField);\n        }\n        if (metaData.progressField) {\n            fields.add(metaData.progressField);\n        }\n        return [...fields];\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {{ groupBy: string[] }} searchParams\n     * @returns {string[]}\n     */\n    _getGroupedBy(metaData, searchParams) {\n        let groupedBy = [...searchParams.groupBy];\n        groupedBy = groupedBy.filter((gb) => {\n            const [fieldName] = gb.split(\".\");\n            const field = metaData.fields[fieldName];\n            return field?.type !== \"properties\";\n        });\n        groupedBy = this._filterDateIngroupedBy(metaData, groupedBy);\n        if (!groupedBy.length) {\n            groupedBy = metaData.defaultGroupBy;\n        }\n        return groupedBy;\n    }\n\n    _getDefaultFocusDate(metaData, searchParams, scaleId) {\n        const { context } = searchParams;\n        let focusDate =\n            \"initialDate\" in context ? deserializeDateTime(context.initialDate) : DateTime.local();\n        focusDate = focusDate.startOf(\"day\");\n        if (metaData.offset) {\n            const { unit } = metaData.scales[scaleId];\n            focusDate = focusDate.plus({ [unit]: metaData.offset });\n        }\n        return focusDate;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {{ context: Record<string, any> }} searchParams\n     * @returns {{ focusDate: DateTime, scaleId: ScaleId, startDate: DateTime, stopDate: DateTime }}\n     */\n    _getInitialRangeParams(metaData, searchParams) {\n        const { context } = searchParams;\n        const localScaleId = this._getScaleIdFromLocalStorage(metaData);\n        /** @type {ScaleId} */\n        const scaleId = localScaleId || context.default_scale || metaData.defaultScale;\n        const { defaultRange } = metaData.scales[scaleId];\n\n        const rangeId =\n            context.default_range in metaData.ranges\n                ? context.range_type\n                : metaData.defaultRange || \"custom\";\n        let focusDate;\n        if (rangeId in metaData.ranges) {\n            focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);\n            return { scaleId, ...getRangeFromDate(rangeId, focusDate) };\n        }\n        let startDate = context.default_start_date && deserializeDate(context.default_start_date);\n        let stopDate = context.default_stop_date && deserializeDate(context.default_stop_date);\n        if (!startDate && !stopDate) {\n            /** @type {DateTime} */\n            focusDate = this._getDefaultFocusDate(metaData, searchParams, scaleId);\n            startDate = firstColumnBefore(focusDate, defaultRange.unit);\n            stopDate = startDate\n                .plus({ [defaultRange.unit]: defaultRange.count })\n                .minus({ day: 1 });\n        } else if (startDate && !stopDate) {\n            const column = firstColumnBefore(startDate, defaultRange.unit);\n            focusDate = startDate;\n            stopDate = column.plus({ [defaultRange.unit]: defaultRange.count }).minus({ day: 1 });\n        } else if (!startDate && stopDate) {\n            const column = firstColumnAfter(stopDate, defaultRange.unit);\n            focusDate = stopDate;\n            startDate = column.minus({ [defaultRange.unit]: defaultRange.count });\n        } else {\n            focusDate = DateTime.local();\n            if (focusDate < startDate) {\n                focusDate = startDate;\n            } else if (focusDate > stopDate) {\n                focusDate = stopDate;\n            }\n        }\n\n        return { focusDate, scaleId, startDate, stopDate, rangeId };\n    }\n\n    _getLocalStorageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    _getProgressBarFields(metaData) {\n        if (metaData.progressBarFields && !this.orm.isSample) {\n            return metaData.progressBarFields.filter(\n                (fieldName) =>\n                    metaData.groupedBy.includes(fieldName) &&\n                    [\"many2many\", \"many2one\"].includes(metaData.fields[fieldName]?.type)\n            );\n        }\n        return [];\n    }\n\n    _getRescheduleContext() {\n        return { ...this.searchParams.context };\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {string} groupedByField\n     * @param {any} value\n     * @returns {string}\n     */\n    _getRowName(metaData, groupedByField, value) {\n        const field = metaData.fields[groupedByField];\n        return this._getFieldFormattedValue(value, field);\n    }\n\n    _getScaleIdFromLocalStorage(metaData) {\n        const { scales } = metaData;\n        const localScaleId = browser.localStorage.getItem(this._getLocalStorageKey());\n        return localScaleId in scales ? localScaleId : null;\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @returns {string[]}\n     */\n    _getUnavailabilityFields(metaData) {\n        if (metaData.displayUnavailability && !this.orm.isSample && metaData.groupedBy.length) {\n            const lastGroupBy = metaData.groupedBy.at(-1);\n            const { type } = metaData.fields[lastGroupBy] || {};\n            if ([\"many2many\", \"many2one\"].includes(type)) {\n                return [lastGroupBy];\n            }\n        }\n        return [];\n    }\n\n    /**\n     * @protected\n     * @param {MetaData} metaData\n     * @param {Record<string, any>[]} records the server records to parse\n     * @returns {Record<string, any>[]}\n     */\n    _parseServerData(metaData, records) {\n        const { dateStartField, dateStopField, fields, globalStart, globalStop } = metaData;\n        /** @type {Record<string, any>[]} */\n        const parsedRecords = [];\n        for (const record of records) {\n            const parsedRecord = parseServerValues(fields, record);\n            const dateStart = parsedRecord[dateStartField];\n            const dateStop = parsedRecord[dateStopField];\n            if (this.orm.isSample) {\n                // In sample mode, we want enough data to be displayed, so we\n                // swap the dates as the records are randomly generated anyway.\n                if (dateStart > dateStop) {\n                    parsedRecord[dateStartField] = dateStop;\n                    parsedRecord[dateStopField] = dateStart;\n                }\n                // Record could also be outside the displayed range since the\n                // sample server doesn't take the domain into account\n                if (parsedRecord[dateStopField] < globalStart) {\n                    parsedRecord[dateStopField] = globalStart;\n                }\n                if (parsedRecord[dateStartField] > globalStop) {\n                    parsedRecord[dateStartField] = globalStop;\n                }\n                parsedRecords.push(parsedRecord);\n            } else if (dateStart <= dateStop) {\n                parsedRecords.push(parsedRecord);\n            }\n        }\n        return parsedRecords;\n    }\n\n    _processProgressBar(progressBar, warning) {\n        const processedProgressBar = {\n            ...progressBar,\n            value_formatted: this._formatTime(progressBar.value),\n            max_value_formatted: this._formatTime(progressBar.max_value),\n            ratio: progressBar.max_value ? (progressBar.value / progressBar.max_value) * 100 : 0,\n            warning,\n        };\n        if (processedProgressBar?.max_value) {\n            processedProgressBar.ratio_formatted = formatPercentage(\n                processedProgressBar.ratio / 100\n            );\n        }\n        return processedProgressBar;\n    }\n\n    _processProgressBars(progressBars) {\n        const processedProgressBars = {};\n        for (const fieldName in progressBars) {\n            processedProgressBars[fieldName] = {};\n            const progressBarInfo = progressBars[fieldName];\n            for (const [resId, progressBar] of Object.entries(progressBarInfo)) {\n                processedProgressBars[fieldName][resId] = this._processProgressBar(\n                    progressBar,\n                    progressBarInfo.warning\n                );\n            }\n        }\n        return processedProgressBars;\n    }\n\n    _processUnavailabilities(unavailabilities) {\n        const processedUnavailabilities = {};\n        for (const fieldName in unavailabilities) {\n            processedUnavailabilities[fieldName] = {};\n            for (const [resId, resUnavailabilities] of Object.entries(\n                unavailabilities[fieldName]\n            )) {\n                processedUnavailabilities[fieldName][resId] = resUnavailabilities.map((u) => ({\n                    start: deserializeDateTime(u.start),\n                    stop: deserializeDateTime(u.stop),\n                }));\n            }\n        }\n        return processedUnavailabilities;\n    }\n\n    /**\n     * @template {Record<string, any>} T\n     * @param {T} schedule\n     * @returns {Partial<T>}\n     */\n    _scheduleToData(schedule) {\n        const allowedFields = [\n            this.metaData.dateStartField,\n            this.metaData.dateStopField,\n            ...this.metaData.groupedBy,\n        ];\n        return pick(schedule, ...allowedFields);\n    }\n}\n", "import { Component, useRef } from \"@odoo/owl\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\nimport { GanttCompiler } from \"./gantt_compiler\";\n\nexport class GanttPopover extends Component {\n    static template = \"web_gantt.GanttPopover\";\n    static components = { ViewButton };\n    static props = [\n        \"title\",\n        \"displayGenericButtons\",\n        \"bodyTemplate?\",\n        \"footerTemplate?\",\n        \"resModel\",\n        \"resId\",\n        \"context\",\n        \"close\",\n        \"reload\",\n        \"buttons\",\n    ];\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n\n        this.templates = { body: \"web_gantt.GanttPopover.default\" };\n        const toCompile = {};\n        const { bodyTemplate, footerTemplate } = this.props;\n        if (bodyTemplate) {\n            toCompile.body = bodyTemplate;\n            if (footerTemplate) {\n                toCompile.footer = footerTemplate;\n            }\n        }\n        Object.assign(\n            this.templates,\n            useViewCompiler(GanttCompiler, toCompile, { recordExpr: \"__record__\" })\n        );\n\n        useViewButtons(this.rootRef, {\n            reload: async () => {\n                await this.props.reload();\n                this.props.close();\n            },\n        });\n    }\n\n    get renderingContext() {\n        return Object.assign({}, this.props.context, {\n            __comp__: this,\n            __record__: { resModel: this.props.resModel, resId: this.props.resId },\n        });\n    }\n\n    async onClick(button) {\n        await button.onClick();\n        this.props.close();\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class GanttPopoverInDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\", \"component\", \"componentProps\", \"dialogTitle\"];\n    static template = \"web_gantt.GanttPopoverInDialog\";\n    get componentProps() {\n        return { ...this.props.componentProps, close: this.props.close };\n    }\n}\n", "import {\n    Component,\n    onWillRender,\n    onWillStart,\n    onWillUpdateProps,\n    reactive,\n    useEffect,\n    useExternalListener,\n    useRef,\n    markup,\n} from \"@odoo/owl\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Domain } from \"@web/core/domain\";\nimport {\n    getStartOfLocalWeek,\n    is24HourFormat,\n    serializeDate,\n    serializeDateTime,\n} from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { user } from \"@web/core/user\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit, pick } from \"@web/core/utils/objects\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport { url } from \"@web/core/utils/urls\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { useVirtualGrid } from \"@web/core/virtual_grid_hook\";\nimport { formatFloatTime } from \"@web/views/fields/formatters\";\nimport { SelectCreateDialog } from \"@web/views/view_dialogs/select_create_dialog\";\nimport { GanttConnector } from \"./gantt_connector\";\nimport {\n    dateAddFixedOffset,\n    diffColumn,\n    getCellColor,\n    getColorIndex,\n    localEndOf,\n    localStartOf,\n    useGanttConnectorDraggable,\n    useGanttDraggable,\n    useGanttResizable,\n    useGanttSelectable,\n    useGanttUndraggable,\n    useMultiHover,\n} from \"./gantt_helpers\";\nimport { GanttPopover } from \"./gantt_popover\";\nimport { GanttRendererControls } from \"./gantt_renderer_controls\";\nimport { GanttResizeBadge } from \"./gantt_resize_badge\";\nimport { GanttRowProgressBar } from \"./gantt_row_progress_bar\";\nimport { clamp } from \"@web/core/utils/numbers\";\n\nconst { DateTime } = luxon;\n\n/**\n * @typedef {`__column__${number}`} ColumnId\n * @typedef {`__connector__${number | \"new\"}`} ConnectorId\n * @typedef {import(\"./gantt_connector\").ConnectorProps} ConnectorProps\n * @typedef {luxon.DateTime} DateTime\n * @typedef {\"copy\" | \"reschedule\"} DragActionMode\n * @typedef {\"drag\" | \"locked\" | \"resize\"} InteractionMode\n * @typedef {`__pill__${number}`} PillId\n * @typedef {import(\"./gantt_model\").RowId} RowId\n *\n * @typedef Column\n * @property {ColumnId} id\n * @property {GridPosition} grid\n * @property {boolean} [isToday]\n * @property {DateTime} start\n * @property {DateTime} stop\n *\n * @typedef GridPosition\n * @property {number | number[]} [row]\n * @property {number | number[]} [column]\n *\n * @typedef Group\n * @property {boolean} break\n * @property {number} col\n * @property {Pill[]} pills\n * @property {number} aggregateValue\n * @property {GridPosition} grid\n *\n * @typedef GanttRendererProps\n * @property {import(\"./gantt_model\").GanttModel} model\n * @property {Document} arch\n * @property {string} class\n * @property {(context: Record<string, any>)} create\n * @property {{ content?: Point }} [scrollPosition]\n * @property {{ el: HTMLDivElement | null }} [contentRef]\n *\n * @typedef HoveredInfo\n * @property {Element | null} connector\n * @property {HTMLElement | null} hoverable\n * @property {HTMLElement | null} pill\n *\n * @typedef Interaction\n * @property {InteractionMode | null} mode\n * @property {DragActionMode} dragAction\n *\n * @typedef Pill\n * @property {PillId} id\n * @property {boolean} disableStartResize\n * @property {boolean} disableStopResize\n * @property {boolean} highlighted\n * @property {number} leftMargin\n * @property {number} level\n * @property {string} name\n * @property {DateTime} startDate\n * @property {DateTime} stopDate\n * @property {GridPosition} grid\n * @property {RelationalRecord} record\n * @property {number} _color\n * @property {number} _progress\n *\n * @typedef Point\n * @property {number} [x]\n * @property {number} [y]\n *\n * @typedef {Record<string, any>} RelationalRecord\n * @property {number | false} id\n *\n * @typedef ResizeBadge\n * @property {Point & { right?: number }} position\n * @property {number} diff\n * @property {string} scale\n *\n * @typedef {import(\"./gantt_model\").Row & {\n *  grid: GridPosition,\n *  pills: Pill[],\n *  cellColors?: Record<string, string>,\n *  thumbnailUrl?: string\n * }} Row\n *\n * @typedef SubColumn\n * @property {ColumnId} columnId\n * @property {boolean} [isToday]\n * @property {DateTime} start\n * @property {DateTime} stop\n */\n\n/** @type {[Omit<InteractionMode, \"drag\"> | DragActionMode, string][]} */\nconst INTERACTION_CLASSNAMES = [\n    [\"connect\", \"o_connect\"],\n    [\"copy\", \"o_copying\"],\n    [\"locked\", \"o_grabbing_locked\"],\n    [\"reschedule\", \"o_grabbing\"],\n    [\"resize\", \"o_resizing\"],\n];\nconst NEW_CONNECTOR_ID = \"__connector__new\";\n\n/**\n * Gantt Renderer\n *\n * @extends {Component<GanttRendererProps, any>}\n */\nexport class GanttRenderer extends Component {\n    static components = {\n        GanttConnector,\n        GanttRendererControls,\n        GanttResizeBadge,\n        GanttRowProgressBar,\n        Popover: GanttPopover,\n    };\n    static props = [\n        \"model\",\n        \"arch\",\n        \"class\",\n        \"create\",\n        \"openDialog\",\n        \"scrollPosition?\",\n        \"contentRef?\",\n    ];\n\n    static template = \"web_gantt.GanttRenderer\";\n    static connectorCreatorTemplate = \"web_gantt.GanttRenderer.ConnectorCreator\";\n    static headerTemplate = \"web_gantt.GanttRenderer.Header\";\n    static pillTemplate = \"web_gantt.GanttRenderer.Pill\";\n    static groupPillTemplate = \"web_gantt.GanttRenderer.GroupPill\";\n    static rowContentTemplate = \"web_gantt.GanttRenderer.RowContent\";\n    static rowHeaderTemplate = \"web_gantt.GanttRenderer.RowHeader\";\n    static totalRowTemplate = \"web_gantt.GanttRenderer.TotalRow\";\n\n    static getRowHeaderWidth = (width) => 100 / (width > 768 ? 6 : 3);\n\n    setup() {\n        this.model = this.props.model;\n\n        this.gridRef = useRef(\"grid\");\n        this.cellContainerRef = useRef(\"cellContainer\");\n\n        this.actionService = useService(\"action\");\n        this.dialogService = useService(\"dialog\");\n        this.notificationService = useService(\"notification\");\n\n        this.is24HourFormat = is24HourFormat();\n\n        /** @type {HoveredInfo} */\n        this.hovered = {\n            connector: null,\n            hoverable: null,\n            pill: null,\n        };\n\n        /** @type {Interaction} */\n        this.interaction = reactive(\n            {\n                mode: null,\n                dragAction: \"reschedule\",\n            },\n            () => this.onInteractionChange()\n        );\n        this.onInteractionChange(); // Used to hook into \"interaction\"\n        /** @type {Record<ConnectorId, ConnectorProps>} */\n        this.connectors = reactive({});\n        this.progressBarsReactive = reactive({ hoveredRowId: null });\n        /** @type {ResizeBadge} */\n        this.resizeBadgeReactive = reactive({});\n\n        /** @type {Object[]} */\n        this.columnsGroups = [];\n        /** @type {Column[]} */\n        this.columns = [];\n        /** @type {Pill[]} */\n        this.extraPills = [];\n        /** @type {Record<PillId, Pill>} */\n        this.pills = {}; // mapping to retrieve pills from pill ids\n        /** @type {Row[]} */\n        this.rows = [];\n        /** @type {SubColumn[]} */\n        this.subColumns = [];\n        /** @type {Record<RowId, Pill[]>} */\n        this.rowPills = {};\n\n        this.mappingColToColumn = new Map();\n        this.mappingColToSubColumn = new Map();\n        this.cursorPosition = {\n            x: 0,\n            y: 0,\n        };\n        const position = \"bottom\";\n        this.popover = usePopover(this.constructor.components.Popover, {\n            position,\n            onPositioned: (el, { direction }) => {\n                if (direction !== position) {\n                    return;\n                }\n                const { left, right } = el.getBoundingClientRect();\n                if ((0 <= left && right <= window.innerWidth) || window.innerWidth < right - left) {\n                    return;\n                }\n                const { left: pillLeft, right: pillRight } =\n                    this.popover.target.getBoundingClientRect();\n                const middle =\n                    (clamp(pillLeft, 0, window.innerWidth) +\n                        clamp(pillRight, 0, window.innerWidth)) /\n                    2;\n                el.style.left = `0px`;\n                const { width } = el.getBoundingClientRect();\n                el.style.left = `${middle - width / 2}px`;\n            },\n            onClose: () => {\n                delete this.popover.target;\n            },\n        });\n\n        this.throttledComputeHoverParams = throttleForAnimation((ev) =>\n            this.computeHoverParams(ev)\n        );\n\n        useExternalListener(window, \"keydown\", (ev) => this.onWindowKeyDown(ev));\n        useExternalListener(window, \"keyup\", (ev) => this.onWindowKeyUp(ev));\n\n        useExternalListener(\n            window,\n            \"resize\",\n            debounce(() => {\n                this.shouldComputeSomeWidths = true;\n                this.render();\n            }, 100)\n        );\n\n        useMultiHover({\n            ref: this.gridRef,\n            selector: \".o_gantt_group\",\n            related: [\"data-row-id\"],\n            className: \"o_gantt_group_hovered\",\n        });\n\n        // Draggable pills\n        this.cellForDrag = { el: null, part: 0 };\n        const dragState = useGanttDraggable({\n            enable: () => Boolean(this.cellForDrag.el),\n            // Refs and selectors\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_draggable\",\n            ignore: \".o_resize_handle,.o_connector_creator_bullet\",\n            cells: \".o_gantt_cell\",\n            // Style classes\n            cellDragClassName: \"o_gantt_cell o_drag_hover\",\n            ghostClassName: \"o_dragged_pill_ghost\",\n            addStickyCoordinates: (rows, columns) => {\n                this.stickyGridRows = Object.assign({}, ...rows.map((row) => ({ [row]: true })));\n                this.stickyGridColumns = Object.assign(\n                    {},\n                    ...columns.map((column) => ({ [column]: true }))\n                );\n                this.setSomeGridStyleProperties();\n            },\n            // Handlers\n            onDragStart: ({ pill }) => {\n                this.popover.close();\n                this.setStickyPill(pill);\n                this.interaction.mode = \"drag\";\n            },\n            onDragEnd: () => {\n                this.setStickyPill();\n                this.interaction.mode = null;\n            },\n            onDrop: (params) => this.dragPillDrop(params),\n        });\n\n        // Un-draggable pills\n        const unDragState = useGanttUndraggable({\n            // Refs and selectors\n            ref: this.gridRef,\n            elements: \".o_undraggable\",\n            ignore: \".o_resize_handle,.o_connector_creator_bullet\",\n            edgeScrolling: { enabled: false },\n            // Handlers\n            onDragStart: () => {\n                this.interaction.mode = \"locked\";\n            },\n            onDragEnd: () => {\n                this.interaction.mode = null;\n            },\n        });\n\n        // Cells selection\n        const selectState = useGanttSelectable({\n            enable: () => {\n                const { canCellCreate, canPlan } = this.model.metaData;\n                return Boolean(this.cellForDrag.el) && (canCellCreate || canPlan);\n            },\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_gantt_cell:not(.o_gantt_group)\",\n            edgeScrolling: { speed: 40, threshold: 150, direction: \"horizontal\" },\n            rtl: () => localization.direction === \"rtl\",\n            onDrop: ({ rowId, startCol, stopCol }) => {\n                const { canPlan } = this.model.metaData;\n                if (canPlan) {\n                    this.onPlan(rowId, startCol, stopCol);\n                } else {\n                    this.onCreate(rowId, startCol, stopCol);\n                }\n            },\n        });\n\n        // Resizable pills\n        const resizeState = useGanttResizable({\n            // Refs and selectors\n            ref: this.gridRef,\n            hoveredCell: this.cellForDrag,\n            elements: \".o_resizable\",\n            innerPills: \".o_gantt_pill\",\n            cells: \".o_gantt_cell\",\n            // Other params\n            handles: \"o_resize_handle\",\n            edgeScrolling: { speed: 40, threshold: 150, direction: \"horizontal\" },\n            showHandles: (pillEl) => {\n                const pill = this.pills[pillEl.dataset.pillId];\n                const hideHandles = this.connectorDragState.dragging;\n                return {\n                    start: !pill.disableStartResize && !hideHandles,\n                    end: !pill.disableStopResize && !hideHandles,\n                };\n            },\n            rtl: () => localization.direction === \"rtl\",\n            precision: () => this.model.metaData.scale.cellPart,\n            // Handlers\n            onDragStart: ({ pill, addClass }) => {\n                this.popover.close();\n                this.setStickyPill(pill);\n                addClass(pill, \"o_resized\");\n                this.interaction.mode = \"resize\";\n            },\n            onDrag: ({ pill, grabbedHandle, diff }) => {\n                const rect = pill.getBoundingClientRect();\n                const position = { top: rect.y + rect.height };\n                if (grabbedHandle === \"left\") {\n                    position.left = rect.x;\n                } else {\n                    position.right = document.body.offsetWidth - rect.x - rect.width;\n                }\n                const { cellTime, unitDescription } = this.model.metaData.scale;\n                Object.assign(this.resizeBadgeReactive, {\n                    position,\n                    diff: diff * cellTime,\n                    scale: unitDescription,\n                });\n            },\n            onDragEnd: ({ pill, removeClass }) => {\n                delete this.resizeBadgeReactive.position;\n                delete this.resizeBadgeReactive.diff;\n                delete this.resizeBadgeReactive.scale;\n                this.setStickyPill();\n                removeClass(pill, \"o_resized\");\n                this.interaction.mode = null;\n            },\n            onDrop: (params) => this.resizePillDrop(params),\n        });\n\n        // Draggable connector\n        let initialPillId;\n        this.connectorDragState = useGanttConnectorDraggable({\n            ref: this.gridRef,\n            elements: \".o_connector_creator_bullet\",\n            parentWrapper: \".o_gantt_cells .o_gantt_pill_wrapper\",\n            onDragStart: ({ sourcePill, x, y, addClass }) => {\n                this.popover.close();\n                initialPillId = sourcePill.dataset.pillId;\n                addClass(sourcePill, \"o_connector_creator_lock\");\n                this.setConnector({\n                    id: NEW_CONNECTOR_ID,\n                    highlighted: true,\n                    sourcePoint: { left: x, top: y },\n                    targetPoint: { left: x, top: y },\n                });\n                this.setStickyPill(sourcePill);\n                this.interaction.mode = \"connect\";\n            },\n            onDrag: ({ connectorCenter, x, y }) => {\n                this.setConnector({\n                    id: NEW_CONNECTOR_ID,\n                    sourcePoint: { left: connectorCenter.x, top: connectorCenter.y },\n                    targetPoint: { left: x, top: y },\n                });\n            },\n            onDragEnd: () => {\n                this.setConnector({ id: NEW_CONNECTOR_ID, sourcePoint: null, targetPoint: null });\n                this.setStickyPill();\n                this.interaction.mode = null;\n            },\n            onDrop: ({ target }) => {\n                if (initialPillId === target.dataset.pillId) {\n                    return;\n                }\n                const { id: masterId } = this.pills[initialPillId].record;\n                const { id: slaveId } = this.pills[target.dataset.pillId].record;\n                this.model.createDependency(masterId, slaveId);\n            },\n        });\n\n        this.dragStates = [dragState, unDragState, resizeState, selectState];\n\n        onWillStart(this.computeDerivedParams);\n        onWillUpdateProps(this.computeDerivedParams);\n\n        this.virtualGrid = useVirtualGrid({\n            scrollableRef: this.props.contentRef,\n            initialScroll: this.props.scrollPosition,\n            bufferCoef: 0.1,\n            onChange: (changed) => {\n                if (\"columnsIndexes\" in changed) {\n                    this.shouldComputeGridColumns = true;\n                }\n                if (\"rowsIndexes\" in changed) {\n                    this.shouldComputeGridRows = true;\n                }\n                this.render();\n            },\n        });\n\n        onWillRender(this.onWillRender);\n\n        useEffect(\n            (content) => {\n                content.addEventListener(\"scroll\", this.throttledComputeHoverParams);\n                return () => {\n                    content.removeEventListener(\"scroll\", this.throttledComputeHoverParams);\n                };\n            },\n            () => [this.gridRef.el?.parentElement]\n        );\n\n        useEffect(() => {\n            if (this.useFocusDate) {\n                this.useFocusDate = false;\n                this.focusDate(this.model.metaData.focusDate);\n            }\n        });\n\n        this.env.getCurrentFocusDateCallBackRecorder.add(this, this.getCurrentFocusDate.bind(this));\n    }\n\n    //-------------------------------------------------------------------------\n    // Getters\n    //-------------------------------------------------------------------------\n\n    get controlsProps() {\n        return {\n            displayExpandCollapseButtons: this.rows[0]?.isGroup, // all rows on same level have same type\n            model: this.model,\n            focusToday: () => this.focusToday(),\n            getCurrentFocusDate: () => this.getCurrentFocusDate(),\n        };\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get hasRowHeaders() {\n        const { groupedBy } = this.model.metaData;\n        const { displayMode } = this.model.displayParams;\n        return groupedBy.length || displayMode === \"sparse\";\n    }\n\n    get isDragging() {\n        return this.dragStates.some((s) => s.dragging);\n    }\n\n    /**\n     * @returns {boolean}\n     */\n    get isTouchDevice() {\n        return isMobileOS() || hasTouch();\n    }\n\n    //-------------------------------------------------------------------------\n    // Methods\n    //-------------------------------------------------------------------------\n\n    /**\n     *\n     * @param {Object} param\n     * @param {Object} param.grid\n     */\n    addCoordinatesToCoarseGrid({ grid }) {\n        if (grid.row) {\n            this.coarseGridRows[this.getFirstGridRow({ grid })] = true;\n            this.coarseGridRows[this.getLastGridRow({ grid })] = true;\n        }\n        if (grid.column) {\n            this.coarseGridCols[this.getFirstGridCol({ grid })] = true;\n            this.coarseGridCols[this.getLastGridCol({ grid })] = true;\n        }\n    }\n\n    /**\n     * @param {Pill} pill\n     * @param {Group} group\n     */\n    addTo(pill, group) {\n        group.pills.push(pill);\n        group.aggregateValue++; // pill count\n        return true;\n    }\n\n    /**\n     * Conditional function for aggregating pills when grouping the gantt view\n     * The first, unused parameter is added in case it's needed when overwriting the method.\n     * @param {Row} row\n     * @param {Group} group\n     * @returns {boolean}\n     */\n    shouldAggregate(row, group) {\n        return Boolean(group.pills.length);\n    }\n\n    /**\n     * Aggregates overlapping pills in group rows.\n     *\n     * @param {Pill[]} pills\n     * @param {Row} row\n     */\n    aggregatePills(pills, row) {\n        /** @type {Record<number, Group>} */\n        const groups = {};\n        function getGroup(col) {\n            if (!(col in groups)) {\n                groups[col] = {\n                    break: false,\n                    col,\n                    pills: [],\n                    aggregateValue: 0,\n                    grid: { column: [col, col + 1] },\n                };\n                // group.break = true means that the group cannot be merged with the previous one\n                // We will merge groups that can be merged together (if this.shouldMergeGroups returns true)\n            }\n            return groups[col];\n        }\n\n        for (const pill of pills) {\n            let addedInPreviousCol = false;\n            let col;\n            for (col = this.getFirstGridCol(pill); col < this.getLastGridCol(pill); col++) {\n                const group = getGroup(col);\n                const added = this.addTo(pill, group);\n                if (addedInPreviousCol !== added) {\n                    group.break = true;\n                }\n                addedInPreviousCol = added;\n            }\n            // here col = this.getLastGridCol(pill)\n            if (addedInPreviousCol && col <= this.columnCount) {\n                const group = getGroup(col);\n                group.break = true;\n            }\n        }\n\n        const filteredGroups = Object.values(groups).filter((g) => this.shouldAggregate(row, g));\n\n        if (this.shouldMergeGroups()) {\n            return this.mergeGroups(filteredGroups);\n        }\n\n        return filteredGroups;\n    }\n\n    /**\n     * Compute minimal levels required to display all pills without overlapping.\n     * Side effect: level key is modified in pills.\n     *\n     * @param {Pill[]} pills\n     */\n    calculatePillsLevel(pills) {\n        const firstPill = pills[0];\n        firstPill.level = 0;\n        const levels = [\n            {\n                pills: [firstPill],\n                maxCol: this.getLastGridCol(firstPill) - 1,\n            },\n        ];\n        for (const currentPill of pills.slice(1)) {\n            const lastCol = this.getLastGridCol(currentPill) - 1;\n            for (let l = 0; l < levels.length; l++) {\n                const level = levels[l];\n                if (this.getFirstGridCol(currentPill) > level.maxCol) {\n                    currentPill.level = l;\n                    level.pills.push(currentPill);\n                    level.maxCol = lastCol;\n                    break;\n                }\n            }\n            if (isNaN(currentPill.level)) {\n                currentPill.level = levels.length;\n                levels.push({\n                    pills: [currentPill],\n                    maxCol: lastCol,\n                });\n            }\n        }\n        return levels.length;\n    }\n\n    makeSubColumn(start, delta, cellTime, time) {\n        const subCellStart = dateAddFixedOffset(start, { [time]: delta * cellTime });\n        const subCellStop = dateAddFixedOffset(start, {\n            [time]: (delta + 1) * cellTime,\n            seconds: -1,\n        });\n        return { start: subCellStart, stop: subCellStop };\n    }\n\n    computeVisibleColumns() {\n        const [firstIndex, lastIndex] = this.virtualGrid.columnsIndexes;\n        this.columnsGroups = [];\n        this.columns = [];\n        this.subColumns = [];\n        this.coarseGridCols = {\n            1: true,\n            [this.columnCount * this.model.metaData.scale.cellPart + 1]: true,\n        };\n\n        const { globalStart, globalStop, scale } = this.model.metaData;\n        const { cellPart, interval, unit } = scale;\n\n        const now = DateTime.local();\n\n        const nowStart = now.startOf(interval);\n        const nowEnd = now.endOf(interval);\n\n        const groupsLeftBound = DateTime.max(\n            globalStart,\n            localStartOf(globalStart.plus({ [interval]: firstIndex }), unit)\n        );\n        const groupsRightBound = DateTime.min(\n            localEndOf(globalStart.plus({ [interval]: lastIndex }), unit),\n            globalStop\n        );\n        let currentGroup = null;\n        for (let j = firstIndex; j <= lastIndex; j++) {\n            const columnId = `__column__${j + 1}`;\n            const col = j * cellPart + 1;\n            const { start, stop } = this.getColumnFromColNumber(col);\n            const column = {\n                id: columnId,\n                grid: { column: [col, col + cellPart] },\n                start,\n                stop,\n            };\n            const isToday = nowStart <= start && start <= nowEnd;\n            if (isToday) {\n                column.isToday = true;\n            }\n            this.columns.push(column);\n\n            for (let i = 0; i < cellPart; i++) {\n                const subColumn = this.getSubColumnFromColNumber(col + i);\n                this.subColumns.push({ ...subColumn, isToday, columnId });\n                this.coarseGridCols[col + i] = true;\n            }\n\n            const groupStart = localStartOf(start, unit);\n            if (!currentGroup || !groupStart.equals(currentGroup.start)) {\n                const groupId = `__group__${this.columnsGroups.length + 1}`;\n                const startingBound = DateTime.max(groupsLeftBound, groupStart);\n                const endingBound = DateTime.min(groupsRightBound, localEndOf(groupStart, unit));\n                const [groupFirstCol, groupLastCol] = this.getGridColumnFromDates(\n                    startingBound,\n                    endingBound\n                );\n                currentGroup = {\n                    id: groupId,\n                    grid: { column: [groupFirstCol, groupLastCol] },\n                    start: groupStart,\n                };\n                this.columnsGroups.push(currentGroup);\n                this.coarseGridCols[groupFirstCol] = true;\n                this.coarseGridCols[groupLastCol] = true;\n            }\n        }\n    }\n\n    computeVisibleRows() {\n        this.coarseGridRows = {\n            1: true,\n            [this.getLastGridRow(this.rows[this.rows.length - 1])]: true,\n        };\n        const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;\n        this.rowsToRender = new Set();\n        for (const row of this.rows) {\n            const [first, last] = row.grid.row;\n            if (last <= rowStart + 1 || first > rowEnd + 1) {\n                continue;\n            }\n            this.addToRowsToRender(row);\n        }\n    }\n\n    getFirstGridCol({ grid }) {\n        const [first] = grid.column;\n        return first;\n    }\n\n    getLastGridCol({ grid }) {\n        const [, last] = grid.column;\n        return last;\n    }\n\n    getFirstGridRow({ grid }) {\n        const [first] = grid.row;\n        return first;\n    }\n\n    getLastGridRow({ grid }) {\n        const [, last] = grid.row;\n        return last;\n    }\n\n    addToPillsToRender(pill) {\n        this.pillsToRender.add(pill);\n        this.addCoordinatesToCoarseGrid(pill);\n    }\n\n    addToRowsToRender(row) {\n        this.rowsToRender.add(row);\n        const [first, last] = row.grid.row;\n        for (let i = first; i <= last; i++) {\n            this.coarseGridRows[i] = true;\n        }\n    }\n\n    /**\n     * give bounds only\n     */\n    getVisibleCols() {\n        const [columnStart, columnEnd] = this.virtualGrid.columnsIndexes;\n        const { cellPart } = this.model.metaData.scale;\n        const firstVisibleCol = 1 + cellPart * columnStart;\n        const lastVisibleCol = 1 + cellPart * (columnEnd + 1);\n        return [firstVisibleCol, lastVisibleCol];\n    }\n\n    /**\n     * give bounds only\n     */\n    getVisibleRows() {\n        const [rowStart, rowEnd] = this.virtualGrid.rowsIndexes;\n        const firstVisibleRow = rowStart + 1;\n        const lastVisibleRow = rowEnd + 1;\n        return [firstVisibleRow, lastVisibleRow];\n    }\n\n    computeVisiblePills() {\n        this.pillsToRender = new Set();\n\n        const [firstVisibleCol, lastVisibleCol] = this.getVisibleCols();\n        const [firstVisibleRow, lastVisibleRow] = this.getVisibleRows();\n\n        const isOut = (pill, filterOnRow = true) =>\n            this.getFirstGridCol(pill) > lastVisibleCol ||\n            this.getLastGridCol(pill) < firstVisibleCol ||\n            (filterOnRow &&\n                (this.getFirstGridRow(pill) > lastVisibleRow ||\n                    this.getLastGridRow(pill) - 1 < firstVisibleRow));\n\n        const getRowPills = (row, filterOnRow) =>\n            (this.rowPills[row.id] || []).filter((pill) => !isOut(pill, filterOnRow));\n\n        for (const row of this.rowsToRender) {\n            for (const rowPill of getRowPills(row)) {\n                this.addToPillsToRender(rowPill);\n            }\n            if (!row.isGroup && row.unavailabilities?.length) {\n                row.cellColors = this.getRowCellColors(row);\n            }\n        }\n\n        if (this.stickyPillId) {\n            this.addToPillsToRender(this.pills[this.stickyPillId]);\n        }\n\n        if (this.totalRow) {\n            this.totalRow.pills = getRowPills(this.totalRow, false);\n            for (const pill of this.totalRow.pills) {\n                this.addCoordinatesToCoarseGrid({ grid: omit(pill.grid, \"row\") });\n            }\n        }\n    }\n\n    computeVisibleConnectors() {\n        const visibleConnectorIds = new Set([NEW_CONNECTOR_ID]);\n\n        for (const pill of this.pillsToRender) {\n            const row = this.getRowFromPill(pill);\n            if (row.isGroup) {\n                continue;\n            }\n            for (const connectorId of this.mappingPillToConnectors[pill.id] || []) {\n                visibleConnectorIds.add(connectorId);\n            }\n        }\n\n        this.connectorsToRender = [];\n        for (const connectorId in this.connectors) {\n            if (!visibleConnectorIds.has(connectorId)) {\n                continue;\n            }\n            this.connectorsToRender.push(this.connectors[connectorId]);\n            const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n            if (sourcePillId) {\n                this.addToPillsToRender(this.pills[sourcePillId]);\n            }\n            if (targetPillId) {\n                this.addToPillsToRender(this.pills[targetPillId]);\n            }\n        }\n    }\n\n    getRowFromPill(pill) {\n        return this.rowByIds[pill.rowId];\n    }\n\n    getColInCoarseGridKeys() {\n        return Object.keys({ ...this.coarseGridCols, ...this.stickyGridColumns });\n    }\n\n    getRowInCoarseGridKeys() {\n        return Object.keys({ ...this.coarseGridRows, ...this.stickyGridRows });\n    }\n\n    computeColsTemplate() {\n        const colsTemplate = [];\n        const colInCoarseGridKeys = this.getColInCoarseGridKeys();\n        for (let i = 0; i < colInCoarseGridKeys.length - 1; i++) {\n            const x = +colInCoarseGridKeys[i];\n            const y = +colInCoarseGridKeys[i + 1];\n            const colName = `c${x}`;\n            const width = (y - x) * this.cellPartWidth;\n            colsTemplate.push(`[${colName}]minmax(${width}px,1fr)`);\n        }\n        colsTemplate.push(`[c${colInCoarseGridKeys.at(-1)}]`);\n        return colsTemplate.join(\"\");\n    }\n\n    computeRowsTemplate() {\n        const rowsTemplate = [];\n        const rowInCoarseGridKeys = this.getRowInCoarseGridKeys();\n        for (let i = 0; i < rowInCoarseGridKeys.length - 1; i++) {\n            const x = +rowInCoarseGridKeys[i];\n            const y = +rowInCoarseGridKeys[i + 1];\n            const rowName = `r${x}`;\n            const height = this.gridRows.slice(x - 1, y - 1).reduce((a, b) => a + b, 0);\n            rowsTemplate.push(`[${rowName}]${height}px`);\n        }\n        rowsTemplate.push(`[r${rowInCoarseGridKeys.at(-1)}]`);\n        return rowsTemplate.join(\"\");\n    }\n\n    computeSomeWidths() {\n        const { cellPart, minimalColumnWidth } = this.model.metaData.scale;\n        this.contentRefWidth = this.props.contentRef.el?.clientWidth ?? document.body.clientWidth;\n        const rowHeaderWidthPercentage = this.hasRowHeaders\n            ? this.constructor.getRowHeaderWidth(this.contentRefWidth)\n            : 0;\n        this.rowHeaderWidth = this.hasRowHeaders\n            ? Math.round((rowHeaderWidthPercentage * this.contentRefWidth) / 100)\n            : 0;\n        const cellContainerWidth = this.contentRefWidth - this.rowHeaderWidth;\n        const columnWidth = Math.floor(cellContainerWidth / this.columnCount);\n        const rectifiedColumnWidth = Math.max(columnWidth, minimalColumnWidth);\n        this.cellPartWidth = Math.floor(rectifiedColumnWidth / cellPart);\n        this.columnWidth = this.cellPartWidth * cellPart;\n        if (columnWidth <= minimalColumnWidth) {\n            // overflow\n            this.totalWidth = this.rowHeaderWidth + this.columnWidth * this.columnCount;\n        } else {\n            this.totalWidth = null;\n        }\n    }\n\n    computeDerivedParams() {\n        const { rows: modelRows } = this.model.data;\n\n        if (this.shouldRenderConnectors()) {\n            /** @type {Record<number, { masterIds: number[], pills: Record<RowId, Pill> }>} */\n            this.mappingRecordToPillsByRow = {};\n            /** @type {Record<RowId, Record<number, Pill>>} */\n            this.mappingRowToPillsByRecord = {};\n            /** @type {Record<ConnectorId, { sourcePillId: PillId, targetPillId: PillId }>} */\n            this.mappingConnectorToPills = {};\n            /** @type {Record<PillId, ConnectorId>} */\n            this.mappingPillToConnectors = {};\n        }\n\n        const { globalStart, globalStop, scale, startDate, stopDate } = this.model.metaData;\n        this.columnCount = diffColumn(globalStart, globalStop, scale.interval);\n        if (\n            !this.currentStartDate ||\n            diffColumn(this.currentStartDate, startDate, \"day\") ||\n            diffColumn(this.currentStopDate, stopDate, \"day\") ||\n            this.currentScaleId !== scale.id\n        ) {\n            this.useFocusDate = true;\n            this.mappingColToColumn = new Map();\n            this.mappingColToSubColumn = new Map();\n        }\n        this.currentStartDate = startDate;\n        this.currentStopDate = stopDate;\n        this.currentScaleId = scale.id;\n\n        this.currentGridRow = 1;\n        this.gridRows = [];\n        this.nextPillId = 1;\n\n        this.pills = {}; // mapping to retrieve pills from pill ids\n        this.rows = [];\n        this.rowPills = {};\n        this.rowByIds = {};\n\n        const prePills = this.getPills();\n\n        let pillsToProcess = [...prePills];\n        for (const row of modelRows) {\n            const result = this.processRow(row, pillsToProcess);\n            this.rows.push(...result.rows);\n            pillsToProcess = result.pillsToProcess;\n        }\n\n        const { displayTotalRow } = this.model.metaData;\n        if (displayTotalRow) {\n            this.totalRow = this.getTotalRow(prePills);\n        }\n\n        if (this.shouldRenderConnectors()) {\n            this.initializeConnectors();\n            this.generateConnectors();\n        }\n\n        this.shouldComputeSomeWidths = true;\n        this.shouldComputeGridColumns = true;\n        this.shouldComputeGridRows = true;\n    }\n\n    computeDerivedParamsFromHover() {\n        const { scale } = this.model.metaData;\n\n        const { connector, hoverable, pill } = this.hovered;\n\n        // Update cell in drag\n        const isCellHovered = hoverable?.matches(\".o_gantt_cell\");\n        this.cellForDrag.el = isCellHovered ? hoverable : null;\n        this.cellForDrag.part = 0;\n        if (isCellHovered && scale.cellPart > 1) {\n            const rect = hoverable.getBoundingClientRect();\n            const x = Math.floor(rect.x);\n            const width = Math.floor(rect.width);\n            this.cellForDrag.part = Math.floor(\n                (this.cursorPosition.x - x) / (width / scale.cellPart)\n            );\n            if (localization.direction === \"rtl\") {\n                this.cellForDrag.part = scale.cellPart - 1 - this.cellForDrag.part;\n            }\n        }\n\n        if (this.isDragging) {\n            this.progressBarsReactive.hoveredRowId = null;\n            return;\n        }\n\n        if (!this.connectorDragState.dragging) {\n            // Highlight connector\n            const hoveredConnectorId = connector?.dataset.connectorId;\n            for (const connectorId in this.connectors) {\n                if (connectorId !== hoveredConnectorId) {\n                    this.toggleConnectorHighlighting(connectorId, false);\n                }\n            }\n            if (hoveredConnectorId) {\n                this.progressBarsReactive.hoveredRowId = null;\n                return this.toggleConnectorHighlighting(hoveredConnectorId, true);\n            }\n        }\n\n        // Highlight pill\n        const hoveredPillId = pill?.dataset.pillId;\n        for (const pillId in this.pills) {\n            if (pillId !== hoveredPillId) {\n                this.togglePillHighlighting(pillId, false);\n            }\n        }\n        this.togglePillHighlighting(hoveredPillId, true);\n\n        // Update progress bars\n        this.progressBarsReactive.hoveredRowId = hoverable ? hoverable.dataset.rowId : null;\n    }\n\n    /**\n     * @param {ConnectorId} connectorId\n     */\n    deleteConnector(connectorId) {\n        delete this.connectors[connectorId];\n        delete this.mappingConnectorToPills[connectorId];\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.pill\n     * @param {Element} params.cell\n     * @param {number} params.diff\n     */\n    async dragPillDrop({ pill, cell, diff }) {\n        const { rowId } = cell.dataset;\n        const { dateStartField, dateStopField, scale } = this.model.metaData;\n        const { cellTime, time } = scale;\n        const { record } = this.pills[pill.dataset.pillId];\n        const params = this.getScheduleParams(pill);\n\n        params.start =\n            diff && dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });\n        params.stop =\n            diff && dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });\n        params.rowId = rowId;\n\n        const schedule = this.model.getSchedule(params);\n\n        if (this.interaction.dragAction === \"copy\") {\n            await this.model.copy(record.id, schedule, this.openPlanDialogCallback);\n        } else {\n            await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);\n        }\n\n        // If the pill lands on a closed group -> open it\n        if (cell.classList.contains(\"o_gantt_group\") && this.model.isClosed(rowId)) {\n            this.model.toggleRow(rowId);\n        }\n    }\n\n    /**\n     * @param {Partial<Pill>} pill\n     * @returns {Pill}\n     */\n    enrichPill(pill) {\n        const { colorField, fields, pillDecorations, progressField } = this.model.metaData;\n\n        pill.displayName = this.getDisplayName(pill);\n\n        const classes = [];\n\n        if (pillDecorations) {\n            const pillContext = Object.assign({}, user.context);\n            for (const [fieldName, value] of Object.entries(pill.record)) {\n                const field = fields[fieldName];\n                switch (field.type) {\n                    case \"date\": {\n                        pillContext[fieldName] = value ? serializeDate(value) : false;\n                        break;\n                    }\n                    case \"datetime\": {\n                        pillContext[fieldName] = value ? serializeDateTime(value) : false;\n                        break;\n                    }\n                    default: {\n                        pillContext[fieldName] = value;\n                    }\n                }\n            }\n\n            for (const decoration in pillDecorations) {\n                const expr = pillDecorations[decoration];\n                if (evaluateBooleanExpr(expr, pillContext)) {\n                    classes.push(decoration);\n                }\n            }\n        }\n\n        if (colorField) {\n            pill._color = getColorIndex(pill.record[colorField]);\n            classes.push(`o_gantt_color_${pill._color}`);\n        }\n\n        if (progressField) {\n            pill._progress = pill.record[progressField] || 0;\n        }\n\n        pill.className = classes.join(\" \");\n\n        return pill;\n    }\n\n    focusDate(date, ifInBounds) {\n        const { globalStart, globalStop } = this.model.metaData;\n        const diff = date.diff(globalStart);\n        const totalDiff = globalStop.diff(globalStart);\n        const factor = diff / totalDiff;\n        if (ifInBounds && (factor < 0 || 1 < factor)) {\n            return false;\n        }\n        const rtlFactor = localization.direction === \"rtl\" ? -1 : 1;\n        const scrollLeft =\n            factor * this.cellContainerRef.el.clientWidth +\n            this.rowHeaderWidth -\n            (this.contentRefWidth + this.rowHeaderWidth) / 2;\n        this.props.contentRef.el.scrollLeft = rtlFactor * scrollLeft;\n        return true;\n    }\n\n    focusFirstPill(rowId) {\n        const pill = this.rowPills[rowId][0];\n        if (pill) {\n            const col = this.getFirstGridCol(pill);\n            const { start: date } = this.getColumnFromColNumber(col);\n            this.focusDate(date);\n        }\n    }\n\n    focusToday() {\n        return this.focusDate(DateTime.local().startOf(\"day\"), true);\n    }\n\n    generateConnectors() {\n        this.nextConnectorId = 1;\n        this.setConnector({\n            id: NEW_CONNECTOR_ID,\n            highlighted: true,\n            sourcePoint: null,\n            targetPoint: null,\n        });\n        for (const slaveId in this.mappingRecordToPillsByRow) {\n            const { masterIds, pills: slavePills } = this.mappingRecordToPillsByRow[slaveId];\n            for (const masterId of masterIds) {\n                if (!(masterId in this.mappingRecordToPillsByRow)) {\n                    continue;\n                }\n                const { pills: masterPills } = this.mappingRecordToPillsByRow[masterId];\n                for (const [slaveRowId, targetPill] of Object.entries(slavePills)) {\n                    for (const [masterRowId, sourcePill] of Object.entries(masterPills)) {\n                        if (\n                            masterRowId === slaveRowId ||\n                            !(\n                                slaveId in this.mappingRowToPillsByRecord[masterRowId] ||\n                                masterId in this.mappingRowToPillsByRecord[slaveRowId]\n                            ) ||\n                            Object.keys(this.mappingRecordToPillsByRow[slaveId].pills).every(\n                                (rowId) =>\n                                    rowId !== masterRowId &&\n                                    masterId in this.mappingRowToPillsByRecord[rowId]\n                            ) ||\n                            Object.keys(this.mappingRecordToPillsByRow[masterId].pills).every(\n                                (rowId) =>\n                                    rowId !== slaveRowId &&\n                                    slaveId in this.mappingRowToPillsByRecord[rowId]\n                            )\n                        ) {\n                            const masterRecord = sourcePill.record;\n                            const slaveRecord = targetPill.record;\n                            this.setConnector(\n                                { alert: this.getConnectorAlert(masterRecord, slaveRecord) },\n                                sourcePill.id,\n                                targetPill.id\n                            );\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * @param {Group} group\n     * @param {Group} previousGroup\n     */\n    getAggregateValue(group, previousGroup) {\n        // both groups have the same pills by construction\n        // here the aggregateValue is the pill count\n        return group.aggregateValue;\n    }\n\n    /**\n     * @param {number} startCol\n     * @param {number} stopCol\n     * @param {boolean} [roundUpStop=true]\n     */\n    getColumnStartStop(startCol, stopCol, roundUpStop = true) {\n        const { start } = this.getColumnFromColNumber(startCol);\n        let { stop } = this.getColumnFromColNumber(stopCol);\n        if (roundUpStop) {\n            stop = stop.plus({ millisecond: 1 });\n        }\n        return { start, stop };\n    }\n\n    /**\n     *\n     * @param {number} masterRecord\n     * @param {number} slaveRecord\n     * @returns {import(\"./gantt_connector\").ConnectorAlert | null}\n     */\n    getConnectorAlert(masterRecord, slaveRecord) {\n        const { dateStartField, dateStopField } = this.model.metaData;\n        if (slaveRecord[dateStartField] < masterRecord[dateStopField]) {\n            if (slaveRecord[dateStartField] < masterRecord[dateStartField]) {\n                return \"error\";\n            } else {\n                return \"warning\";\n            }\n        }\n        return null;\n    }\n\n    /**\n     * @param {Row} row\n     * @param {Column} column\n     * @return {Object}\n     */\n    ganttCellAttClass(row, column) {\n        return {\n            o_sample_data_disabled: this.isDisabled(row),\n            o_gantt_today: column.isToday,\n            o_gantt_group: row.isGroup,\n            o_gantt_hoverable: this.isHoverable(row),\n            o_group_open: !this.model.isClosed(row.id),\n        };\n    }\n\n    getCurrentFocusDate() {\n        const { globalStart, globalStop } = this.model.metaData;\n        const rtlFactor = localization.direction === \"rtl\" ? -1 : 1;\n        const cellGridMiddleX =\n            rtlFactor * this.props.contentRef.el.scrollLeft +\n            (this.contentRefWidth + this.rowHeaderWidth) / 2;\n        const factor =\n            (cellGridMiddleX - this.rowHeaderWidth) / this.cellContainerRef.el.clientWidth;\n        const totalDiff = globalStop.diff(globalStart);\n        const diff = factor * totalDiff;\n        const focusDate = globalStart.plus(diff);\n        return focusDate;\n    }\n\n    /**\n     * @param {\"top\"|\"bottom\"} vertical the vertical alignment of the connector creator\n     * @returns {{ vertical: \"top\"|\"bottom\", horizontal: \"left\"|\"right\" }}\n     */\n    getConnectorCreatorAlignment(vertical) {\n        const alignment = { vertical };\n        if (localization.direction === \"rtl\") {\n            alignment.horizontal = vertical === \"top\" ? \"right\" : \"left\";\n        } else {\n            alignment.horizontal = vertical === \"top\" ? \"left\" : \"right\";\n        }\n        return alignment;\n    }\n\n    /**\n     * Get schedule parameters\n     *\n     * @param {Element} pill\n     * @returns {Object} - An object containing parameters needed for scheduling the pill.\n     */\n    getScheduleParams(pill) {\n        return {};\n    }\n\n    /**\n     * This function will add a 'label' property to each\n     * non-consolidated pill included in the pills list.\n     * This new property is a string meant to replace\n     * the text displayed on a pill.\n     *\n     * @param {Pill} pill\n     */\n    getDisplayName(pill) {\n        const { computePillDisplayName, dateStartField, dateStopField, scale } =\n            this.model.metaData;\n        const { id: scaleId } = scale;\n        const { record } = pill;\n\n        if (!computePillDisplayName) {\n            return record.display_name;\n        }\n\n        const startDate = record[dateStartField];\n        const stopDate = record[dateStopField];\n        const yearlessDateFormat = omit(DateTime.DATE_SHORT, \"year\");\n\n        const spanAccrossDays =\n            stopDate.startOf(\"day\") > startDate.startOf(\"day\") &&\n            startDate.endOf(\"day\").diff(startDate, \"hours\").toObject().hours >= 3 &&\n            stopDate.diff(stopDate.startOf(\"day\"), \"hours\").toObject().hours >= 3;\n        const spanAccrossWeeks = getStartOfLocalWeek(stopDate) > getStartOfLocalWeek(startDate);\n        const spanAccrossMonths = stopDate.startOf(\"month\") > startDate.startOf(\"month\");\n\n        /** @type {string[]} */\n        const labelElements = [];\n\n        // Start & End Dates\n        if (scaleId === \"year\" && !spanAccrossDays) {\n            labelElements.push(startDate.toLocaleString(yearlessDateFormat));\n        } else if (\n            (scaleId === \"day\" && spanAccrossDays) ||\n            (scaleId === \"week\" && spanAccrossWeeks) ||\n            (scaleId === \"month\" && spanAccrossMonths) ||\n            (scaleId === \"year\" && spanAccrossDays)\n        ) {\n            labelElements.push(startDate.toLocaleString(yearlessDateFormat));\n            labelElements.push(stopDate.toLocaleString(yearlessDateFormat));\n        }\n\n        // Start & End Times\n        if (record.allocated_hours && !spanAccrossDays && [\"week\", \"month\"].includes(scaleId)) {\n            const durationStr = this.getDurationStr(record);\n            labelElements.push(startDate.toFormat(\"t\"), `${stopDate.toFormat(\"t\")}${durationStr}`);\n        }\n\n        // Original Display Name\n        if (scaleId !== \"month\" || !record.allocated_hours || spanAccrossDays) {\n            labelElements.push(record.display_name);\n        }\n\n        return labelElements.filter((el) => !!el).join(\" - \");\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     */\n    getDurationStr(record) {\n        const durationStr = formatFloatTime(record.allocated_hours, {\n            noLeadingZeroHour: true,\n        }).replace(/(:00|:)/g, \"h\");\n        return ` (${durationStr})`;\n    }\n\n    /**\n     * @param {Pill} pill\n     */\n    getGroupPillDisplayName(pill) {\n        return pill.aggregateValue;\n    }\n\n    /**\n     * @param {{ column?: [number, number], row?: [number, number] }} position\n     */\n    getGridPosition(position) {\n        const style = [];\n        const keys = Object.keys(pick(position, \"column\", \"row\"));\n        for (const key of keys) {\n            const prefix = key.slice(0, 1);\n            const [first, last] = position[key];\n            style.push(`grid-${key}:${prefix}${first}/${prefix}${last}`);\n        }\n        return style.join(\";\");\n    }\n\n    setSomeGridStyleProperties() {\n        const rowsTemplate = this.computeRowsTemplate();\n        const colsTemplate = this.computeColsTemplate();\n        this.gridRef.el.style.setProperty(\"--Gantt__GridRows-grid-template-rows\", rowsTemplate);\n        this.gridRef.el.style.setProperty(\n            \"--Gantt__GridColumns-grid-template-columns\",\n            colsTemplate\n        );\n    }\n\n    getGridStyle() {\n        const rowsTemplate = this.computeRowsTemplate();\n        const colsTemplate = this.computeColsTemplate();\n        const style = {\n            \"--Gantt__RowHeader-width\": `${this.rowHeaderWidth}px`,\n            \"--Gantt__Pill-height\": \"35px\",\n            \"--Gantt__Thumbnail-max-height\": \"16px\",\n            \"--Gantt__GridRows-grid-template-rows\": rowsTemplate,\n            \"--Gantt__GridColumns-grid-template-columns\": colsTemplate,\n        };\n        if (this.totalWidth !== null) {\n            style.width = `${this.totalWidth}px`;\n        }\n        return Object.entries(style)\n            .map((entry) => entry.join(\":\"))\n            .join(\";\");\n    }\n\n    /**\n     * @param {RelationalRecord} record\n     * @returns {Partial<Pill>}\n     */\n    getPill(record) {\n        const { canEdit, dateStartField, dateStopField, disableDrag, globalStart, globalStop } =\n            this.model.metaData;\n\n        const startOutside = record[dateStartField] < globalStart;\n\n        let recordDateStopField = record[dateStopField];\n        if (this.model.dateStopFieldIsDate()) {\n            recordDateStopField = recordDateStopField.plus({ day: 1 });\n        }\n\n        const stopOutside = recordDateStopField > globalStop;\n\n        /** @type {DateTime} */\n        const pillStartDate = startOutside ? globalStart : record[dateStartField];\n        /** @type {DateTime} */\n        const pillStopDate = stopOutside ? globalStop : recordDateStopField;\n\n        const disableStartResize = !canEdit || startOutside;\n        const disableStopResize = !canEdit || stopOutside;\n\n        /** @type {Partial<Pill>} */\n        const pill = {\n            disableDrag: disableDrag || disableStartResize || disableStopResize,\n            disableStartResize,\n            disableStopResize,\n            grid: { column: this.getGridColumnFromDates(pillStartDate, pillStopDate) },\n            record,\n        };\n\n        return pill;\n    }\n\n    getGridColumnFromDates(startDate, stopDate) {\n        const { globalStart, scale } = this.model.metaData;\n        const { cellPart, interval } = scale;\n        const { column: column1, delta: delta1 } = this.getSubColumnFromDate(startDate);\n        const { column: column2, delta: delta2 } = this.getSubColumnFromDate(stopDate, false);\n        const firstCol = 1 + diffColumn(globalStart, column1, interval) * cellPart + delta1;\n        const span = diffColumn(column1, column2, interval) * cellPart + delta2 - delta1;\n        return [firstCol, firstCol + span];\n    }\n\n    getSubColumnFromDate(date, onLeft = true) {\n        const { interval, cellPart, cellTime, time } = this.model.metaData.scale;\n        const column = date.startOf(interval);\n        let delta;\n        if (onLeft) {\n            delta = 0;\n            for (let i = 1; i < cellPart; i++) {\n                const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });\n                if (subCellStart <= date) {\n                    delta += 1;\n                } else {\n                    break;\n                }\n            }\n        } else {\n            delta = cellPart;\n            for (let i = cellPart - 1; i >= 0; i--) {\n                const subCellStart = dateAddFixedOffset(column, { [time]: i * cellTime });\n                if (subCellStart >= date) {\n                    delta -= 1;\n                } else {\n                    break;\n                }\n            }\n        }\n        return { column, delta };\n    }\n\n    getSubColumnFromColNumber(col) {\n        let subColumn = this.mappingColToSubColumn.get(col);\n        if (!subColumn) {\n            const { globalStart, scale } = this.model.metaData;\n            const { interval, cellPart, cellTime, time } = scale;\n            const delta = (col - 1) % cellPart;\n            const columnIndex = (col - 1 - delta) / cellPart;\n            const start = globalStart.plus({ [interval]: columnIndex });\n            subColumn = this.makeSubColumn(start, delta, cellTime, time);\n            this.mappingColToSubColumn.set(col, subColumn);\n        }\n        return subColumn;\n    }\n\n    getColumnFromColNumber(col) {\n        let column = this.mappingColToColumn.get(col);\n        if (!column) {\n            const { globalStart, scale } = this.model.metaData;\n            const { interval, cellPart } = scale;\n            const delta = (col - 1) % cellPart;\n            const columnIndex = (col - 1 - delta) / cellPart;\n            const start = globalStart.plus({ [interval]: columnIndex });\n            const stop = start.endOf(interval);\n            column = { start, stop };\n            this.mappingColToColumn.set(col, column);\n        }\n        return column;\n    }\n\n    /**\n     * @param {PillId} pillId\n     */\n    getPillEl(pillId) {\n        return this.getPillWrapperEl(pillId).querySelector(\".o_gantt_pill\");\n    }\n\n    /**\n     * @param {Object} group\n     * @param {number} maxAggregateValue\n     * @param {boolean} consolidate\n     */\n    getPillFromGroup(group, maxAggregateValue, consolidate) {\n        const { excludeField, field, maxValue } = this.model.metaData.consolidationParams;\n\n        const minColor = 215;\n        const maxColor = 100;\n\n        const newPill = {\n            id: `__pill__${this.nextPillId++}`,\n            level: 0,\n            aggregateValue: group.aggregateValue,\n            grid: group.grid,\n        };\n\n        // Enrich the aggregates with consolidation data\n        if (consolidate && field) {\n            newPill.consolidationValue = 0;\n            for (const pill of group.pills) {\n                if (!pill.record[excludeField]) {\n                    newPill.consolidationValue += pill.record[field];\n                }\n            }\n            newPill.consolidationMaxValue = maxValue;\n            newPill.consolidationExceeded =\n                newPill.consolidationValue > newPill.consolidationMaxValue;\n        }\n\n        if (consolidate && maxValue) {\n            const status = newPill.consolidationExceeded ? \"danger\" : \"success\";\n            newPill.className = `bg-${status} border-${status}`;\n            newPill.displayName = newPill.consolidationValue;\n        } else {\n            const color =\n                minColor -\n                Math.round((newPill.aggregateValue - 1) / maxAggregateValue) *\n                    (minColor - maxColor);\n            newPill.style = `background-color:rgba(${color},${color},${color},0.6)`;\n            newPill.displayName = this.getGroupPillDisplayName(newPill);\n        }\n\n        return newPill;\n    }\n\n    /**\n     * There are two forms of pills: pills comming from fetched records\n     * and pills that are some kind of aggregation of the previous.\n     *\n     * Here we create the pills of the firs type.\n     *\n     * The basic properties (independent of rows,...) of the pills of\n     * the first type should be computed here.\n     *\n     * @returns {Partial<Pill>[]}\n     */\n    getPills() {\n        const { records } = this.model.data;\n        const { dateStartField } = this.model.metaData;\n        const pills = [];\n        for (const record of records) {\n            const pill = this.getPill(record);\n            pills.push(this.enrichPill(pill));\n        }\n        return pills.sort(\n            (p1, p2) =>\n                p1.grid.column[0] - p2.grid.column[0] ||\n                p1.record[dateStartField] - p2.record[dateStartField]\n        );\n    }\n\n    /**\n     * @param {PillId} pillId\n     */\n    getPillWrapperEl(pillId) {\n        const pillSelector = `:scope > [data-pill-id=\"${pillId}\"]`;\n        return this.cellContainerRef.el?.querySelector(pillSelector);\n    }\n\n    /**\n     * Get domain of records for plan dialog in the gantt view.\n     *\n     * @param {Object} state\n     * @returns {any[][]}\n     */\n    getPlanDialogDomain() {\n        const { dateStartField, dateStopField } = this.model.metaData;\n        const newDomain = Domain.removeDomainLeaves(this.env.searchModel.globalDomain, [\n            dateStartField,\n            dateStopField,\n        ]);\n        return Domain.and([\n            newDomain,\n            [\"|\", [dateStartField, \"=\", false], [dateStopField, \"=\", false]],\n        ]).toList({});\n    }\n\n    /**\n     * @param {PillId} pillId\n     * @param {boolean} onRight\n     */\n    getPoint(pillId, onRight) {\n        if (localization.direction === \"rtl\") {\n            onRight = !onRight;\n        }\n        const pillEl = this.getPillEl(pillId);\n        const pillRect = pillEl.getBoundingClientRect();\n        return {\n            left: pillRect.left + (onRight ? pillRect.width : 0),\n            top: pillRect.top + pillRect.height / 2,\n        };\n    }\n\n    /**\n     * @param {Pill} pill\n     */\n    getPopoverProps(pill) {\n        const { record } = pill;\n        const { id: resId, display_name: displayName } = record;\n        const { canEdit, dateStartField, dateStopField, popoverArchParams, resModel } =\n            this.model.metaData;\n        const context = popoverArchParams.bodyTemplate\n            ? { ...record }\n            : /* Default context */ {\n                  name: displayName,\n                  start: record[dateStartField].toFormat(\"f\"),\n                  stop: record[dateStopField].toFormat(\"f\"),\n              };\n\n        return {\n            ...popoverArchParams,\n            title: displayName,\n            context,\n            resId,\n            resModel,\n            reload: () => this.model.fetchData(),\n            buttons: [\n                {\n                    id: \"open_view_edit_dialog\",\n                    text: canEdit ? _t(\"Edit\") : _t(\"View\"),\n                    class: \"btn btn-sm btn-primary\",\n                    // Sync with the mutex to wait for potential changes on the view\n                    onClick: () =>\n                        this.model.mutex.exec(\n                            () => this.props.openDialog({ resId }) // (canEdit is also considered in openDialog)\n                        ),\n                },\n            ],\n        };\n    }\n\n    /**\n     * @param {Row} row\n     */\n    getProgressBarProps(row) {\n        return {\n            progressBar: row.progressBar,\n            reactive: this.progressBarsReactive,\n            rowId: row.id,\n        };\n    }\n\n    /**\n     * @param {Row} row\n     */\n    getRowCellColors(row) {\n        const { unavailabilities } = row;\n        const { cellPart } = this.model.metaData.scale;\n        // We assume that the unavailabilities have been normalized\n        // (i.e. are naturally ordered and are pairwise disjoint).\n        // A subCell is considered unavailable (and greyed) when totally covered by\n        // an unavailability.\n        let index = 0;\n        let j = 0;\n        /** @type {Record<string, string>} */\n        const cellColors = {};\n        const subSlotUnavailabilities = [];\n        for (const subColumn of this.subColumns) {\n            const { isToday, start, stop, columnId } = subColumn;\n            if (index < unavailabilities.length) {\n                let subSlotUnavailable = 0;\n                for (let i = index; i < unavailabilities.length; i++) {\n                    const u = unavailabilities[i];\n                    if (stop > u.stop) {\n                        index++;\n                        continue;\n                    } else if (u.start <= start) {\n                        subSlotUnavailable = 1;\n                    }\n                    break;\n                }\n                subSlotUnavailabilities.push(subSlotUnavailable);\n                if ((j + 1) % cellPart === 0) {\n                    const style = getCellColor(cellPart, subSlotUnavailabilities, isToday);\n                    subSlotUnavailabilities.splice(0, cellPart);\n                    if (style) {\n                        cellColors[columnId] = style;\n                    }\n                }\n                j++;\n            }\n        }\n        return cellColors;\n    }\n\n    getFromData(groupedByField, resId, key, defaultVal) {\n        const values = this.model.data[key];\n        if (groupedByField) {\n            return values[groupedByField]?.[resId ?? false] || defaultVal;\n        }\n        return values.__default?.false || defaultVal;\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {Object}\n     */\n    getRowProgressBar(groupedByField, resId) {\n        return this.getFromData(groupedByField, resId, \"progressBars\", null);\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {{ start: DateTime, stop: DateTime }[]}\n     */\n    getRowUnavailabilities(groupedByField, resId) {\n        return this.getFromData(groupedByField, resId, \"unavailabilities\", []);\n    }\n\n    /**\n     * @param {\"t0\" | \"t1\" | \"t2\"} type\n     * @returns {number}\n     */\n    getRowTypeHeight(type) {\n        return {\n            t0: 24,\n            t1: 36,\n            t2: 16,\n        }[type];\n    }\n\n    getRowTitleStyle(row) {\n        return `grid-column: ${row.groupLevel + 2} / -1`;\n    }\n\n    openPlanDialogCallback() {}\n\n    getSelectCreateDialogProps(params) {\n        const domain = this.getPlanDialogDomain();\n        const schedule = this.model.getDialogContext(params);\n        return {\n            title: _t(\"Plan\"),\n            resModel: this.model.metaData.resModel,\n            context: schedule,\n            domain,\n            noCreate: !this.model.metaData.canCellCreate,\n            onSelected: (resIds) => {\n                if (resIds.length) {\n                    this.model.reschedule(resIds, schedule, this.openPlanDialogCallback.bind(this));\n                }\n            },\n        };\n    }\n\n    /**\n     * @param {Pill[]} pills\n     */\n    getTotalRow(pills) {\n        const preRow = {\n            groupLevel: 0,\n            id: \"[]\",\n            rows: [],\n            name: _t(\"Total\"),\n            recordIds: pills.map(({ record }) => record.id),\n        };\n\n        this.currentGridRow = 1;\n        const result = this.processRow(preRow, pills);\n        const [totalRow] = result.rows;\n        const allPills = this.rowPills[totalRow.id] || [];\n        const maxAggregateValue = Math.max(...allPills.map((p) => p.aggregateValue));\n\n        totalRow.factor = maxAggregateValue ? 90 / maxAggregateValue : 0;\n\n        return totalRow;\n    }\n\n    highlightPill(pillId, highlighted) {\n        const pill = this.pills[pillId];\n        if (!pill) {\n            return;\n        }\n        pill.highlighted = highlighted;\n        const pillWrapper = this.getPillWrapperEl(pillId);\n        pillWrapper?.classList.toggle(\"highlight\", highlighted);\n        pillWrapper?.classList.toggle(\n            \"o_connector_creator_highlight\",\n            highlighted && this.connectorDragState.dragging\n        );\n    }\n\n    initializeConnectors() {\n        for (const connectorId in this.connectors) {\n            this.deleteConnector(connectorId);\n        }\n    }\n\n    isPillSmall(pill) {\n        return this.cellPartWidth * pill.grid.column[1] < pill.displayName.length * 10;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    isDisabled(row = null) {\n        return this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    isHoverable(row) {\n        return !this.model.useSampleModel;\n    }\n\n    /**\n     * @param {Group[]} groups\n     * @returns {Group[]}\n     */\n    mergeGroups(groups) {\n        if (groups.length <= 1) {\n            return groups;\n        }\n        const index = Math.floor(groups.length / 2);\n        const left = this.mergeGroups(groups.slice(0, index));\n        const right = this.mergeGroups(groups.slice(index));\n        const group = right[0];\n        if (!group.break) {\n            const previousGroup = left.pop();\n            group.break = previousGroup.break;\n            group.grid.column[0] = previousGroup.grid.column[0];\n            group.aggregateValue = this.getAggregateValue(group, previousGroup);\n        }\n        return [...left, ...right];\n    }\n\n    onWillRender() {\n        if (this.noDisplayedConnectors && this.shouldRenderConnectors()) {\n            delete this.noDisplayedConnectors;\n            this.computeDerivedParams();\n        }\n\n        if (this.shouldComputeSomeWidths) {\n            this.computeSomeWidths();\n        }\n\n        if (this.shouldComputeSomeWidths || this.shouldComputeGridColumns) {\n            this.virtualGrid.setColumnsWidths(new Array(this.columnCount).fill(this.columnWidth));\n            this.computeVisibleColumns();\n        }\n\n        if (this.shouldComputeGridRows) {\n            this.virtualGrid.setRowsHeights(this.gridRows);\n            this.computeVisibleRows();\n        }\n\n        if (\n            this.shouldComputeSomeWidths ||\n            this.shouldComputeGridColumns ||\n            this.shouldComputeGridRows\n        ) {\n            delete this.shouldComputeSomeWidths;\n            delete this.shouldComputeGridColumns;\n            delete this.shouldComputeGridRows;\n            this.computeVisiblePills();\n            if (this.shouldRenderConnectors()) {\n                this.computeVisibleConnectors();\n            } else {\n                this.noDisplayedConnectors = true;\n            }\n        }\n\n        delete this.shouldComputeSomeWidths;\n        delete this.shouldComputeGridColumns;\n        delete this.shouldComputeGridRows;\n    }\n\n    pushGridRows(gridRows) {\n        for (const key of [\"t0\", \"t1\", \"t2\"]) {\n            if (key in gridRows) {\n                const types = new Array(gridRows[key]).fill(this.getRowTypeHeight(key));\n                this.gridRows.push(...types);\n            }\n        }\n    }\n\n    processPillsAsRows(row, pills) {\n        const rows = [];\n        const parsedId = JSON.parse(row.id);\n        if (pills.length) {\n            for (const pill of pills) {\n                const { id: resId, display_name: name } = pill.record;\n                const subRow = {\n                    id: JSON.stringify([...parsedId, { id: resId }]),\n                    resId,\n                    name,\n                    groupLevel: row.groupLevel + 1,\n                    recordIds: [resId],\n                    fromServer: row.fromServer,\n                    parentResId: row.resId ?? row.parentResId,\n                    parentGroupedField: row.groupedByField || row.parentGroupedField,\n                };\n                const res = this.processRow(subRow, [pill], false);\n                rows.push(...res.rows);\n            }\n        } else {\n            const subRow = {\n                id: JSON.stringify([...parsedId, {}]),\n                resId: false,\n                name: \"\",\n                groupLevel: row.groupLevel + 1,\n                recordIds: [],\n                fromServer: row.fromServer,\n                parentResId: row.resId ?? row.parentResId,\n                parentGroupedField: row.groupedByField || row.parentGroupedField,\n            };\n            const res = this.processRow(subRow, [], false);\n            rows.push(...res.rows);\n        }\n\n        return rows;\n    }\n\n    /**\n     * @param {Row} row\n     * @param {Pill[]} pills\n     * @param {boolean} [processAsGroup=false]\n     */\n    processRow(row, pills, processAsGroup = true) {\n        const { dependencyField, displayUnavailability, fields } = this.model.metaData;\n        const { displayMode } = this.model.displayParams;\n        const {\n            consolidate,\n            fromServer,\n            groupedByField,\n            groupLevel,\n            id,\n            name,\n            parentResId,\n            parentGroupedField,\n            resId,\n            rows,\n            recordIds,\n            __extra__,\n        } = row;\n\n        // compute the subset pills at row level\n        const remainingPills = [];\n        let rowPills = [];\n        const groupPills = [];\n        const isMany2many = groupedByField && fields[groupedByField].type === \"many2many\";\n        for (const pill of pills) {\n            const { record } = pill;\n            const pushPill = recordIds.includes(record.id);\n            let keepPill = false;\n            if (pushPill && isMany2many) {\n                const value = record[groupedByField];\n                if (Array.isArray(value) && value.length > 1) {\n                    keepPill = true;\n                }\n            }\n            if (pushPill) {\n                const rowPill = { ...pill };\n                rowPills.push(rowPill);\n                groupPills.push(pill);\n            }\n            if (!pushPill || keepPill) {\n                remainingPills.push(pill);\n            }\n        }\n\n        if (displayMode === \"sparse\" && __extra__) {\n            const rows = this.processPillsAsRows(row, groupPills);\n            return { rows, pillsToProcess: remainingPills };\n        }\n\n        const isGroup = displayMode === \"sparse\" ? processAsGroup : Boolean(rows);\n\n        const gridRowTypes = isGroup ? { t0: 1 } : { t1: 1 };\n        if (rowPills.length) {\n            if (isGroup) {\n                if (this.shouldComputeAggregateValues(row)) {\n                    const groups = this.aggregatePills(rowPills, row);\n                    const maxAggregateValue = Math.max(\n                        ...groups.map((group) => group.aggregateValue)\n                    );\n                    rowPills = groups.map((group) =>\n                        this.getPillFromGroup(group, maxAggregateValue, consolidate)\n                    );\n                } else {\n                    rowPills = [];\n                }\n            } else {\n                const level = this.calculatePillsLevel(rowPills);\n                gridRowTypes.t1 = level;\n                if (!this.isTouchDevice) {\n                    gridRowTypes.t2 = 1;\n                }\n            }\n        }\n\n        const progressBar = this.getRowProgressBar(groupedByField, resId);\n        if (progressBar && this.isTouchDevice && (!gridRowTypes.t1 || gridRowTypes.t1 === 1)) {\n            // In mobile: rows span over 2 rows to alllow progressbars to properly display\n            gridRowTypes.t1 = (gridRowTypes.t1 || 0) + 1;\n        }\n        if (row.id !== \"[]\") {\n            this.pushGridRows(gridRowTypes);\n        }\n\n        for (const rowPill of rowPills) {\n            rowPill.id = `__pill__${this.nextPillId++}`;\n            const pillFirstRow = this.currentGridRow + rowPill.level;\n            rowPill.grid = {\n                ...rowPill.grid, // rowPill is a shallow copy of a prePill (possibly copied several times)\n                row: [pillFirstRow, pillFirstRow + 1],\n            };\n            if (!isGroup) {\n                const { record } = rowPill;\n                if (this.shouldRenderRecordConnectors(record)) {\n                    if (!this.mappingRecordToPillsByRow[record.id]) {\n                        this.mappingRecordToPillsByRow[record.id] = {\n                            masterIds: record[dependencyField],\n                            pills: {},\n                        };\n                    }\n                    this.mappingRecordToPillsByRow[record.id].pills[id] = rowPill;\n                    if (!this.mappingRowToPillsByRecord[id]) {\n                        this.mappingRowToPillsByRecord[id] = {};\n                    }\n                    this.mappingRowToPillsByRecord[id][record.id] = rowPill;\n                }\n            }\n            rowPill.rowId = id;\n            this.pills[rowPill.id] = rowPill;\n        }\n\n        this.rowPills[id] = rowPills; // all row pills\n\n        const subRowsCount = Object.values(gridRowTypes).reduce((acc, val) => acc + val, 0);\n        /** @type {Row} */\n        const processedRow = {\n            cellColors: {},\n            fromServer,\n            groupedByField,\n            groupLevel,\n            id,\n            isGroup,\n            name,\n            progressBar,\n            resId,\n            grid: {\n                row: [this.currentGridRow, this.currentGridRow + subRowsCount],\n            },\n        };\n        if (displayUnavailability && !isGroup) {\n            processedRow.unavailabilities = this.getRowUnavailabilities(\n                parentGroupedField || groupedByField,\n                parentResId ?? resId\n            );\n        }\n\n        this.rowByIds[id] = processedRow;\n\n        this.currentGridRow += subRowsCount;\n\n        const field = this.model.metaData.thumbnails[groupedByField];\n        if (field) {\n            const model = this.model.metaData.fields[groupedByField].relation;\n            processedRow.thumbnailUrl = url(\"/web/image\", {\n                model,\n                id: resId,\n                field,\n            });\n        }\n\n        const result = { rows: [processedRow], pillsToProcess: remainingPills };\n\n        if (!this.model.isClosed(id)) {\n            if (rows) {\n                let pillsToProcess = groupPills;\n                for (const subRow of rows) {\n                    const res = this.processRow(subRow, pillsToProcess);\n                    result.rows.push(...res.rows);\n                    pillsToProcess = res.pillsToProcess;\n                }\n            } else if (displayMode === \"sparse\" && processAsGroup) {\n                const rows = this.processPillsAsRows(row, groupPills);\n                result.rows.push(...rows);\n            }\n        }\n\n        return result;\n    }\n\n    /**\n     * @param {string} [groupedByField]\n     * @param {false|number} [resId]\n     * @returns {{ start: DateTime, stop: DateTime }[]}\n     */\n    _getRowUnavailabilities(groupedByField, resId) {\n        const { unavailabilities } = this.model.data;\n        if (groupedByField) {\n            return unavailabilities[groupedByField]?.[resId ?? false] || [];\n        }\n        return unavailabilities.__default?.false || [];\n    }\n\n    /**\n     * @param {Object} params\n     * @param {Element} params.pill\n     * @param {number} params.diff\n     * @param {\"start\" | \"end\"} params.direction\n     */\n    async resizePillDrop({ pill, diff, direction }) {\n        const { dateStartField, dateStopField, scale } = this.model.metaData;\n        const { cellTime, time } = scale;\n        const { record } = this.pills[pill.dataset.pillId];\n        const params = this.getScheduleParams(pill);\n\n        if (direction === \"start\") {\n            params.start = dateAddFixedOffset(record[dateStartField], { [time]: cellTime * diff });\n        } else {\n            params.stop = dateAddFixedOffset(record[dateStopField], { [time]: cellTime * diff });\n        }\n        const schedule = this.model.getSchedule(params);\n\n        await this.model.reschedule(record.id, schedule, this.openPlanDialogCallback);\n    }\n\n    /**\n     * @param {Partial<ConnectorProps>} params\n     * @param {PillId | null} [sourceId=null]\n     * @param {PillId | null} [targetId=null]\n     */\n    setConnector(params, sourceId = null, targetId = null) {\n        const connectorParams = { ...params };\n        const connectorId = params.id || `__connector__${this.nextConnectorId++}`;\n\n        if (sourceId) {\n            connectorParams.sourcePoint = () => this.getPoint(sourceId, true);\n        }\n\n        if (targetId) {\n            connectorParams.targetPoint = () => this.getPoint(targetId, false);\n        }\n\n        if (this.connectors[connectorId]) {\n            Object.assign(this.connectors[connectorId], connectorParams);\n        } else {\n            this.connectors[connectorId] = {\n                id: connectorId,\n                highlighted: false,\n                displayButtons: false,\n                ...connectorParams,\n            };\n            this.mappingConnectorToPills[connectorId] = {\n                sourcePillId: sourceId,\n                targetPillId: targetId,\n            };\n        }\n\n        if (sourceId) {\n            if (!this.mappingPillToConnectors[sourceId]) {\n                this.mappingPillToConnectors[sourceId] = [];\n            }\n            this.mappingPillToConnectors[sourceId].push(connectorId);\n        }\n\n        if (targetId) {\n            if (!this.mappingPillToConnectors[targetId]) {\n                this.mappingPillToConnectors[targetId] = [];\n            }\n            this.mappingPillToConnectors[targetId].push(connectorId);\n        }\n    }\n\n    /**\n     * @param {HTMLElement} [pillEl]\n     */\n    setStickyPill(pillEl) {\n        this.stickyPillId = pillEl ? pillEl.dataset.pillId : null;\n    }\n\n    /**\n     * @param {Row} row\n     */\n    shouldComputeAggregateValues(row) {\n        return true;\n    }\n\n    shouldMergeGroups() {\n        return true;\n    }\n\n    /**\n     * Returns whether connectors should be rendered or not.\n     * The connectors won't be rendered on sampleData as we can't be sure that data are coherent.\n     * The connectors won't be rendered on mobile as the usability is not guarantied.\n     *\n     * @return {boolean}\n     */\n    shouldRenderConnectors() {\n        return (\n            this.model.metaData.dependencyField && !this.model.useSampleModel && !this.env.isSmall\n        );\n    }\n\n    /**\n     * Returns whether connectors should be rendered on particular records or not.\n     * This method is intended to be overridden in particular modules in order to set particular record's condition.\n     *\n     * @param {RelationalRecord} record\n     * @return {boolean}\n     */\n    shouldRenderRecordConnectors(record) {\n        return this.shouldRenderConnectors();\n    }\n\n    /**\n     * @param {ConnectorId | null} connectorId\n     * @param {boolean} highlighted\n     */\n    toggleConnectorHighlighting(connectorId, highlighted) {\n        const connector = this.connectors[connectorId];\n        if (!connector || (!connector.highlighted && !highlighted)) {\n            return;\n        }\n\n        connector.highlighted = highlighted;\n        connector.displayButtons = highlighted;\n\n        const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n\n        this.highlightPill(sourcePillId, highlighted);\n        this.highlightPill(targetPillId, highlighted);\n    }\n\n    /**\n     * @param {PillId} pillId\n     * @param {boolean} highlighted\n     */\n    togglePillHighlighting(pillId, highlighted) {\n        const pill = this.pills[pillId];\n        if (!pill || pill.highlighted === highlighted) {\n            return;\n        }\n\n        const { record } = pill;\n        const pillIdsToHighlight = new Set([pillId]);\n\n        if (record && this.shouldRenderRecordConnectors(record)) {\n            // Find other related pills\n            const { pills: relatedPills } = this.mappingRecordToPillsByRow[record.id];\n            for (const pill of Object.values(relatedPills)) {\n                pillIdsToHighlight.add(pill.id);\n            }\n\n            // Highlight related connectors\n            for (const [connectorId, connector] of Object.entries(this.connectors)) {\n                const ids = Object.values(this.getRecordIds(connectorId));\n                if (ids.includes(record.id)) {\n                    connector.highlighted = highlighted;\n                    connector.displayButtons = false;\n                }\n            }\n        }\n\n        // Highlight pills from found IDs\n        for (const id of pillIdsToHighlight) {\n            this.highlightPill(id, highlighted);\n        }\n    }\n\n    //-------------------------------------------------------------------------\n    // Handlers\n    //-------------------------------------------------------------------------\n\n    onCellClicked(rowId, col) {\n        if (!this.preventClick) {\n            this.preventClick = true;\n            setTimeout(() => (this.preventClick = false), 1000);\n            const { canCellCreate, canPlan } = this.model.metaData;\n            if (canPlan) {\n                this.onPlan(rowId, col, col);\n            } else if (canCellCreate) {\n                this.onCreate(rowId, col, col);\n            }\n        }\n    }\n\n    onCreate(rowId, startCol, stopCol) {\n        const { start, stop } = this.getColumnStartStop(startCol, stopCol);\n        const context = this.model.getDialogContext({\n            rowId,\n            start,\n            stop,\n            withDefault: true,\n        });\n        this.props.create(context);\n    }\n\n    onInteractionChange() {\n        let { dragAction, mode } = this.interaction;\n        if (mode === \"drag\") {\n            mode = dragAction;\n        }\n        if (this.gridRef.el) {\n            for (const [action, className] of INTERACTION_CLASSNAMES) {\n                this.gridRef.el.classList.toggle(className, mode === action);\n            }\n        }\n    }\n\n    onPointerLeave() {\n        this.throttledComputeHoverParams.cancel();\n\n        if (!this.isDragging) {\n            const hoveredConnectorId = this.hovered.connector?.dataset.connectorId;\n            this.toggleConnectorHighlighting(hoveredConnectorId, false);\n\n            const hoveredPillId = this.hovered.pill?.dataset.pillId;\n            this.togglePillHighlighting(hoveredPillId, false);\n        }\n\n        this.hovered.connector = null;\n        this.hovered.pill = null;\n        this.hovered.hoverable = null;\n\n        this.computeDerivedParamsFromHover();\n    }\n\n    /**\n     * Updates all hovered elements, then calls \"computeDerivedParamsFromHover\".\n     *\n     * @see computeDerivedParamsFromHover\n     * @param {Event} ev\n     */\n    computeHoverParams(ev) {\n        // Lazily compute elements from point as it is a costly operation\n        let els = null;\n        let position = {};\n        if (ev.type === \"scroll\") {\n            position = this.cursorPosition;\n        } else {\n            position.x = ev.clientX;\n            position.y = ev.clientY;\n            this.cursorPosition = position;\n        }\n        const pointedEls = () => els || (els = document.elementsFromPoint(position.x, position.y));\n\n        // To find hovered elements, also from pointed elements\n        const find = (selector) =>\n            ev.target.closest?.(selector) ||\n            pointedEls().find((el) => el.matches(selector)) ||\n            null;\n\n        this.hovered.connector = find(\".o_gantt_connector\");\n        this.hovered.hoverable = find(\".o_gantt_hoverable\");\n        this.hovered.pill = find(\".o_gantt_pill_wrapper\");\n\n        this.computeDerivedParamsFromHover();\n    }\n\n    /**\n     * @param {PointerEvent} ev\n     * @param {Pill} pill\n     */\n    onPillClicked(ev, pill) {\n        if (this.popover.isOpen) {\n            return;\n        }\n        this.popover.target = ev.target.closest(\".o_gantt_pill_wrapper\");\n        this.popover.open(this.popover.target, this.getPopoverProps(pill));\n    }\n\n    onPlan(rowId, startCol, stopCol) {\n        const { start, stop } = this.getColumnStartStop(startCol, stopCol);\n        this.dialogService.add(\n            SelectCreateDialog,\n            this.getSelectCreateDialogProps({ rowId, start, stop, withDefault: true })\n        );\n    }\n\n    getRecordIds(connectorId) {\n        const { sourcePillId, targetPillId } = this.mappingConnectorToPills[connectorId];\n        return {\n            masterId: this.pills[sourcePillId]?.record.id,\n            slaveId: this.pills[targetPillId]?.record.id,\n        };\n    }\n\n    /**\n     *\n     * @param {Object} params\n     * @param {ConnectorId} connectorId\n     */\n    onRemoveButtonClick(connectorId) {\n        const { masterId, slaveId } = this.getRecordIds(connectorId);\n        this.model.removeDependency(masterId, slaveId);\n    }\n    rescheduleAccordingToDependencyCallback(result) {\n        if (result[\"type\"] !== \"warning\" && \"old_vals_per_pill_id\" in result) {\n            this.model.toggleHighlightPlannedFilter(\n                Object.keys(result[\"old_vals_per_pill_id\"]).map(Number)\n            );\n        }\n        this.notificationFn?.();\n        this.notificationFn = this.notificationService.add(\n            markup(\n                `<i class=\"fa btn-link fa-check\"></i><span class=\"ms-1\">${escape(\n                    result[\"message\"]\n                )}</span>`\n            ),\n            {\n                type: result[\"type\"],\n                sticky: true,\n                buttons:\n                    result[\"type\"] === \"warning\"\n                        ? []\n                        : [\n                              {\n                                  name: \"Undo\",\n                                  icon: \"fa-undo\",\n                                  onClick: async () => {\n                                      const ids = Object.keys(result[\"old_vals_per_pill_id\"]).map(\n                                          Number\n                                      );\n                                      await this.orm.call(\n                                          this.model.metaData.resModel,\n                                          \"action_rollback_scheduling\",\n                                          [ids, result[\"old_vals_per_pill_id\"]]\n                                      );\n                                      this.notificationFn();\n                                      await this.model.fetchData();\n                                  },\n                              },\n                          ],\n            }\n        );\n    }\n\n    /**\n     *\n     * @param {\"forward\" | \"backward\"} direction\n     * @param {ConnectorId} connectorId\n     */\n    async onRescheduleButtonClick(direction, connectorId) {\n        const { masterId, slaveId } = this.getRecordIds(connectorId);\n        await this.model.rescheduleAccordingToDependency(\n            direction,\n            masterId,\n            slaveId,\n            this.rescheduleAccordingToDependencyCallback.bind(this)\n        );\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeyDown(ev) {\n        if (ev.key === \"Control\") {\n            this.prevDragAction =\n                this.interaction.dragAction === \"copy\" ? \"reschedule\" : this.interaction.dragAction;\n            this.interaction.dragAction = \"copy\";\n        }\n    }\n\n    /**\n     * @param {KeyboardEvent} ev\n     */\n    onWindowKeyUp(ev) {\n        if (ev.key === \"Control\") {\n            this.interaction.dragAction = this.prevDragAction || \"reschedule\";\n        }\n    }\n}\n", "import { Component, useState } from \"@odoo/owl\";\nimport { useDateTimePicker } from \"@web/core/datetime/datetime_hook\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { useDropdownState } from \"@web/core/dropdown/dropdown_hooks\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { formatDate } from \"@web/core/l10n/dates\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport {\n    diffColumn,\n    getRangeFromDate,\n    localStartOf,\n    useGanttResponsivePopover,\n} from \"./gantt_helpers\";\n\nconst { DateTime } = luxon;\n\nconst KEYS = [\"startDate\", \"stopDate\", \"rangeId\", \"focusDate\"];\n\nexport class GanttRendererControls extends Component {\n    static template = \"web_gantt.GanttRendererControls\";\n    static components = {\n        Dropdown,\n        DropdownItem,\n    };\n    static props = [\"model\", \"displayExpandCollapseButtons\", \"focusToday\", \"getCurrentFocusDate\"];\n    static toolbarContentTemplate = \"web_gantt.GanttRendererControls.ToolbarContent\";\n    static rangeMenuTemplate = \"web_gantt.GanttRendererControls.RangeMenu\";\n\n    setup() {\n        this.model = this.props.model;\n        this.updateMetaData = debounce(() => this.model.fetchData(this.makeParams()), 500);\n\n        const { metaData } = this.model;\n        this.state = useState({\n            scaleIndex: this.getScaleIndex(metaData.scale.id),\n            ...pick(metaData, ...KEYS),\n        });\n        this.pickerValues = useState({\n            startDate: metaData.startDate,\n            stopDate: metaData.stopDate,\n        });\n        this.scalesRange = { min: 0, max: Object.keys(metaData.scales).length - 1 };\n\n        const getPickerProps = (key) => ({ type: \"date\", value: this.pickerValues[key] });\n        this.startPicker = useDateTimePicker({\n            target: \"start-picker\",\n            onApply: (date) => {\n                this.pickerValues.startDate = date;\n                if (this.pickerValues.stopDate < date) {\n                    this.pickerValues.stopDate = date;\n                } else if (date.plus({ year: 10, day: -1 }) < this.pickerValues.stopDate) {\n                    this.pickerValues.stopDate = date.plus({ year: 10, day: -1 });\n                }\n            },\n            get pickerProps() {\n                return getPickerProps(\"startDate\");\n            },\n            createPopover: (...args) => useGanttResponsivePopover(_t(\"Gantt start date\"), ...args),\n            ensureVisibility: () => false,\n        });\n        this.stopPicker = useDateTimePicker({\n            target: \"stop-picker\",\n            onApply: (date) => {\n                this.pickerValues.stopDate = date;\n                if (date < this.pickerValues.startDate) {\n                    this.pickerValues.startDate = date;\n                } else if (this.pickerValues.startDate.plus({ year: 10, day: -1 }) < date) {\n                    this.pickerValues.startDate = date.minus({ year: 10, day: -1 });\n                }\n            },\n            get pickerProps() {\n                return getPickerProps(\"stopDate\");\n            },\n            createPopover: (...args) => useGanttResponsivePopover(_t(\"Gantt stop date\"), ...args),\n            ensureVisibility: () => false,\n        });\n\n        this.dropdownState = useDropdownState();\n    }\n\n    get dateDescription() {\n        const { focusDate, rangeId } = this.state;\n        switch (rangeId) {\n            case \"quarter\":\n                return focusDate.toFormat(`Qq yyyy`);\n            case \"day\":\n                return formatDate(focusDate);\n            default:\n                return this.model.metaData.scales[rangeId].groupHeaderFormatter(\n                    focusDate,\n                    this.env\n                );\n        }\n    }\n\n    getFormattedDate(date) {\n        return formatDate(date);\n    }\n\n    getScaleIdFromIndex(index) {\n        const keys = Object.keys(this.model.metaData.scales);\n        return keys[keys.length - 1 - index];\n    }\n\n    getScaleIndex(scaleId) {\n        const keys = Object.keys(this.model.metaData.scales);\n        return keys.length - 1 - keys.findIndex((id) => id === scaleId);\n    }\n\n    getScaleIndexFromRangeId(rangeId) {\n        const { ranges } = this.model.metaData;\n        const scaleId = ranges[rangeId].scaleId;\n        return this.getScaleIndex(scaleId);\n    }\n\n    /**\n     * @param {1|-1} inc\n     */\n    incrementScale(inc) {\n        if (\n            inc === 1\n                ? this.state.scaleIndex < this.scalesRange.max\n                : this.scalesRange.min < this.state.scaleIndex\n        ) {\n            this.state.scaleIndex += inc;\n            this.updateMetaData();\n        }\n    }\n\n    isSelected(rangeId) {\n        if (rangeId === \"custom\") {\n            return (\n                this.state.rangeId === rangeId ||\n                !localStartOf(this.state.focusDate, this.state.rangeId).equals(\n                    localStartOf(DateTime.now(), this.state.rangeId)\n                )\n            );\n        }\n        return (\n            this.state.rangeId === rangeId &&\n            localStartOf(this.state.focusDate, rangeId).equals(\n                localStartOf(DateTime.now(), rangeId)\n            )\n        );\n    }\n\n    makeParams() {\n        return {\n            currentFocusDate: this.props.getCurrentFocusDate(),\n            scaleId: this.getScaleIdFromIndex(this.state.scaleIndex),\n            ...pick(this.state, ...KEYS),\n        };\n    }\n\n    onApply() {\n        this.state.startDate = this.pickerValues.startDate;\n        this.state.stopDate = this.pickerValues.stopDate;\n        this.state.rangeId = \"custom\";\n        this.updateMetaData();\n        this.dropdownState.close();\n    }\n\n    onTodayClicked() {\n        const success = this.props.focusToday();\n        if (success) {\n            return;\n        }\n        this.state.focusDate = DateTime.local().startOf(\"day\");\n        if (this.state.rangeId === \"custom\") {\n            const diff = diffColumn(this.state.startDate, this.state.stopDate, \"day\");\n            const n = Math.floor(diff / 2);\n            const m = diff - n;\n            this.state.startDate = this.state.focusDate.minus({ day: n });\n            this.state.stopDate = this.state.focusDate.plus({ day: m - 1 });\n        } else {\n            this.state.startDate = this.state.focusDate.startOf(this.state.rangeId);\n            this.state.stopDate = this.state.focusDate.endOf(this.state.rangeId).startOf(\"day\");\n        }\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectRange(direction) {\n        const sign = direction === \"next\" ? 1 : -1;\n        const { focusDate, rangeId, startDate, stopDate } = this.state;\n        if (rangeId === \"custom\") {\n            const diff = diffColumn(startDate, stopDate, \"day\") + 1;\n            this.state.focusDate = focusDate.plus({ day: sign * diff });\n            this.state.startDate = startDate.plus({ day: sign * diff });\n            this.state.stopDate = stopDate.plus({ day: sign * diff });\n        } else {\n            Object.assign(\n                this.state,\n                getRangeFromDate(rangeId, focusDate.plus({ [rangeId]: sign }))\n            );\n        }\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectRangeId(rangeId) {\n        Object.assign(this.state, getRangeFromDate(rangeId, DateTime.now().startOf(\"day\")));\n        this.state.scaleIndex = this.getScaleIndexFromRangeId(rangeId);\n        this.updatePickerValues();\n        this.updateMetaData();\n    }\n\n    selectScale(index) {\n        this.state.scaleIndex = Number(index);\n        this.updateMetaData();\n    }\n\n    updatePickerValues() {\n        this.pickerValues.startDate = this.state.startDate;\n        this.pickerValues.stopDate = this.state.stopDate;\n    }\n}\n", "import { Component } from \"@odoo/owl\";\n\nexport class GanttResizeBadge extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                position: {\n                    type: Object,\n                    shape: {\n                        top: Number,\n                        right: { type: Number, optional: true },\n                        left: { type: Number, optional: true },\n                    },\n                    optional: true,\n                },\n                diff: { type: Number, optional: true },\n                scale: { type: String, optional: true },\n            },\n        },\n    };\n    static template = \"web_gantt.GanttResizeBadge\";\n\n    get diff() {\n        return this.props.reactive.diff || 0;\n    }\n\n    get diffText() {\n        const { diff, props } = this;\n        const prefix = this.diff > 0 ? \"+\" : \"\";\n        return `${prefix}${diff} ${props.reactive.scale}`;\n    }\n\n    get positionStyle() {\n        const { position } = this.props.reactive;\n        const style = [`top:${position.top}px`];\n        if (\"left\" in position) {\n            style.push(`left:${position.left}px`);\n        } else {\n            style.push(`right:${position.right}px`);\n        }\n        return style.join(\";\");\n    }\n}\n", "import { Component } from \"@odoo/owl\";\nimport { hasTouch, isMobileOS } from \"@web/core/browser/feature_detection\";\n\nexport class GanttRowProgressBar extends Component {\n    static props = {\n        reactive: {\n            type: Object,\n            shape: {\n                hoveredRowId: [String, { value: null }],\n            },\n        },\n        rowId: String,\n        progressBar: {\n            type: Object,\n            shape: {\n                max_value: Number,\n                max_value_formatted: String,\n                ratio: Number,\n                value_formatted: String,\n                warning: { type: String, optional: true },\n                \"*\": true,\n            },\n        },\n    };\n    static template = \"web_gantt.GanttRowProgressBar\";\n\n    get show() {\n        const { reactive, rowId } = this.props;\n        return reactive.hoveredRowId === rowId || isMobileOS() || hasTouch();\n    }\n\n    get status() {\n        const { ratio } = this.props.progressBar;\n        return ratio > 100 ? \"danger\" : ratio > 0 ? \"success\" : null;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\n\nfunction _mockGetGanttData(params) {\n    const lazy = !params.limit && !params.offset && params.groupby.length === 1;\n    let { groups, length } = this._mockWebReadGroup({\n        ...params,\n        lazy,\n        fields: [\"__record_ids:array_agg(id)\"],\n    });\n    if (params.limit) {\n        // we don't care about pager feature in sample mode\n        // but we want to present something coherent\n        groups = groups.slice(0, params.limit);\n        length = groups.length;\n    }\n    groups.forEach((g) => (g.__record_ids = g.id)); // the sample server does not use the key __record_ids\n\n    const recordIds = [];\n    for (const group of groups) {\n        recordIds.push(...(group.__record_ids || []));\n    }\n\n    const { records } = this._mockWebSearchReadUnity({\n        model: params.model,\n        domain: [[\"id\", \"in\", recordIds]],\n        context: params.context,\n        specification: params.read_specification,\n    });\n\n    const unavailabilities = {};\n    for (const fieldName of params.unavailability_fields || []) {\n        unavailabilities[fieldName] = {};\n    }\n\n    const progress_bars = {};\n    for (const fieldName of params.progress_bar_fields || []) {\n        progress_bars[fieldName] = {};\n    }\n\n    return { groups, length, records, unavailabilities, progress_bars };\n}\n\nregistry.category(\"sample_server\").add(\"get_gantt_data\", _mockGetGanttData);\n", "import { registry } from \"@web/core/registry\";\nimport { scrollSymbol } from \"@web/search/action_hook\";\nimport { GanttArchParser } from \"./gantt_arch_parser\";\nimport { GanttController } from \"./gantt_controller\";\nimport { GanttModel } from \"./gantt_model\";\nimport { GanttRenderer } from \"./gantt_renderer\";\nimport { omit } from \"@web/core/utils/objects\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const ganttView = {\n    type: \"gantt\",\n    Controller: GanttController,\n    Renderer: GanttRenderer,\n    Model: GanttModel,\n    ArchParser: GanttArchParser,\n    searchMenuTypes: [\"filter\", \"groupBy\", \"favorite\"],\n    buttonTemplate: \"web_gantt.GanttView.Buttons\",\n\n    props: (genericProps, view, config) => {\n        const modelParams = {};\n        let scrollPosition;\n        if (genericProps.state) {\n            scrollPosition = genericProps.state[scrollSymbol];\n            modelParams.metaData = genericProps.state.metaData;\n            modelParams.displayParams = genericProps.state.displayParams;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const parser = new view.ArchParser();\n            const archInfo = parser.parse(arch);\n\n            let formViewId = archInfo.formViewId;\n            if (!formViewId) {\n                const formView = config.views.find((v) => v[1] === \"form\");\n                if (formView) {\n                    formViewId = formView[0];\n                }\n            }\n\n            modelParams.metaData = {\n                ...omit(archInfo, \"displayMode\"),\n                fields,\n                resModel,\n                formViewId,\n            };\n            modelParams.displayParams = {\n                displayMode: archInfo.displayMode,\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n            scrollPosition,\n        };\n    },\n};\n\nviewRegistry.add(\"gantt\", ganttView);\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { INTERVALS, MODES, TIMELINES } from \"./cohort_model\";\n\nexport class CohortArchParser {\n    parse(arch, fields) {\n        const archInfo = {\n            fieldAttrs: {},\n            widgets: {},\n        };\n        visitXML(arch, (node) => {\n            switch (node.tagName) {\n                case \"cohort\": {\n                    if (node.hasAttribute(\"disable_linking\")) {\n                        archInfo.disableLinking = exprToBoolean(\n                            node.getAttribute(\"disable_linking\")\n                        );\n                    }\n                    const title = node.getAttribute(\"string\");\n                    if (title) {\n                        archInfo.title = title;\n                    }\n                    const dateStart = node.getAttribute(\"date_start\");\n                    if (dateStart) {\n                        archInfo.dateStart = dateStart;\n                        archInfo.dateStartString = fields[dateStart].string;\n                    } else {\n                        throw new Error(_t('Cohort view has not defined \"date_start\" attribute.'));\n                    }\n                    const dateStop = node.getAttribute(\"date_stop\");\n                    if (dateStop) {\n                        archInfo.dateStop = dateStop;\n                        archInfo.dateStopString = fields[dateStop].string;\n                    } else {\n                        throw new Error(_t('Cohort view has not defined \"date_stop\" attribute.'));\n                    }\n                    const mode = node.getAttribute(\"mode\") || \"retention\";\n                    if (mode && MODES.includes(mode)) {\n                        archInfo.mode = mode;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(mode)s is not a valid mode. Here are the modes: %(modes)s\",\n                                { mode, modes: MODES }\n                            )\n                        );\n                    }\n                    const timeline = node.getAttribute(\"timeline\") || \"forward\";\n                    if (timeline && TIMELINES.includes(timeline)) {\n                        archInfo.timeline = timeline;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(timeline)s is not a valid timeline. Here are the timelines: %(timelines)s\",\n                                { timeline, timelines: TIMELINES }\n                            )\n                        );\n                    }\n                    archInfo.measure = node.getAttribute(\"measure\") || \"__count\";\n                    const interval = node.getAttribute(\"interval\") || \"day\";\n                    if (interval && interval in INTERVALS) {\n                        archInfo.interval = interval;\n                    } else {\n                        throw new Error(\n                            _t(\n                                \"The argument %(interval)s is not a valid interval. Here are the intervals: %(intervals)s\",\n                                { interval, intervals: INTERVALS }\n                            )\n                        );\n                    }\n                    break;\n                }\n                case \"field\": {\n                    const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n\n                    archInfo.fieldAttrs[fieldName] = {};\n                    if (node.hasAttribute(\"string\")) {\n                        archInfo.fieldAttrs[fieldName].string = node.getAttribute(\"string\");\n                    }\n                    if (\n                        node.getAttribute(\"invisible\") === \"True\" ||\n                        node.getAttribute(\"invisible\") === \"1\"\n                    ) {\n                        archInfo.fieldAttrs[fieldName].isInvisible = true;\n                        break;\n                    }\n                    if (node.hasAttribute(\"widget\")) {\n                        archInfo.widgets[fieldName] = node.getAttribute(\"widget\");\n                    }\n                }\n            }\n        });\n        return archInfo;\n    }\n}\n", "/* @odoo-module */\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\n\nimport { Component, toRaw, useRef } from \"@odoo/owl\";\n\nexport class CohortController extends Component {\n    static template = \"web_cohort.CohortView\";\n    static components = { Layout, SearchBar, CogMenu };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        modelParams: Object,\n        Renderer: Function,\n        buttonTemplate: String,\n    };\n\n    setup() {\n        this.actionService = useService(\"action\");\n        this.model = useModelWithSampleData(this.props.Model, toRaw(this.props.modelParams));\n\n        useSetupAction({\n            rootRef: useRef(\"root\"),\n            getLocalState: () => {\n                return { metaData: this.model.metaData };\n            },\n            getContext: () => this.getContext(),\n        });\n    }\n\n    getContext() {\n        const { measure, interval } = this.model.metaData;\n        return { cohort_measure: measure, cohort_interval: interval };\n    }\n\n    /**\n     * @param {Object} row\n     */\n    onRowClicked(row) {\n        if (row.value === undefined || this.model.metaData.disableLinking) {\n            return;\n        }\n\n        const context = Object.assign({}, this.model.searchParams.context);\n        const domain = row.domain;\n        const views = {};\n        for (const [viewId, viewType] of this.env.config.views || []) {\n            views[viewType] = viewId;\n        }\n        function getView(viewType) {\n            return [context[`${viewType}_view_id`] || views[viewType] || false, viewType];\n        }\n        const actionViews = [getView(\"list\"), getView(\"form\")];\n        this.actionService.doAction({\n            type: \"ir.actions.act_window\",\n            name: this.model.metaData.title,\n            res_model: this.model.metaData.resModel,\n            views: actionViews,\n            view_mode: \"list\",\n            target: \"current\",\n            context: context,\n            domain: domain,\n        });\n    }\n}\n", "/* @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Race } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { computeReportMeasures, processMeasure } from \"@web/views/utils\";\nimport { browser } from \"@web/core/browser/browser\";\n\nexport const MODES = [\"retention\", \"churn\"];\nexport const TIMELINES = [\"forward\", \"backward\"];\nexport const INTERVALS = {\n    day: _t(\"Day\"),\n    week: _t(\"Week\"),\n    month: _t(\"Month\"),\n    year: _t(\"Year\"),\n};\n\n/**\n * @typedef {import(\"@web/search/search_model\").SearchParams} SearchParams\n */\n\nexport class CohortModel extends Model {\n    /**\n     * @override\n     */\n    setup(params) {\n        // concurrency management\n        this.keepLast = new KeepLast();\n        this.race = new Race();\n        const _load = this._load.bind(this);\n        this._load = (...args) => {\n            return this.race.add(_load(...args));\n        };\n\n        this.metaData = params;\n        this.data = null;\n        this.searchParams = null;\n        this.intervals = INTERVALS;\n\n        const activeInterval = browser.localStorage.getItem(this.storageKey) || params.interval;\n        if (Object.keys(this.intervals).includes(activeInterval)) {\n            this.metaData.interval = activeInterval;\n        }\n    }\n\n    /**\n     * @param {SearchParams} searchParams\n     */\n    load(searchParams) {\n        const { comparison, context, domain } = searchParams;\n        this.searchParams = { context };\n        if (comparison) {\n            this.searchParams.domains = comparison.domains;\n        } else {\n            this.searchParams.domains = [{ arrayRepr: domain, description: null }];\n        }\n        const { cohort_interval, cohort_measure } = searchParams.context;\n        this.metaData.interval = cohort_interval || this.metaData.interval;\n\n        this.metaData.measure = processMeasure(cohort_measure) || this.metaData.measure;\n        this.metaData.measures = computeReportMeasures(\n            this.metaData.fields,\n            this.metaData.fieldAttrs,\n            [this.metaData.measure],\n            { sumAggregatorOnly: true }\n        );\n        return this._load(this.metaData);\n    }\n\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.data.some((data) => data.rows.length > 0);\n    }\n\n    /**\n     * @param {Object} params\n     */\n    async updateMetaData(params) {\n        Object.assign(this.metaData, params);\n        browser.localStorage.setItem(this.storageKey, this.metaData.interval);\n        await this._load(this.metaData);\n        this.notify();\n    }\n\n    //--------------------------------------------------------------------------\n    // Protected\n    //--------------------------------------------------------------------------\n\n    /**\n     * @protected\n     * @param {Object} metaData\n     */\n    async _load(metaData) {\n        this.data = await this.keepLast.add(this._fetchData(metaData));\n        for (const i in this.data) {\n            this.data[i].title = this.searchParams.domains[i].description;\n            this.data[i].rows.forEach((row) => {\n                row.columns = row.columns.filter((col) => col.percentage !== \"\");\n            });\n        }\n    }\n\n    /**\n     * @protected\n     * @param {Object} metaData\n     */\n    async _fetchData(metaData) {\n        return Promise.all(\n            this.searchParams.domains.map(({ arrayRepr: domain }) => {\n                return this.orm.call(metaData.resModel, \"get_cohort_data\", [], {\n                    date_start: metaData.dateStart,\n                    date_stop: metaData.dateStop,\n                    measure: metaData.measure,\n                    interval: metaData.interval,\n                    domain: domain,\n                    mode: metaData.mode,\n                    timeline: metaData.timeline,\n                    context: this.searchParams.context,\n                });\n            })\n        );\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { formatPercentage } from \"@web/views/fields/formatters\";\nimport { registry } from \"@web/core/registry\";\n\nimport { Component } from \"@odoo/owl\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\nimport { download } from \"@web/core/network/download\";\nimport { ReportViewMeasures } from \"@web/views/view_components/report_view_measures\";\n\nconst formatters = registry.category(\"formatters\");\n\nexport class CohortRenderer extends Component {\n    static components = { Dropdown, DropdownItem, ViewScaleSelector, ReportViewMeasures };\n    static template = \"web_cohort.CohortRenderer\";\n    static props = [\"class\", \"model\", \"onRowClicked\"];\n\n    setup() {\n        this.model = this.props.model;\n    }\n\n    range(n) {\n        return Array.from({ length: n }, (_, i) => i);\n    }\n\n    getFormattedValue(value) {\n        const fieldName = this.model.metaData.measure;\n        const field = this.model.metaData.measures[fieldName];\n        let formatType = this.model.metaData.widgets[fieldName];\n        if (!formatType) {\n            const fieldType = field.type;\n            formatType = [\"many2one\", \"reference\"].includes(fieldType) ? \"integer\" : fieldType;\n        }\n        const formatter = formatters.get(formatType);\n        return formatter(value, field);\n    }\n\n    formatPercentage(value) {\n        return formatPercentage(value, { digits: [false, 1] });\n    }\n\n    getCellTitle(period, measure, count) {\n        return _t(\"Period: %(period)s\\n%(measure)s: %(count)s\", { period, measure, count });\n    }\n\n    get scales() {\n        return Object.fromEntries(\n            Object.entries(this.model.intervals).map(([s, d]) => [s, { description: d }])\n        );\n    }\n\n    /**\n     * @param {String} scale\n     */\n    setScale(scale) {\n        this.model.updateMetaData({\n            interval: scale,\n        });\n    }\n\n    /**\n     * @param {Object} param0\n     * @param {string} param0.measure\n     */\n    onMeasureSelected({ measure }) {\n        this.model.updateMetaData({ measure });\n    }\n\n    /**\n     * Export cohort data in Excel file\n     */\n    async downloadExcel() {\n        const {\n            title,\n            resModel,\n            interval,\n            measure,\n            measures,\n            dateStartString,\n            dateStopString,\n            timeline,\n        } = this.model.metaData;\n        const { domains } = this.model.searchParams;\n        const data = {\n            title: title,\n            model: resModel,\n            interval_string: this.model.intervals[interval].toString(), // intervals are lazy-translated\n            measure_string: measures[measure].string,\n            date_start_string: dateStartString,\n            date_stop_string: dateStopString,\n            timeline: timeline,\n            rangeDescription: domains[0].description,\n            report: this.model.data[0],\n            comparisonRangeDescription: domains[1] && domains[1].description,\n            comparisonReport: this.model.data[1],\n        };\n        this.env.services.ui.block();\n        try {\n            // FIXME: [SAD/JPP] some data seems to be missing from the export in master. (check the python)\n            await download({\n                url: \"/web/cohort/export\",\n                data: { data: JSON.stringify(data) },\n            });\n        } finally {\n            this.env.services.ui.unblock();\n        }\n    }\n}\n", "/* @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { CohortController } from \"./cohort_controller\";\nimport { CohortRenderer } from \"./cohort_renderer\";\nimport { CohortArchParser } from \"./cohort_arch_parser\";\nimport { CohortModel } from \"./cohort_model\";\n\nexport const cohortView = {\n    type: \"cohort\",\n    buttonTemplate: \"web_cohort.CohortView.Buttons\",\n    searchMenuTypes: [\"filter\", \"comparison\", \"favorite\"],\n    Model: CohortModel,\n    ArchParser: CohortArchParser,\n    Controller: CohortController,\n    Renderer: CohortRenderer,\n\n    props: (genericProps, view) => {\n        let modelParams;\n        if (genericProps.state) {\n            modelParams = genericProps.state.metaData;\n        } else {\n            const { arch, fields, resModel } = genericProps;\n            const { ArchParser } = view;\n            const archInfo = new ArchParser().parse(arch, fields);\n            modelParams = {\n                dateStart: archInfo.dateStart,\n                dateStartString: archInfo.dateStartString,\n                dateStop: archInfo.dateStop,\n                dateStopString: archInfo.dateStopString,\n                fieldAttrs: archInfo.fieldAttrs,\n                fields: fields,\n                interval: archInfo.interval,\n                measure: archInfo.measure,\n                mode: archInfo.mode,\n                resModel: resModel,\n                timeline: archInfo.timeline,\n                title: archInfo.title,\n                disableLinking: Boolean(archInfo.disableLinking),\n                widgets: archInfo.widgets,\n            };\n        }\n\n        return {\n            ...genericProps,\n            modelParams,\n            Model: view.Model,\n            Renderer: view.Renderer,\n            buttonTemplate: view.buttonTemplate,\n        };\n    },\n};\n\nregistry.category(\"views\").add(\"cohort\", cohortView);\n", "/** @odoo-module */\n\nimport { parseDate } from \"@web/core/l10n/dates\";\nimport { registry } from \"@web/core/registry\";\nimport { SampleServer } from \"@web/model/sample_server\";\n\n/**\n * This function mocks calls to the 'get_cohort_data' method. It is\n * registered to the SampleServer's mockRegistry, so it is called with a\n * SampleServer instance as \"this\".\n * @private\n * @param {Object} params\n * @param {string} params.model\n * @param {Object} params.kwargs\n * @returns {Object}\n */\nfunction _mockGetCohortData(params) {\n    const { model, date_start, interval, measure, mode, timeline } = params;\n\n    const columns_avg = {};\n    const rows = [];\n    let initialChurnValue = 0;\n\n    const groups = this._mockReadGroup({\n        model,\n        fields: [date_start],\n        groupBy: [date_start + \":\" + interval],\n    });\n    const totalCount = groups.length;\n    let totalValue = 0;\n    for (const group of groups) {\n        const format = SampleServer.FORMATS[interval];\n        const displayFormat = SampleServer.DISPLAY_FORMATS[interval];\n        const date = parseDate(group[date_start + \":\" + interval], { format });\n        const now = luxon.DateTime.local();\n        let colStartDate = date;\n        if (timeline === \"backward\") {\n            colStartDate = colStartDate.plus({ [`${interval}s`]: -15 });\n        }\n\n        let value =\n            measure === \"__count\"\n                ? this._getRandomInt(SampleServer.MAX_INTEGER)\n                : this._generateFieldValue(model, measure);\n        value = value || 25;\n        totalValue += value;\n        let initialValue = value;\n        let max = value;\n\n        const columns = [];\n        for (let column = 0; column <= 15; column++) {\n            if (!columns_avg[column]) {\n                columns_avg[column] = { percentage: 0, count: 0 };\n            }\n            if (colStartDate.plus({ [`${interval}s`]: column }) > now) {\n                columns.push({ value: \"-\", churn_value: \"-\", percentage: \"\" });\n                continue;\n            }\n            let colValue = 0;\n            if (max > 0) {\n                colValue = Math.min(Math.round(Math.random() * max), max);\n                max -= colValue;\n            }\n            if (timeline === \"backward\" && column === 0) {\n                initialValue = Math.min(Math.round(Math.random() * value), value);\n                initialChurnValue = value - initialValue;\n            }\n            const previousValue = column === 0 ? initialValue : columns[column - 1].value;\n            const remainingValue = previousValue - colValue;\n            const previousChurnValue =\n                column === 0 ? initialChurnValue : columns[column - 1].churn_value;\n            const churn_value = colValue + previousChurnValue;\n            let percentage = value ? parseFloat(remainingValue / value) : 0;\n            if (mode === \"churn\") {\n                percentage = 1 - percentage;\n            }\n            percentage = Number((100 * percentage).toFixed(1));\n            columns_avg[column].percentage += percentage;\n            columns_avg[column].count += 1;\n            columns.push({\n                value: remainingValue,\n                churn_value,\n                percentage,\n                period: column, // used as a t-key but we don't care about value itself\n            });\n        }\n        const keepRow = columns.some((c) => c.percentage !== \"\");\n        if (keepRow) {\n            rows.push({ date: date.toFormat(displayFormat), value, columns });\n        }\n    }\n    const avg_value = totalCount ? totalValue / totalCount : 0;\n    const avg = { avg_value, columns_avg };\n    return { rows, avg };\n}\n\nregistry.category(\"sample_server\").add(\"get_cohort_data\", _mockGetCohortData);\n", "import { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { GraphController } from \"@web/views/graph/graph_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrGraphController extends GraphController {\n    static template = \"hr.GraphView\";\n    static components = { ...GraphController.components, HrActionHelper };\n}\nexport const HrGraphView = {\n    ...graphView,\n    Controller: HrGraphController,\n};\n\nregistry.category(\"views\").add(\"hr_graph_view\", HrGraphView);\n", "import { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { PivotController } from \"@web/views/pivot/pivot_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrPivotController extends PivotController {\n    static template = \"hr.PivotView\";\n    static components = { ...PivotController.components, HrActionHelper };\n}\nexport const HrPivotView = {\n    ...pivotView,\n    Controller: HrPivotController,\n};\n\nregistry.category(\"views\").add(\"hr_pivot_view\", HrPivotView);\n", "/** @odoo-module */\n\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { stringToOrderBy } from \"@web/search/utils/order_by\";\nimport { Field } from \"@web/views/fields/field\";\nimport { getActiveActions } from \"@web/views/utils\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\n\nexport class HierarchyArchParser {\n    parse(xmlDoc, models, modelName) {\n        const archInfo = {\n            activeActions: getActiveActions(xmlDoc),\n            defaultOrder: stringToOrderBy(xmlDoc.getAttribute(\"default_order\") || null),\n            draggable: false,\n            icon: \"fa-share-alt o_hierarchy_icon\",\n            parentFieldName: \"parent_id\",\n            fieldNodes: {},\n            templateDocs: {},\n            xmlDoc,\n        };\n        const fieldNextIds = {};\n        const fields = models[modelName].fields;\n\n        visitXML(xmlDoc, (node) => {\n            if (node.hasAttribute(\"t-name\")) {\n                archInfo.templateDocs[node.getAttribute(\"t-name\")] = node;\n                return;\n            }\n            if (node.tagName === \"hierarchy\") {\n                if (node.hasAttribute(\"parent_field\")) {\n                    const parentFieldName = node.getAttribute(\"parent_field\");\n                    if (!(parentFieldName in fields)) {\n                        throw new Error(`The parent field set (${parentFieldName}) is not defined in the model (${modelName}).`);\n                    } else if (fields[parentFieldName].type !== \"many2one\") {\n                        throw new Error(`Invalid parent field, it should be a Many2One field.`);\n                    } else if (fields[parentFieldName].relation !== modelName) {\n                        throw new Error(`Invalid parent field, the co-model should be same model than the current one (expected: ${modelName}).`);\n                    }\n                    archInfo.parentFieldName = parentFieldName;\n                }\n                if (node.hasAttribute(\"child_field\")) {\n                    const childFieldName = node.getAttribute(\"child_field\");\n                    if (!(childFieldName in fields)) {\n                        throw new Error(`The child field set (${childFieldName}) is not defined in the model (${modelName}).`);\n                    } else if (fields[childFieldName].type !== \"one2many\") {\n                        throw new Error(`Invalid child field, it should be a One2Many field.`);\n                    } else if (fields[childFieldName].relation !== modelName) {\n                        throw new Error(`Invalid child field, the co-model should be same model than the current one (expected: ${modelName}).`);\n                    }\n                    archInfo.childFieldName = childFieldName;\n                }\n                if (node.hasAttribute(\"draggable\")) {\n                    archInfo.draggable = exprToBoolean(node.getAttribute(\"draggable\"));\n                }\n                if (node.hasAttribute(\"icon\")) {\n                    archInfo.icon = node.getAttribute(\"icon\");\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldInfo = Field.parseFieldNode(node, models, modelName, \"hierarchy\");\n                const name = fieldInfo.name;\n                if (!(name in fieldNextIds)) {\n                    fieldNextIds[name] = 0;\n                }\n                const fieldId = `${name}_${fieldNextIds[name]++}`;\n                archInfo.fieldNodes[fieldId] = fieldInfo;\n                node.setAttribute(\"field_id\", fieldId);\n            }\n        });\n\n        const cardDoc = archInfo.templateDocs[\"hierarchy-box\"];\n        if (!cardDoc) {\n            throw new Error(\"Missing 'hierarchy-box' template.\");\n        }\n\n        return archInfo;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nimport { evaluateBooleanExpr } from \"@web/core/py_js/py\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useViewCompiler } from \"@web/views/view_compiler\";\n\nimport { HierarchyCompiler } from \"./hierarchy_compiler\";\nimport { getFormattedRecord } from \"@web/views/kanban/kanban_record\";\n\nexport class HierarchyCard extends Component {\n    static components = {\n        Record,\n        Field,\n        ViewButton,\n    };\n    static props = {\n        node: Object,\n        openRecord: Function,\n        archInfo: Object,\n        templates: Object,\n        classNames: { type: String, optional: true },\n    };\n    static defaultProps = {\n        classNames: \"\",\n    };\n    static template = \"web_hierarchy.HierarchyCard\";\n    static Compiler = HierarchyCompiler;\n\n    setup() {\n        const { templates } = this.props;\n        this.templates = useViewCompiler(this.constructor.Compiler, templates);\n        this.evaluateBooleanExpr = evaluateBooleanExpr;\n    }\n\n    get classNames() {\n        const classNames = [this.props.classNames];\n        if (this.props.node.nodes.length) {\n            classNames.push(\"o_hierarchy_node_unfolded\");\n        }\n        return classNames.join(\" \");\n    }\n\n    getRenderingContext(data) {\n        const record = getFormattedRecord(data.record);\n        return {\n            context: this.props.node.context,\n            JSON,\n            luxon,\n            record,\n            __comp__: Object.assign(Object.create(this), { this: this }),\n            __record__: data.record,\n        };\n    }\n\n    onGlobalClick(ev) {\n        if (ev.target.closest(\"button\")) {\n            return;\n        }\n        this.props.openRecord(this.props.node);\n    }\n\n    onClickArrowUp(ev) {\n        this.props.node.fetchParentNode();\n    }\n\n    onClickArrowDown(ev) {\n        if (this.props.node.nodes.length) {\n            this.props.node.collapseChildNodes();\n        } else {\n            this.props.node.showChildNodes();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport { KanbanCompiler } from \"@web/views/kanban/kanban_compiler\";\n\nexport class HierarchyCompiler extends KanbanCompiler {\n    /**\n     * @override\n     * @param {Element} el\n     * @param {Object} params\n     * @returns {Element}\n     */\n    compileField(el, params) {\n        const fieldName = el.getAttribute(\"name\");\n        return super.compileField(el, {\n            ...(params || {}),\n            recordExpr: \"__record__\",\n            dataPointIdExpr: \"__comp__.props.node.id\",\n            formattedValueExpr: `record['${fieldName}'].value`,\n        });\n    }\n\n    compileButton(el, params) {\n        return super.compileButton(el, {\n            ...(params || {}),\n            recordExpr: \"__record__\",\n        });\n    }\n\n    /**\n     * Allow access to the record during compilation, to properly evaluate\n     * invisible on any hierarchy card nodes declared in the view.\n     *\n     * @override\n     */\n    compileNode(node, params = {}, evalInvisible = true) {\n        return super.compileNode(\n            node,\n            {\n                ...params,\n                recordExpr: \"__record__\",\n            },\n            evalInvisible\n        );\n    }\n}\n", "/** @odoo-module */\n\nimport { Component, useRef } from \"@odoo/owl\";\n\nimport { useBus } from \"@web/core/utils/hooks\";\nimport { useModel } from \"@web/model/model\";\nimport { addFieldDependencies, extractFieldsFromArchInfo } from \"@web/model/relational_model/utils\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { Layout } from \"@web/search/layout\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\n\nexport class HierarchyController extends Component {\n    static components = {\n        Layout,\n        CogMenu,\n        SearchBar,\n    };\n    static props = {\n        ...standardViewProps,\n        Model: Function,\n        Renderer: Function,\n        buttonTemplate: String,\n        archInfo: Object,\n    };\n    static template = \"web_hierarchy.HierarchyView\";\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        const { parentFieldName, childFieldName } = this.props.archInfo;\n        const { activeFields, fields } = extractFieldsFromArchInfo(this.props.archInfo, this.props.fields);\n        addFieldDependencies(activeFields, fields, [{ name: parentFieldName }]);\n        this.model = useModel(this.props.Model, {\n            resModel: this.props.resModel,\n            activeFields,\n            defaultOrderBy: this.props.archInfo.defaultOrder,\n            fields,\n            parentFieldName,\n            childFieldName,\n        });\n        useBus(\n            this.model.bus,\n            \"update\",\n            () => {\n                this.render(true);\n            }\n        );\n        useViewButtons(this.rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: this.model.reload.bind(this.model),\n        });\n        this.searchBarToggler = useSearchBarToggler();\n    }\n    get displayNoContent() {\n        return this.model.resIds.length === 0;\n    }\n\n    async openRecord(node, mode) {\n        const activeIds = this.model.root.resIds;\n        this.props.selectRecord(node.resId, { activeIds, mode });\n    }\n\n    async beforeExecuteActionButton(clickParams) {}\n\n    async afterExecuteActionButton(clickParams) {}\n}\n", "/** @odoo-module */\n\nimport { Domain } from \"@web/core/domain\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { Model } from \"@web/model/model\";\nimport { orderByToString } from \"@web/search/utils/order_by\";\n\nlet nodeId = 0;\nlet forestId = 0;\nlet treeId = 0;\n\n/**\n * Get the id of the given many2one field value\n *\n * @param {false | [Number, string]} value many2one value\n * @returns {false | Number} id of the many2one\n */\nfunction getIdOfMany2oneField(value) {\n    return value && value[0];\n}\n\nexport class HierarchyNode {\n    /**\n     * Constructor of hierarchy node stored in hierarchy tree\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config\n     * @param {Object} data\n     * @param {HierarchyTree} tree\n     * @param {HierarchyNode} parentNode\n     * @param {Boolean} populateChildNodes\n     */\n    constructor(model, config, data, tree, parentNode = null, populateChildNodes = true) {\n        this.id = nodeId++;\n        this.data = data;\n        this.parentNode = parentNode;\n        this.tree = tree;\n        this.model = model;\n        this._config = config;\n        this.hidden = false;\n        tree.addNode(this);\n        if (populateChildNodes) {\n            this.populateChildNodes();\n        }\n    }\n\n    /**\n     * Get ancestor node\n     *\n     * @returns {HierarchyNode} ancestor node\n     */\n    get ancestorNode() {\n        return this.parentNode ? this.ancestorNode : this;\n    }\n\n    /**\n     * Is leaf?\n     *\n     * @returns {Boolean} False if the current node has node as child nodes, otherwise True.\n     */\n    get isLeaf() {\n        return !this.nodes.length;\n    }\n\n    /**\n     * Get forest of the current node\n     *\n     * @returns {HierarchyForest}\n     */\n    get forest() {\n        return this.tree.forest;\n    }\n\n    /**\n     * Get the resId of current node\n     *\n     * @returns {Number}\n     */\n    get resId() {\n        return this.data.id;\n    }\n\n    /**\n     * Get parent field name\n     *\n     * @returns {String}\n     */\n    get parentFieldName() {\n        return this.model.parentFieldName;\n    }\n\n    /**\n     * Get parent res id\n     *\n     * @returns {Number}\n     */\n    get parentResId() {\n        return this.parentNode?.resId || getIdOfMany2oneField(this.data[this.parentFieldName]);\n    }\n\n    /**\n     * Get child node res ids\n     *\n     * @returns {Number[]}\n     */\n    get childResIds() {\n        return this.nodes.length ? this.nodes.map((node) => node.resId) : this.data[this.childFieldName]?.map((d) => typeof d === \"number\" ? d : d.id) || [];\n    }\n\n    /**\n     * Get child field name\n     *\n     * @returns {String}\n     */\n    get childFieldName() {\n        return this.model.childFieldName || this.model.defaultChildFieldName;\n    }\n\n    /**\n     * Has child nodes?\n     *\n     * @returns {Boolean}\n     */\n    get hasChildren() {\n        return this.nodes.length > 0 || this.data[this.childFieldName]?.length > 0;\n    }\n\n    /**\n     * Can show parent node\n     *\n     * Knows if the parent node can be fetched and displayed inside the view\n     *\n     * @returns {Boolean} True if the current node has a parent node but it is not yet displayed and the data of the\n     *                    current node is not already displayed in another node.\n     */\n    get canShowParentNode() {\n        return Boolean(this.parentResId)\n            && !this.parentNode\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1;\n    }\n\n\n    /**\n     * Can show child nodes\n     *\n     * Knows if the child nodes can be fetched and displayed inside the view\n     *\n     * @returns {Boolean} True if the current node has child nodes but they are not yet displayed and the data of the\n     *                    current node is not already displayed in another node.\n     */\n    get canShowChildNodes() {\n        return this.hasChildren\n            && this.nodes.length === 0\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1;\n    }\n\n    get descendantNodes() {\n        const subNodes = [];\n        if (!this.isLeaf) {\n            subNodes.push(...this.nodes);\n            for (const node of this.nodes) {\n                if (node.descendantNodes.length) {\n                    subNodes.push(...node.descendantNodes);\n                }\n            }\n        }\n        return subNodes;\n    }\n\n    /**\n     * Get all descendants nodes parents. If the current node has descendants,\n     * it is also included in the result.\n     *\n     * @returns {Array} contains descendants parents in order of depth (closest\n     *          to root first).\n     */\n    get descendantsParentNodes() {\n        const descendantsParentNodes = [];\n        if (!this.isLeaf) {\n            descendantsParentNodes.push(this);\n            this.nodes.reduce((parents, node) => {\n                if (!node.isLeaf) {\n                    parents.push(...node.descendantsParentNodes);\n                }\n                return parents;\n            }, descendantsParentNodes);\n        }\n        return descendantsParentNodes;\n    }\n\n    /**\n     * Get all descendants nodes resIds\n     *\n     * @returns {Number[]}\n     */\n    get allSubsidiaryResIds() {\n        return this.descendantNodes.map((n) => n.resId);\n    }\n\n    /**\n     * Populate child nodes\n     *\n     * Uses to create child nodes of the current one according to its data.\n     */\n    populateChildNodes() {\n        this.nodes = [];\n        const children = this.data[this.childFieldName] || [];\n        if (\n            children.length\n            && children[0] instanceof Object\n            && this.tree.forest.resIds.filter((resId) => resId === this.resId).length === 1\n        ) {\n            this.createChildNodes(children);\n        }\n    }\n\n    /**\n     * create child nodes\n     *\n     * @param {Object[]} childNodesData data of child nodes to generate\n     */\n    createChildNodes(childNodesData) {\n        this.nodes = (childNodesData || this.data[this.childFieldName]).map(\n            (childData) =>\n                new HierarchyNode(\n                    this.model,\n                    this._config,\n                    childData,\n                    this.tree,\n                    this\n                )\n        );\n    }\n\n    removeParentNode() {\n        this.parentNode?.removeChildNode(this);\n        this.parentNode = null;\n        this.data[this.parentFieldName] = false;\n    }\n\n    /**\n     * Fetch parent node\n     */\n    async fetchParentNode() {\n        await this.model.fetchManager(this);\n    }\n\n    /**\n     * Fetch child nodes\n     */\n    async showChildNodes() {\n        await this.model.fetchSubordinates(this);\n    }\n\n    /**\n     * Collapse child nodes\n     *\n     * Removes the descendant nodes of the current one and stores\n     * the resIds of the child nodes in the data of the current one\n     * to know it has child nodes to be able to show them again\n     * when it is needed.\n     */\n    collapseChildNodes() {\n        const childrenData = [];\n        for (const childNode of this.nodes) {\n            childNode.data[this.childFieldName] = childNode.childResIds;\n            childrenData.push(childNode.data);\n        }\n        this.data[this.childFieldName] = childrenData;\n        this.removeChildNodes();\n        this.model.notify();\n    }\n\n    removeChildNode(node) {\n        node.removeChildNodes();\n        this.tree.removeNodes([node]);\n        this.nodes = this.nodes.filter((n) => n.id !== node.id);\n        this.data[this.childFieldName] = this.nodes.map((n) => n.data);\n    }\n\n    /**\n     * Remove descendant nodes of the current one\n     */\n    removeChildNodes() {\n        for (const childNode of this.nodes) {\n            if (!childNode.isLeaf) {\n                childNode.removeChildNodes();\n            }\n        }\n        this.tree.removeNodes(this.nodes);\n        this.nodes = [];\n    }\n\n    /**\n     * Set parent node to the current node\n     *\n     * @param {HierarchyNode} node parent node to set\n     */\n    setParentNode(node) {\n        this.parentNode = node;\n        node.addChildNode(this);\n        const tree = node.tree;\n        if (tree.root === this) {\n            tree.root = node;\n        } else if (this.tree.root === this) {\n            this.tree.removeRoot();\n            this.setTree(node.tree);\n        }\n    }\n\n    setTree(tree) {\n        this.tree = tree;\n        for (const childNode of this.nodes) {\n            childNode.setTree(tree);\n        }\n    }\n\n    /**\n     * Adds child node to the current node\n     *\n     * @param {HierarchyNode} node child node to add\n     */\n    addChildNode(node) {\n        this.nodes.push(node);\n        this.data[this.childFieldName].push(node.data);\n        this.tree.addNode(node);\n    }\n}\n\nexport class HierarchyTree {\n    /**\n     * Constructor\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config config of the model\n     * @param {Object} data root node data of the tree to create\n     * @param {HierarchyForest} forest hierarchy forest containing the tree to create\n     */\n    constructor(model, config, data, forest) {\n        this.id = treeId++;\n        this.nodePerNodeId = {};\n        this.forest = forest;\n        if (data) {\n            this.root = new HierarchyNode(model, config, data, this);\n            this.forest.nodePerNodeId = {\n                ...this.forest.nodePerNodeId,\n                ...this.nodePerNodeId,\n            };\n        }\n        this.model = model;\n        this._config = config;\n    }\n\n    /**\n     * Get node res ids inside the current tree\n     *\n     * @returns {Number}\n     */\n    get resIds() {\n        return Object.values(this.nodePerNodeId).map((node) => node.resId);\n    }\n\n    /**\n     * Add node inside the current tree\n     *\n     * @param {HierarchyNode} node node to add inside the current tree\n     */\n    addNode(node) {\n        this.nodePerNodeId[node.id] = node;\n        this.forest.addNode(node);\n    }\n\n    /**\n     * Remove nodes inside the current tree\n     *\n     * @param {HierarchyNode} nodes nodes to remove\n     */\n    removeNodes(nodes) {\n        const nodeIds = nodes.map((node) => node.id);\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId,]) => !nodeIds.includes(Number(nodeId))\n                )\n            );\n        this.forest.removeNodes(nodes);\n    }\n\n    removeRoot() {\n        this.forest.removeTree(this);\n    }\n}\n\nexport class HierarchyForest {\n    /**\n     *\n     * @param {HierarchyModel} model\n     * @param {Object} config model config\n     * @param {Object[]} data list of tree root nodes data\n     */\n    constructor(model, config, data) {\n        this.id = forestId++;\n        this.nodePerNodeId = {};\n        this.trees = data.map((d) => new HierarchyTree(model, config, d, this));\n        this.model = model;\n        this._config = config;\n    }\n\n    /**\n     * Get node res ids containing inside the current forest\n     *\n     * @returns {Number}\n     */\n    get resIds() {\n        return Object.values(this.nodePerNodeId).map((node) => node.resId);\n    }\n\n    /**\n     * Get root node of all trees inside the current forest\n     *\n     * @returns {HierarchyNode[]} root nodes\n     */\n    get rootNodes() {\n        return this.trees.map((t) => t.root);\n    }\n\n    /**\n     * Add a node inside the current forest\n     *\n     * @param {HierarchyNode} node node to add inside the current forest\n     */\n    addNode(node) {\n        this.nodePerNodeId[node.id] = node;\n    }\n\n    /**\n     * Removes nodes inside the current forest\n     *\n     * @param {HierarchyNode} nodes nodes to remove inside the current forest\n     */\n    removeNodes(nodes) {\n        const nodeIds = nodes.map((node) => node.id);\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId,]) => !nodeIds.includes(Number(nodeId))\n                )\n        );\n    }\n\n    addNewRootNode(node) {\n        const tree = new HierarchyTree(this.model, this._config, null, this);\n        tree.root = node;\n        node.tree = tree;\n        tree.addNode(node);\n        for (const subNode of node.descendantNodes) {\n            tree.addNode(subNode);\n        }\n        this.trees.push(tree);\n    }\n\n    removeTree(tree) {\n        this.nodePerNodeId = Object.fromEntries(\n            Object.entries(this.nodePerNodeId)\n                .filter(\n                    ([nodeId, ]) => !(nodeId in tree.nodePerNodeId)\n                )\n        );\n        this.trees = this.trees.filter((t) => t.id !== tree.id);\n    }\n}\n\nexport class HierarchyModel extends Model {\n    static services = [\"notification\"];\n\n    setup(params, { notification }) {\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        this.resModel = params.resModel;\n        this.fields = params.fields;\n        this.parentFieldName = params.parentFieldName;\n        this.childFieldName = params.childFieldName;\n        this.activeFields = params.activeFields;\n        this.defaultOrderBy = params.defaultOrderBy;\n        this.notification = notification;\n        this.config = {\n            domain: [],\n            isRoot: true,\n        };\n    }\n\n    /**\n     * Get parent field info\n     *\n     * @returns {Object} parent field info\n     */\n    get parentField() {\n        return this.fields[this.parentFieldName];\n    }\n\n    /**\n     * Get res ids of all nodes displayed in the view\n     *\n     * @returns {Number[]} resIds of all nodes displayed in the view\n     */\n    get resIds() {\n        return this.root?.resIds || [];\n    }\n\n    /**\n     * Get default child field name when no child field name is given to the view\n     *\n     * @returns {String} default child field name to use\n     */\n    get defaultChildFieldName() {\n        return \"__child_ids__\";\n    }\n\n    /**\n     * Get default domain to use, when no domain is given in the config\n     *\n     * @returns {import(\"@web/src/core/domain\").DomainListRepr} default domain\n     */\n    get defaultDomain() {\n        return [[this.parentFieldName, \"=\", false]];\n    }\n\n    /**\n     * Get the global domain of the view (which is the domain defined on the\n     * view without applying filters).\n     *\n     * @returns {import(\"@web/src/core/domain\").DomainListRepr} global domain\n     */\n    get globalDomain() {\n        if (!this.env.searchModel?.globalDomain.length) {\n            return [];\n        }\n        return new Domain(this.env.searchModel.globalDomain).toList(\n            this.env.searchModel.domainEvalContext\n        );\n    }\n\n    /**\n     * Get active fields name\n     *\n     * @returns {String[]} active fields name\n     */\n    get activeFieldNames() {\n        return Object.keys(this.activeFields);\n    }\n\n    /**\n     * Get fields to fetch\n     * @returns {String[]} fields to fetch\n     */\n    get fieldsToFetch() {\n        const fieldsToFetch = [\n            ...this.activeFieldNames,\n        ];\n        if (this.childFieldName) {\n            fieldsToFetch.push(this.childFieldName);\n        }\n        return fieldsToFetch;\n    }\n\n    get context() {\n        return {\n            bin_size: true,\n            ...(this.config.context || {}),\n        };\n    }\n\n    /**\n     * Load the config and data for hierarchy view\n     *\n     * @param {Object} params params to use to load data of hierarchy view\n     */\n    async load(params = {}) {\n        nodeId = forestId = treeId = 0;\n        const config = this._getNextConfig(this.config, params);\n        const data = await this.keepLast.add(this._loadData(config));\n        this.root = this._createRoot(config, data);\n        this.config = config;\n        this.notify();\n    }\n\n    /**\n     * Reload the current view with all currently loaded records\n     */\n    async reload() {\n        nodeId = forestId = treeId = 0;\n        const data = await this.keepLast.add(this._loadData(this.config, true));\n        this.root = this._createRoot(this.config, data);\n        this.notify({ scrollTarget: \"none\" });\n    }\n\n    /**\n     * @override\n     * Each notify should specify a scroll target (default is to scroll to the\n     * bottom).\n     */\n    notify(payload = { scrollTarget: \"bottom\" }) {\n        super.notify();\n        this.bus.trigger(\"hierarchyScrollTarget\", payload);\n    }\n\n    /**\n     * Fetch parent node of given node\n     * @param {HierarchyNode} node node to fetch its parent node\n     */\n    async fetchManager(node) {\n        if (this.root.trees.length > 1) { // reset the hierarchy\n            const treeExpanded = this._findTreeExpanded();\n            const resIdsToFetch = [node.parentResId, node.resId, ...node.allSubsidiaryResIds];\n            if (treeExpanded && treeExpanded.root.id !== node.id && treeExpanded.root.parentResId === node.parentResId) {\n                resIdsToFetch.push(...treeExpanded.root.allSubsidiaryResIds);\n            }\n            const config = {\n                ...this.config,\n                domain: [\"|\", [this.parentFieldName, \"=\", node.parentResId], [\"id\", \"in\", resIdsToFetch]],\n            }\n            const data = await this._loadData(config);\n            this.root = this._createRoot(config, data);\n            this.notify();\n            return;\n        }\n        const managerData = await this.keepLast.add(this._fetchManager(node));\n        if (managerData) {\n            const parentNode = new HierarchyNode(this, this.config, managerData, node.tree, null, false);\n            parentNode.createChildNodes();\n            node.setParentNode(parentNode);\n            this.notify();\n        }\n    }\n\n    /**\n     * Fetch child nodes of given node\n     *\n     * @param {HierarchyNode} node node to fetch its child nodes\n     */\n    async fetchSubordinates(node) {\n        const childFieldName = this.childFieldName || this.defaultChildFieldName;\n        const children = node.data[childFieldName];\n        if (children.length) {\n            const nodesToUpdate = [];\n            if (!(children[0] instanceof Object)) {\n                const allNodeResIds = this.root.resIds;\n                const existingChildResIds = children.filter((childResId) => allNodeResIds.includes(childResId))\n                if (existingChildResIds.length) { // special case with result found with the search view\n                    for (const tree of this.root.trees) {\n                        if (existingChildResIds.includes(tree.root.resId)) {\n                            nodesToUpdate.push(tree.root);\n                        }\n                    }\n                }\n                const data = await this.keepLast.add(this._fetchSubordinates(node, existingChildResIds));\n                if (data && data.length) {\n                    node.data[childFieldName] = data;\n                }\n            }\n            const nodeToCollapse = this._searchNodeToCollapse(node);\n            if (nodeToCollapse && !nodesToUpdate.includes(nodeToCollapse)) {\n                nodeToCollapse.collapseChildNodes();\n            }\n            node.populateChildNodes();\n            for (const n of nodesToUpdate) {\n                n.setParentNode(node);\n            }\n            this.notify();\n        }\n    }\n\n    /**\n     * Search node to collapse to be able to show the child nodes of node given in parameter\n     *\n     * @param {HierarchyNode} node node to show its child nodes.\n     * @returns {HierarchyNode | null} node found to collapse\n     */\n    _searchNodeToCollapse(node) {\n        const parentNode = node.parentNode;\n        let nodeToCollapse = null;\n        if (parentNode) {\n            nodeToCollapse = parentNode.nodes.find((n) => n.nodes.length);\n        } else {\n            const treeExpanded = this._findTreeExpanded();\n            if (treeExpanded) {\n                nodeToCollapse = treeExpanded.root;\n            }\n        }\n        return nodeToCollapse;\n    }\n\n    _findTreeExpanded() {\n        return this.root.trees.find((t) => t.root.nodes.length);\n    }\n\n    /**\n     * Get the next model config to use\n     *\n     * @param {Object} currentConfig current model config used\n     * @param {Object} params new params\n     * @returns {Object} new model config to use\n     */\n    _getNextConfig(currentConfig, params) {\n        const config = Object.assign({}, currentConfig);\n        config.context = \"context\" in params ? params.context : config.context;\n        if (\"domain\" in params) {\n            config.domain = params.domain;\n            if (this.isSearchDefaultOrEmpty() && config.context.hierarchy_res_id) {\n                config.domain = [[\"id\", \"=\", config.context.hierarchy_res_id]];\n                const globalDomain = this.globalDomain;\n                if (globalDomain.length) {\n                    config.domain = Domain.and([config.domain, globalDomain]);\n                }\n                // Just needed for the first load.\n                delete config.context.hierarchy_res_id;\n            }\n        }\n\n        // orderBy\n        config.orderBy = \"orderBy\" in params ? params.orderBy : config.orderBy;\n        // re-apply previous orderBy if not given (or no order)\n        if (!config.orderBy.length) {\n            config.orderBy = currentConfig.orderBy || [];\n        }\n        // apply default order if no order\n        if (this.defaultOrderBy && !config.orderBy.length) {\n            config.orderBy = this.defaultOrderBy;\n        }\n        return config;\n    }\n\n    /**\n     * Evaluate if the current search query is the default one.\n     *\n     * @returns {boolean}\n     */\n    isSearchDefaultOrEmpty() {\n        if (!this.env.searchModel) {\n            return true;\n        }\n        const isDisabledOptionalSearchMenuType = (type) => {\n            return (\n                [\"filter\", \"groupBy\", \"favorite\"].includes(type) &&\n                !this.env.searchModel.searchMenuTypes.has(type)\n            );\n        };\n        const activeSearchItems = this.env.searchModel.getSearchItems(\n            (item) => item.isActive && !isDisabledOptionalSearchMenuType(item.type)\n        );\n        if (!activeSearchItems.length) {\n            return true;\n        }\n        const defaultSearchItems = this.env.searchModel.getSearchItems(\n            (item) =>\n                item.isDefault &&\n                item.type !== \"favorite\" &&\n                !isDisabledOptionalSearchMenuType(item.type)\n        );\n        return JSON.stringify(defaultSearchItems) === JSON.stringify(activeSearchItems);\n    }\n\n    /**\n     * Load data for hierarchy view\n     *\n     * @param {Object} config model config\n     * @param {boolean} reload all currently loaded resIds instead of using\n     *        the config domain\n     * @returns {Object[]} main data for hierarchy view\n     */\n    async _loadData(config, reload = false) {\n        let onlyRoots = false;\n        let domain = config.domain;\n        const resIds = this.resIds;\n        if (reload && resIds.length > 0) {\n            domain = [[\"id\", \"in\", resIds]];\n        } else if (this.isSearchDefaultOrEmpty()) {\n            // If the current SearchModel query is the default one\n            // configured for the action or there is no search query, an\n            // additional constraint is added to only display \"root\"\n            // records (without a parent).\n            onlyRoots = true;\n            domain = !domain.length\n                ? this.defaultDomain\n                : Domain.and([this.defaultDomain, domain]).toList({});\n        }\n        const hierarchyRead = async () => {\n            return await this.orm.call(\n                this.resModel,\n                \"hierarchy_read\",\n                [\n                    domain,\n                    this.fieldsToFetch,\n                    this.parentFieldName,\n                    this.childFieldName,\n                    orderByToString(config.orderBy),\n                ],\n                { context: this.context }\n            );\n        };\n        let result = await hierarchyRead();\n        if (!result.length && onlyRoots) {\n            domain = config.domain;\n            result = await hierarchyRead();\n        }\n        return this._formatData(result);\n    }\n\n    _formatData(data) {\n        const dataStringified = JSON.stringify(data);\n        const recordsPerParentId = {};\n        const recordPerId = {};\n        for (const record of data) {\n            recordPerId[record.id] = record;\n            const parentId = getIdOfMany2oneField(record[this.parentFieldName]);\n            if (!(parentId.toString() in recordsPerParentId)) {\n                recordsPerParentId[parentId] = [];\n            }\n            recordsPerParentId[parentId].push(record);\n        }\n        const formattedData = [];\n        const recordIds = []; // to check if we have only one arborescence to display otherwise we display the data as the kanban view\n        for (const [parentId, records] of Object.entries(recordsPerParentId)) {\n            if (!parentId || !(parentId in recordPerId)) {\n                formattedData.push(...records);\n            } else {\n                const parentRecord = recordPerId[parentId];\n                if (recordIds.includes(parentRecord.id)) {\n                    return JSON.parse(dataStringified);\n                }\n                const ancestorId = getIdOfMany2oneField(parentRecord[this.parentFieldName]);\n                if (ancestorId in recordsPerParentId) {\n                    recordIds.push(...recordsPerParentId[ancestorId].map((r) => r.id));\n                }\n                parentRecord[this.childFieldName || this.defaultChildFieldName] = records;\n            }\n        }\n        if (!formattedData.length && data?.length) {\n            formattedData.push(recordPerId[Object.keys(recordsPerParentId)[0]]);\n        }\n        return formattedData;\n    }\n\n    /**\n     * Create forest\n     *\n     * @param {Object} config model config to use\n     * @param {Object[]} data root data\n     * @returns {HierarchyForest} forest hierarchy\n     */\n    _createRoot(config, data) {\n        return new HierarchyForest(this, config, data);\n    }\n\n    /**\n     * Fetch parent node and its children nodes data\n     *\n     * @param {HierarchyNode} node node to fetch its parent node\n     * @returns {Object} the parent node data with children data inside childFieldName\n     */\n    async _fetchManager(node, exclude_node=true) {\n        let domain = new Domain([\n            \"|\",\n                [\"id\", \"=\", node.parentResId],\n                [this.parentFieldName, \"=\", node.parentResId],\n        ]);\n        if (exclude_node) {\n            domain = Domain.and([\n                domain,\n                [[\"id\", \"!=\", node.resId]],\n            ])\n        }\n        const result = await this.orm.searchRead(\n            this.resModel,\n            domain.toList({}),\n            this.fieldsToFetch,\n            {\n                context: this.context,\n                order: orderByToString(this.config.orderBy),\n            },\n        );\n        let managerData = {};\n        const children = [];\n        for (const data of result) {\n            if (data.id === node.parentResId) {\n                managerData = data;\n            } else {\n                children.push(data);\n            }\n        }\n        if (!this.childFieldName) {\n            if (children.length) {\n                await this._fetchDescendants(children);\n            }\n        }\n        managerData[this.childFieldName || this.defaultChildFieldName] = children;\n        return managerData;\n    }\n\n    /**\n     * Fetch children nodes data for a given node\n     *\n     * @param {HierarchyNode} node node to fetch its children nodes\n     * @param {Array<number> | null} excludeResIds list of ids to exclude (because the nodes already exist)\n     * @returns {Object[]} list of child node data\n     */\n    async _fetchSubordinates(node, excludeResIds = null) {\n        let childrenResIds = node.data[this.childFieldName || this.defaultChildFieldName];\n        if (excludeResIds) {\n            childrenResIds = childrenResIds.filter((childResId) => !excludeResIds.includes(childResId));\n        }\n        const data = await this.orm.searchRead(\n            this.resModel,\n            [[\"id\", \"in\", childrenResIds]],\n            this.fieldsToFetch,\n            {\n                context: this.context,\n                order: orderByToString(this.config.orderBy),\n            },\n        )\n        if (!this.childFieldName) {\n            await this._fetchDescendants(data);\n        }\n        return data;\n    }\n\n    /**\n     * fetch descendants nodes resIds to know if the child nodes have descendants\n     *\n     * @param {Object[]} childrenData child nodes data to fetch its descendants\n     */\n    async _fetchDescendants(childrenData) {\n        const resIds = childrenData.map((d) => d.id);\n        if (resIds.length) {\n            const fetchChildren = await this.orm.readGroup(\n                this.resModel,\n                [[this.parentFieldName, \"in\", resIds]],\n                ['id:array_agg'],\n                [this.parentFieldName],\n                {\n                    context: this.context || {},\n                    orderby: orderByToString(this.config.orderBy),\n                },\n            );\n            const childIdsPerId = Object.fromEntries(\n                fetchChildren.map((r) => [r[this.parentFieldName][0], r.id])\n            );\n            for (const d of childrenData) {\n                if (d.id.toString() in childIdsPerId) {\n                    d[this.defaultChildFieldName] = childIdsPerId[d.id.toString()];\n                }\n            }\n        }\n    }\n\n    /**\n     * ORM call to update the parentId of a record during @see updateParentNode\n     * Can be overridden to not use \"write\".\n     *\n     * @param {HierarchyNode} node node related to the record which parentId\n     *        should be changed\n     * @param {Number} parentResId id of the new parent record\n     */\n    async updateParentId(node, parentResId = false) {\n        return this.orm.write(\n            this.resModel,\n            [node.resId],\n            { [this.parentFieldName]: parentResId },\n            { context: this.context }\n        );\n    }\n\n    /**\n     * @param {Number} nodeId of the node to update\n     * @param {Object} parentInfo\n     * @param {Number} [parentInfo.parentNodeId] nodeId of the parent\n     * @param {Number | false} [parentInfo.parentResId] resId of the parent\n     * @returns {Promise}\n     */\n    async updateParentNode(nodeId, { parentNodeId, parentResId }) {\n        const node = this.root.nodePerNodeId[nodeId];\n        const resId = node.resId;\n        // Validation.\n        if (!node) {\n            return;\n        }\n        const parentNode = parentNodeId ? this.root.nodePerNodeId[parentNodeId] : null;\n        parentResId = parentResId || parentNode?.resId || false;\n        const oldParentNode = node.parentNode;\n        if (\n            (parentNode && !this.validateUpdateParentNode(node, parentNode)) ||\n            parentNode?.resId === oldParentNode?.resId\n        ) {\n            return;\n        }\n        // Hide the node while waiting for the server response.\n        node.hidden = true;\n        this.notify({ scrollTarget: \"none\" });\n        // Update the parent server side.\n        await this.mutex.exec(async () => {\n            try {\n                await this.updateParentId(node, parentResId);\n            } catch (error) {\n                // Show the node again since the operation failed, don't update the view.\n                node.hidden = false;\n                this.notify({ scrollTarget: \"none\" });\n                throw error;\n            }\n        });\n        // Reload impacted records.\n        const domain = this.computeUpdateParentNodeDomain(node, parentResId, parentNode);\n        const data = await this.orm.searchRead(this.resModel, domain, this.fieldsToFetch, {\n            context: this.context,\n            order: orderByToString(this.config.orderBy),\n        });\n        const formattedData = this._formatData(data);\n        // Validate that data coming from the server is still compatible with the current\n        // configuration of the hierarchy.\n        for (const record of formattedData) {\n            if (getIdOfMany2oneField(record[this.parentFieldName]) !== parentResId) {\n                node.hidden = false;\n                this.notify({ scrollTarget: \"none\" });\n                this.notification.add(\n                    _t(\n                        `The parent of \"%s\" was successfully updated. Reloading records to account for other changes.`,\n                        node.data.display_name || node.data.name\n                    ),\n                    { type: \"success\" }\n                );\n                return this.reload();\n            }\n        }\n        // Handle the expanded tree.\n        let nodeToCollapse;\n        const treeExpanded = this._findTreeExpanded();\n        const expandedParentNodeIds =\n            treeExpanded?.root.descendantsParentNodes.map((node) => node.id) || [];\n        if (!node.isLeaf || !expandedParentNodeIds.includes(parentNode?.id)) {\n            // Handle cases where the expanded tree will be altered.\n            // If node is not a leaf, the new expanded tree will contain its descendants.\n            // If parentNode is not a parent in the current expanded tree, it will become one\n            // in the new expanded tree.\n            // Compute the depth of the parent of parentNode. That node is guaranteed to be a\n            // parent in the current expanded tree.\n            const depth = expandedParentNodeIds.findIndex(\n                (id) => id === parentNode?.parentNode?.id\n            );\n            if (depth === -1) {\n                // Drop as root or drop as the child of a root that is not part of the current\n                // expanded tree. The current expanded tree should be fully closed.\n                nodeToCollapse = treeExpanded?.root;\n            } else {\n                // Drop anywhere else (at a position that can be related to the expanded tree with\n                // the depth of the parent of parentNode). In that case the existing hierarchy is\n                // split at the depth of the parent, and will be completed by node's remaining\n                // expanded tree.\n                const nodeIdToCollapse = expandedParentNodeIds.at(depth + 1);\n                if (nodeIdToCollapse) {\n                    nodeToCollapse = treeExpanded?.nodePerNodeId[nodeIdToCollapse];\n                }\n            }\n        } else {\n            // Handle cases where node is a leaf dropped in the current expanded tree. In that case,\n            // the tree is kept open.\n            // Descendants of parentNode will always be reloaded to account for changes caused by\n            // the drop operation.\n            nodeToCollapse = parentNode;\n        }\n        // Update the view.\n        if (oldParentNode) {\n            oldParentNode.removeChildNode(node);\n        } else {\n            node.tree.removeNodes([node]);\n        }\n        nodeToCollapse?.collapseChildNodes();\n        if (!parentNode) {\n            // Drop as root, reset the hierarchy.\n            nodeId = forestId = treeId = 0;\n            this.root = this._createRoot(this.config, formattedData);\n        } else {\n            // Update parentNode data.\n            parentNode.data[this.childFieldName || this.defaultChildFieldName] = formattedData;\n            parentNode.populateChildNodes();\n        }\n        const newNodeId = Object.keys(this.root.nodePerNodeId).find((key) => {\n            return this.root.nodePerNodeId[key].resId === resId;\n        });\n        this.notify({ scrollTarget: newNodeId });\n    }\n\n    validateUpdateParentNode(node, parentNode) {\n        if (parentNode.resId === node.resId) {\n            this.notification.add(_t(\"The parent record cannot be the record dragged.\"), {\n                type: \"danger\",\n            });\n            return false;\n        } else if (node.allSubsidiaryResIds.includes(parentNode.resId)) {\n            this.notification.add(_t(\"Cannot change the parent because it will cause a cyclic.\"), {\n                type: \"danger\",\n            });\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Returns a domain to get a recordSet containing:\n     * - node.\n     * - all children under the new parent.\n     * - all descendants in the final expanded tree (after the operation), which\n     *   are at a depth impacted by the update @see updateParentNode (part\n     *   about the expanded tree).\n     *\n     * @param {HierarchyNode} node that is moving\n     * @param {Number | false} parentResId resId of the parent\n     * @param {HierarchyNode} [parentNode] which receives node as its child\n     *                        (undefined if node is dropped as a root).\n     * @returns {Array} domain\n     */\n    computeUpdateParentNodeDomain(node, parentResId, parentNode) {\n        const domainsOr = [[[\"id\", \"=\", node.resId]]];\n        // Include the new parent children (for ordering).\n        domainsOr.push([[this.parentFieldName, \"=\", parentResId]]);\n        if (!node.isLeaf) {\n            // Include node descendants (keep that part of the expanded tree).\n            const expandedTreeParentResIds = node.descendantsParentNodes.map((node) => node.resId);\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        } else if (!parentNode) {\n            // Keep the current expanded tree (if any) from its root if node is a leaf dropped as a\n            // root.\n            const expandedTreeParentResIds = node.tree.root.descendantsParentNodes.map(\n                (node) => node.resId\n            );\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        } else if (!parentNode.isLeaf) {\n            // Keep the current expanded tree (if any) from the target parent if node is a leaf.\n            const expandedTreeParentResIds = parentNode.descendantsParentNodes.map(\n                (node) => node.resId\n            );\n            domainsOr.push([[this.parentFieldName, \"in\", expandedTreeParentResIds]]);\n        }\n        let domain = Domain.or(domainsOr);\n        const globalDomain = this.globalDomain;\n        if (globalDomain.length) {\n            domain = Domain.and([domain, globalDomain]);\n        }\n        return domain.toList({});\n    }\n}\n", "/** @odoo-module */\n\nimport { onWillUnmount, reactive, useEffect, useExternalListener } from \"@odoo/owl\";\nimport { useThrottleForAnimation } from \"@web/core/utils/timing\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { makeDraggableHook } from \"@web/core/utils/draggable_hook_builder\";\n\nconst hookParams = {\n    name: \"useHierarchyNodeDraggable\",\n    acceptedParams: {\n        rows: [String],\n    },\n    defaultParams: {\n        edgeScrolling: { speed: 20, threshold: 60 },\n        rows: null,\n    },\n    onComputeParams({ ctx, params }) {\n        // Row selector\n        ctx.rowSelector = params.rows || null;\n        if (ctx.rowSelector) {\n            ctx.fullSelector = `${ctx.rowSelector} ${ctx.fullSelector}`;\n        }\n    },\n    onDragStart(params) {\n        const { ctx, addListener, callHandler } = params;\n\n        const onElementPointerEnter = (ev) => {\n            const element = ev.currentTarget;\n            current.hierarchyElement = element;\n            callHandler(\"onElementEnter\", { element });\n        };\n\n        const onElementPointerLeave = (ev) => {\n            const element = ev.currentTarget;\n            current.hierarchyElement = null;\n            callHandler(\"onElementLeave\", { element });\n        };\n\n        const onRowPointerEnter = (ev) => {\n            const row = ev.currentTarget;\n            current.hierarchyRow = row;\n            callHandler(\"onRowEnter\", { row });\n        };\n\n        const onRowPointerLeave = (ev) => {\n            const row = ev.currentTarget;\n            current.hierarchyRow = null;\n            callHandler(\"onRowLeave\", { row });\n        };\n\n        const { ref, current, elementSelector, rowSelector } = ctx;\n\n        for (const rowEl of ref.el.querySelectorAll(rowSelector)) {\n            addListener(rowEl, \"pointerenter\", onRowPointerEnter);\n            addListener(rowEl, \"pointerleave\", onRowPointerLeave);\n        }\n\n        for (const siblingEl of ref.el.querySelectorAll(elementSelector)) {\n            if (siblingEl !== current.element) {\n                addListener(siblingEl, \"pointerenter\", onElementPointerEnter);\n                addListener(siblingEl, \"pointerleave\", onElementPointerLeave);\n            }\n        }\n\n        return pick(current, \"element\", \"row\");\n    },\n    onDragEnd({ ctx }) {\n        return pick(ctx.current, \"element\", \"row\", \"hierarchyRow\");\n    },\n    onDrop({ ctx }) {\n        const { current } = ctx;\n        const rowElement = current.hierarchyRow;\n        const element = current.hierarchyElement;\n        if ((rowElement && rowElement !== current.row) || element) {\n            return {\n                element: current.element,\n                row: current.row,\n                nextRow: rowElement && current.row !== rowElement ? rowElement : null,\n                newParentNode: element,\n            };\n        }\n    },\n    onWillStartDrag({ ctx }) {\n        const { current, rowSelector } = ctx;\n\n        if (rowSelector) {\n            current.row = current.element.closest(rowSelector);\n        }\n\n        return pick(current, \"element\", \"row\");\n    },\n};\n\nexport function useHierarchyNodeDraggable(params) {\n    const setupHooks = {\n        addListener: useExternalListener,\n        setup: useEffect,\n        teardown: onWillUnmount,\n        throttle: useThrottleForAnimation,\n        wrapState: reactive,\n    }\n    return makeDraggableHook({ ...hookParams, setupHooks })(params);\n}\n", "/** @odoo-module */\n\nimport { Component, useRef, onPatched } from \"@odoo/owl\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useBus, useService } from \"@web/core/utils/hooks\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\n\nimport { HierarchyCard } from \"./hierarchy_card\";\nimport { useHierarchyNodeDraggable } from \"./hierarchy_node_draggable\";\n\nexport class HierarchyRenderer extends Component {\n    static components = {\n        HierarchyCard,\n    };\n    static props = {\n        model: Object,\n        openRecord: Function,\n        archInfo: Object,\n        templates: Object,\n    };\n    static template = \"web_hierarchy.HierarchyRenderer\";\n\n    setup() {\n        this.rendererRef = useRef(\"renderer\");\n        this.notification = useService(\"notification\");\n        if (this.canDragAndDropRecord) {\n            useHierarchyNodeDraggable({\n                ref: this.rendererRef,\n                enable: this.draggable,\n                elements: \".o_hierarchy_node_container\",\n                handle: \".o_hierarchy_node\",\n                rows: \".o_hierarchy_row\",\n                ignore: \"button\",\n                onDragStart: ({ addClass, element }) => {\n                    addClass(element, \"o_hierarchy_dragged\");\n                    addClass(element.querySelector(\".o_hierarchy_node\"), \"shadow\");\n                },\n                onDragEnd: ({ removeClass, element, row, hierarchyRow }) => {\n                    removeClass(element, \"o_hierarchy_dragged\");\n                    if (row) {\n                        removeClass(row, \"o_hierarchy_hover\");\n                    }\n                    if (hierarchyRow) {\n                        removeClass(hierarchyRow, \"o_hierarchy_hover\");\n                    }\n                },\n                onDrop: (params) => {\n                    this.nodeDrop(params);\n                },\n                onElementEnter: ({ addClass, element }) => {\n                    addClass(element, \"o_hierarchy_hover\");\n                },\n                onElementLeave: ({ removeClass, element }) => {\n                    removeClass(element, \"o_hierarchy_hover\");\n                },\n                onRowEnter: ({ addClass, row }) => {\n                    addClass(row, \"o_hierarchy_hover\");\n                },\n                onRowLeave: ({ removeClass, row }) => {\n                    removeClass(row, \"o_hierarchy_hover\");\n                },\n            });\n        }\n        this.scrollTarget = \"none\";\n        useBus(this.props.model.bus, \"hierarchyScrollTarget\", (ev) => {\n            this.scrollTarget = ev.detail?.scrollTarget || \"none\";\n        });\n        onPatched(this.onPatched);\n    }\n\n    onPatched() {\n        if (this.scrollTarget === \"none\") {\n            return;\n        }\n        const row =\n            this.scrollTarget === \"bottom\"\n                ? this.rendererRef.el.querySelector(\":scope .o_hierarchy_row:last-child\")\n                : this.rendererRef.el\n                      .querySelector(\n                          `:scope .o_hierarchy_node[data-node-id=\"${this.scrollTarget}\"]`\n                      )\n                      ?.closest(\".o_hierarchy_row\");\n        this.scrollTarget = \"none\";\n        if (!row) {\n            return;\n        }\n        scrollTo(row, { behavior: \"smooth\" });\n    }\n\n    get canDragAndDropRecord() {\n        return this.draggable && !this.env.isSmall;\n    }\n\n    get draggable() {\n        return this.props.archInfo.draggable;\n    }\n\n    get rows() {\n        const rootNodes = this.props.model.root.rootNodes.filter((n) => !n.hidden);\n        const rows = [{ nodes: rootNodes }];\n        const processNode = (node) => {\n            if (!node.isLeaf) {\n                const subNodes = node.nodes.filter((n) => !n.hidden);\n                rows.push({ parentNode: node, nodes: subNodes });\n                for (const subNode of subNodes) {\n                    processNode(subNode);\n                }\n            }\n        };\n\n        for (const node of this.props.model.root.rootNodes) {\n            processNode(node);\n        }\n\n        return rows;\n    }\n\n    async nodeDrop({ element, row, nextRow, newParentNode }) {\n        let parentNodeId, parentResId;\n        if (newParentNode) {\n            parentNodeId = newParentNode.dataset.nodeId;\n        } else if (nextRow?.dataset.rowId !== row.dataset.rowId) {\n            parentNodeId = nextRow.dataset.parentNodeId;\n            if (!parentNodeId) {\n                const nodes = this.rows[nextRow.dataset.rowId].nodes || [];\n                if (nodes) {\n                    parentNodeId = nodes[0].parentNode?.id;\n                    if (!parentNodeId) {\n                        parentResId = nodes[0].parentResId;\n                        if (!nodes.every((node) => node.parentResId === parentResId)) {\n                            this.notification.add(\n                                _t(\"Impossible to update the parent node of the dragged node because no parent has been found.\"),\n                                {\n                                    type: \"danger\",\n                                }\n                            );\n                            return;\n                        }\n                    }\n                }\n            }\n        }\n        await this.props.model.updateParentNode(element.dataset.nodeId, { parentResId, parentNodeId });\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { HierarchyArchParser } from \"./hierarchy_arch_parser\";\nimport { HierarchyController } from \"./hierarchy_controller\";\nimport { HierarchyModel } from \"./hierarchy_model\";\nimport { HierarchyRenderer } from \"./hierarchy_renderer\";\n\nexport const hierarchyView = {\n    type: \"hierarchy\",\n    ArchParser: HierarchyArchParser,\n    Controller: HierarchyController,\n    Model: HierarchyModel,\n    Renderer: HierarchyRenderer,\n    buttonTemplate: \"web_hierarchy.HierarchyButtons\",\n    searchMenuTypes: [\"filter\"],\n\n    props: (genericProps, view) => {\n        const { ArchParser, Model, Renderer, buttonTemplate: viewButtonTemplate } = view;\n        const { arch, relatedModels, resModel, buttonTemplate } = genericProps;\n        return {\n            ...genericProps,\n            archInfo: new ArchParser().parse(arch, relatedModels, resModel),\n            buttonTemplate: buttonTemplate || viewButtonTemplate,\n            Model,\n            Renderer,\n        };\n    }\n}\n\nregistry.category(\"views\").add(\"hierarchy\", hierarchyView);\n", "/** @odoo-module */\n\nimport { HierarchyCard } from \"@web_hierarchy/hierarchy_card\";\n\nexport class KnowledgeHierarchyCard extends HierarchyCard {\n    /**\n     * @override\n     * Add a context variable to be able to show/hide the section if a node\n     * is a root (in the hierarchy view).\n     */\n    getRenderingContext(data) {\n        const context = super.getRenderingContext(data);\n        return {\n            ...context,\n            isRoot: !this.props.node.parentNode,\n        };\n    }\n}\n", "/** @odoo-module */\n\nimport { HierarchyModel } from \"@web_hierarchy/hierarchy_model\";\n\nexport class KnowledgeHierarchyModel extends HierarchyModel {\n    /**\n     * @override\n     * Use the `move_to` method of the model instead of a simple `write` for\n     * some extra processing and validations.\n     */\n    async updateParentId(node, parentResId = false) {\n        return this.orm.call(\"knowledge.article\", \"move_to\", [node.resId], {\n            parent_id: parentResId,\n            category: parentResId ? false : node.data.category,\n        });\n    }\n}\n", "/** @odoo-module */\n\nimport { HierarchyRenderer } from \"@web_hierarchy/hierarchy_renderer\";\nimport { KnowledgeHierarchyCard } from \"@knowledge/views/hierarchy/knowledge_hierarchy_card\";\n\nexport class KnowledgeHierarchyRenderer extends HierarchyRenderer {\n    static components = {\n        ...HierarchyRenderer.components,\n        HierarchyCard: KnowledgeHierarchyCard,\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { hierarchyView } from \"@web_hierarchy/hierarchy_view\";\nimport { KnowledgeHierarchyModel } from \"@knowledge/views/hierarchy/knowledge_hierarchy_model\";\nimport { KnowledgeHierarchyRenderer } from \"@knowledge/views/hierarchy/knowledge_hierarchy_renderer\";\n\nexport const KnowledgeHierarchyView = {\n    ...hierarchyView,\n    Model: KnowledgeHierarchyModel,\n    Renderer: KnowledgeHierarchyRenderer,\n    searchMenuTypes: [\"filter\", \"favorite\"],\n};\n\nregistry.category(\"views\").add(\"knowledge_hierarchy\", KnowledgeHierarchyView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { GraphModel } from \"@web/views/graph/graph_model\";\n\nexport class HelpdeskTicketGraphModel extends GraphModel {\n    /**\n     * @override\n     */\n    _getDefaultFilterLabel(field) {\n        if (field.fieldName === \"sla_deadline\") {\n            return _t(\"Deadline reached\");\n        }\n        if (field.fieldName == \"user_id\") {\n            return _t(\"Unassigned\");\n        }\n        return super._getDefaultFilterLabel(field);\n    }\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { graphView } from \"@web/views/graph/graph_view\";\nimport { HelpdeskTicketGraphModel } from \"./helpdesk_ticket_graph_model\";\n\nconst helpdeskTicketGraphView = {\n    ...graphView,\n    Model: HelpdeskTicketGraphModel,\n};\n\nregistry.category(\"views\").add(\"helpdesk_ticket_graph\", helpdeskTicketGraphView);\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { PivotModel } from \"@web/views/pivot/pivot_model\";\n\nexport class HelpdeskTicketPivotModel extends PivotModel {\n    /**\n     * @override\n     */\n    _getEmptyGroupLabel(fieldName) {\n        if (fieldName === \"sla_deadline\") {\n            return _t(\"Deadline reached\");\n        } else {\n            return super._getEmptyGroupLabel(fieldName);\n        }\n    }\n}\n", "/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\nimport { pivotView } from \"@web/views/pivot/pivot_view\";\nimport { HelpdeskTicketPivotModel } from \"./helpdesk_ticket_pivot_model\";\n\nconst helpdeskTicketPivotView = {\n    ...pivotView,\n    Model: HelpdeskTicketPivotModel,\n};\n\nregistry.category(\"views\").add(\"helpdesk_ticket_pivot\", helpdeskTicketPivotView);\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { graphView } from \"@web/views/graph/graph_view\";\n\nexport class SkillsGraphRenderer extends GraphRenderer {\n    getScaleOptions() {\n        const scaleOptions = super.getScaleOptions();\n\n        if ('y' in scaleOptions) {\n            scaleOptions.y.suggestedMax = 100;\n        }\n\n        return scaleOptions;\n    }\n}\n\nexport const skillsGraphView = {\n    ...graphView,\n    Renderer: SkillsGraphRenderer,\n};\n\nregistry.category(\"views\").add(\"skills_graph\", skillsGraphView);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"@web/views/fields/formatters\";\nimport { GridCell } from \"./grid_cell\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatFactor(value, options);\n}\n\nexport class FloatFactorGridCell extends GridCell {\n    static props = {\n        ...GridCell.props,\n        factor: { type: Number, optional: true },\n    };\n\n    parse(value) {\n        const factorValue = value / this.factor;\n        return super.parse(factorValue.toString());\n    }\n\n    get factor() {\n        return this.props.factor || this.props.fieldInfo.options?.factor || 1;\n    }\n\n    get value() {\n        return super.value * this.factor;\n    }\n\n    get formattedValue() {\n        return formatter(this.value);\n    }\n}\n\nexport const floatFactorGridCell = {\n    component: FloatFactorGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_factor\", floatFactorGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { parseFloatTime } from \"@web/views/fields/parsers\";\nimport { formatFloatTime } from \"@web/views/fields/formatters\";\nimport { GridCell } from \"./grid_cell\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatTime(value, { ...options, noLeadingZeroHour: true });\n}\n\nexport class FloatTimeGridCell extends GridCell {\n    get formattedValue() {\n        return formatter(this.value);\n    }\n\n    parse(value) {\n        return parseFloatTime(value);\n    }\n}\n\nexport const floatTimeGridCell = {\n    component: FloatTimeGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_time\", floatTimeGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { formatFloatFactor } from \"@web/views/fields/formatters\";\nimport { useGridCell, useMagnifierGlass } from \"@web_grid/hooks/grid_cell_hook\";\nimport { standardGridCellProps } from \"./grid_cell\";\n\nimport { Component, useRef, useState, useEffect } from \"@odoo/owl\";\n\nfunction formatter(value, options = {}) {\n    return formatFloatFactor(value, options);\n}\n\nexport class FloatToggleGridCell extends Component {\n    static props = {\n        ...standardGridCellProps,\n        factor: { type: Number, optional: true },\n    };\n    static template = \"web_grid.FloatToggleGridCell\";\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        this.buttonRef = useRef(\"toggleButton\");\n        this.magnifierGlassHook = useMagnifierGlass();\n        this.state = useState({\n            edit: this.props.editMode,\n            invalid: false,\n            cell: null,\n        });\n        useGridCell();\n\n        useEffect(\n            (buttonEl) => {\n                if (buttonEl) {\n                    buttonEl.focus();\n                }\n            },\n            () => [this.buttonRef.el]\n        );\n    }\n\n    get factor() {\n        return this.props.factor || this.props.fieldInfo.options?.factor || 1;\n    }\n\n    get range() {\n        return this.props.fieldInfo.options?.range || [0.0, 0.5, 1.0];\n    }\n\n    get value() {\n        return (this.state.cell.value || 0) * this.factor;\n    }\n\n    get formattedValue() {\n        return formatter(this.state.cell.value || 0, {\n            digits: this.props.fieldInfo.attrs?.digits || 2,\n            factor: this.factor,\n        });\n    }\n\n    isEditable(props = this.props) {\n        return (\n            !props.readonly && this.state.cell?.readonly === false && !this.state.cell.row.isSection\n        );\n    }\n\n    onChange() {\n        let currentIndex = this.range.indexOf(this.value);\n        currentIndex++;\n        if (currentIndex > this.range.length - 1) {\n            currentIndex = 0;\n        }\n        this.update(this.range[currentIndex] / this.factor);\n    }\n\n    update(value) {\n        this.state.cell.update(value);\n    }\n\n    onCellClick(ev) {\n        if (this.isEditable() && !this.state.edit && !ev.target.closest(\".o_grid_search_btn\")) {\n            this.onChange();\n            this.props.onEdit(true);\n        }\n    }\n\n    onKeyDown(ev) {\n        this.props.onKeyDown(ev, this.state.cell);\n    }\n}\n\nexport const floatToggleGridCell = {\n    component: FloatToggleGridCell,\n    formatter,\n};\n\nregistry.category(\"grid_components\").add(\"float_toggle\", floatToggleGridCell);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nimport { useNumpadDecimal } from \"@web/views/fields/numpad_decimal_hook\";\nimport { formatInteger } from \"@web/views/fields/formatters\";\nimport { formatFloat } from \"@web/core/utils/numbers\";\nimport { parseInteger, parseFloat } from \"@web/views/fields/parsers\";\nimport { useInputHook } from \"@web_grid/hooks/input_hook\";\n\nimport { Component, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { useGridCell, useMagnifierGlass } from \"@web_grid/hooks/grid_cell_hook\";\n\nexport const standardGridCellProps = {\n    name: String,\n    classNames: String,\n    fieldInfo: Object,\n    readonly: { type: Boolean, optional: true },\n    editMode: { type: Boolean, optional: true },\n    reactive: {\n        type: Object,\n        shape: {\n            cell: [HTMLElement, { value: null }],\n        },\n    },\n    openRecords: Function,\n    onEdit: Function,\n    getCell: Function,\n    onKeyDown: { type: Function, optional: true },\n};\n\nexport class GridCell extends Component {\n    static template = \"web_grid.Cell\";\n    static props = standardGridCellProps;\n    static defaultProps = {\n        readonly: true,\n        editMode: false,\n    };\n\n    setup() {\n        this.rootRef = useRef(\"root\");\n        this.state = useState({\n            edit: this.props.editMode,\n            invalid: false,\n            cell: null,\n        });\n        this.discardChanges = false;\n        this.magnifierGlassHook = useMagnifierGlass();\n        this.inputRef = useInputHook({\n            getValue: () => this.formattedValue,\n            refName: \"numpadDecimal\",\n            parse: this.parse.bind(this),\n            notifyChange: this.onChange.bind(this),\n            commitChanges: this.saveEdition.bind(this),\n            onKeyDown: (ev) => this.props.onKeyDown(ev, this.state.cell),\n            discard: this.discard.bind(this),\n            setInvalid: () => {\n                this.state.invalid = true;\n            },\n            setDirty: () => {\n                this.state.invalid = false;\n            },\n            isInvalid: () => this.state.invalid,\n        });\n        useNumpadDecimal();\n\n        useGridCell();\n        useEffect(\n            (edit, inputEl, cellEl) => {\n                if (inputEl) {\n                    inputEl.value = this.formattedValue;\n                }\n                if (edit && inputEl) {\n                    inputEl.focus();\n                    if (inputEl.type === \"text\") {\n                        if (inputEl.selectionStart === null) {\n                            return;\n                        }\n                        if (inputEl.selectionStart === inputEl.selectionEnd) {\n                            inputEl.selectionStart = 0;\n                            inputEl.selectionEnd = inputEl.value.length;\n                        }\n                    }\n                }\n                this.discardChanges = false;\n            },\n            () => [this.state.edit, this.inputRef.el, this.props.reactive.cell]\n        );\n    }\n\n    get value() {\n        return this.state.cell?.value || 0;\n    }\n\n    get section() {\n        return this.row.getSection();\n    }\n\n    get row() {\n        return this.state.cell?.row;\n    }\n\n    get formattedValue() {\n        const { type, digits } = this.props.fieldInfo;\n        if (type === \"integer\") {\n            return formatInteger(this.value);\n        }\n        return formatFloat(this.value, { digits: digits || 2 });\n    }\n\n    isEditable(props = this.props) {\n        return (\n            !props.readonly && this.state.cell?.readonly === false && !this.state.cell.row.isSection\n        );\n    }\n\n    parse(value) {\n        if (this.props.fieldInfo.type === \"integer\") {\n            return parseInteger(value);\n        }\n        return parseFloat(value);\n    }\n\n    onChange(value) {\n        if (!this.discardChanges) {\n            this.update(value);\n        }\n    }\n\n    update(value) {\n        this.state.cell.update(value);\n    }\n\n    saveEdition(value) {\n        const changesCommitted = (value || false) !== (this.state.cell.value || false);\n        if ((value || false) !== (this.state.cell?.value || false)) {\n            this.update(value);\n        }\n        this.props.onEdit(false);\n        return changesCommitted;\n    }\n\n    discard() {\n        this.discardChanges = true;\n        this.props.onEdit(false);\n    }\n\n    onCellClick(ev) {\n        if (this.isEditable() && !this.state.edit) {\n            this.discardChanges = false;\n            this.props.onEdit(true);\n        }\n    }\n}\n\nexport const integerGridCell = {\n    component: GridCell,\n    formatter: formatInteger,\n};\n\nregistry.category(\"grid_components\").add(\"integer\", integerGridCell);\n\nexport const floatGridCell = {\n    component: GridCell,\n    formatter: formatFloat,\n};\n\nregistry.category(\"grid_components\").add(\"float\", floatGridCell);\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { registry } from \"@web/core/registry\";\n\nimport { GridCell } from \"../grid_cell\";\nimport { GridRow } from \"../grid_row/grid_row\";\n\nconst gridComponentRegistry = registry.category(\"grid_components\");\n\nexport class GridComponent extends Component {\n    static props = [\"name\", \"type\", \"isMeasure?\", \"component?\", \"*\"];\n    static template = \"web_grid.GridComponent\"\n\n    get gridComponent() {\n        if (this.props.component) {\n            return this.props.component;\n        }\n        if (gridComponentRegistry.contains(this.props.type)) {\n            return gridComponentRegistry.get(this.props.type).component;\n        }\n        if (this.props.isMeasure) {\n            console.warn(`Missing widget: ${this.props.type} for grid component`);\n            return GridCell;\n        }\n        return GridRow;\n    }\n\n    get gridComponentProps() {\n        const gridComponentProps = Object.fromEntries(\n            Object.entries(this.props).filter(\n                ([key,]) => key in this.gridComponent.props\n            )\n        );\n        gridComponentProps.classNames = `o_grid_component o_grid_component_${this.props.type} ${gridComponentProps.classNames || \"\"}`;\n        return gridComponentProps;\n    }\n}\n", "/** @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\n\nimport { registry } from \"@web/core/registry\";\n\nexport class GridRow extends Component {\n    static template = \"web_grid.GridRow\";\n    static props = {\n        name: String,\n        model: Object,\n        row: Object,\n        classNames: { type: String, optional: true },\n        context: { type: Object, optional: true },\n        style: { type: String, optional: true },\n        value: { optional: true },\n    };\n    static defaultProps = {\n        classNames: \"\",\n        context: {},\n        style: \"\",\n    };\n\n    get value() {\n        let value = 'value' in this.props ? this.props.value : this.props.row.initialRecordValues[this.props.name];\n        const fieldInfo = this.props.model.fieldsInfo[this.props.name];\n        if (fieldInfo.type === \"selection\") {\n            value = fieldInfo.selection.find(([key,]) => key === value)?.[1];\n        }\n        return value;\n    }\n}\n\nexport const gridRow = {\n    component: GridRow,\n};\n\nregistry\n    .category(\"grid_components\")\n    .add(\"selection\", gridRow)\n    .add(\"char\", gridRow);\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { GridRow, gridRow } from \"../grid_row/grid_row\";\n\nexport class Many2OneGridRow extends GridRow {\n    static template = \"web_grid.Many2OneGridRow\";\n    static props = {\n        ...GridRow.props,\n        relation: { type: String, optional: true },\n        canOpen: { type: Boolean, optional: true },\n    }\n    static defaultProps = {\n        ...GridRow.defaultProps,\n        canOpen: true,\n    };\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.actionService = useService(\"action\");\n    }\n\n    get relation() {\n        return this.props.relation || this.props.model.fieldsInfo[this.props.name].relation;\n    }\n\n    get displayName() {\n        return this.value && this.value[1].split(\"\\n\", 1)[0];\n    }\n\n    get extraLines() {\n        return this.value\n            ? this.value[1]\n                  .split(\"\\n\")\n                  .map((line) => line.trim())\n                  .slice(1)\n            : [];\n    }\n\n    get resId() {\n        return this.value && this.value[0];\n    }\n\n    async openAction() {\n        const action = await this.orm.call(this.relation, \"get_formview_action\", [[this.resId]], {\n            context: this.props.context,\n        });\n        await this.actionService.doAction(action);\n    }\n\n    onClick(ev) {\n        if (this.props.canOpen) {\n            ev.stopPropagation();\n            this.openAction();\n        }\n    }\n}\n\nexport const many2OneGridRow = {\n    ...gridRow,\n    component: Many2OneGridRow,\n};\n\nregistry.category(\"grid_components\").add(\"many2one\", many2OneGridRow);\n", "/** @odoo-module */\n\nimport { useComponent, useEffect } from \"@odoo/owl\";\n\nexport function useMagnifierGlass() {\n    const component = useComponent();\n    return {\n        onMagnifierGlassClick() {\n            const { context, domain, title } = component.state.cell;\n            component.props.openRecords(title, domain.toList(), context);\n        },\n    };\n}\n\nexport function useGridCell() {\n    const component = useComponent();\n    useEffect(\n        /** @param {HTMLElement | null} cellEl */\n        (cellEl) => {\n            if (!cellEl) {\n                component.state.cell = null;\n                return;\n            }\n            component.state.cell = component.props.getCell(\n                cellEl.dataset.row,\n                cellEl.dataset.column\n            );\n            Object.assign(component.rootRef.el.style, {\n                \"grid-row\": cellEl.style[\"grid-row\"],\n                \"grid-column\": cellEl.style[\"grid-column\"],\n                \"z-index\": 1,\n            });\n            component.rootRef.el.dataset.gridRow = cellEl.dataset.gridRow;\n            component.rootRef.el.dataset.gridColumn = cellEl.dataset.gridColumn;\n            cellEl.querySelector(\".o_grid_cell_readonly\").classList.add(\"d-none\");\n            component.rootRef.el.classList.toggle(\n                \"o_field_cursor_disabled\",\n                !component.state.cell.row.isSection && !component.isEditable()\n            );\n            component.rootRef.el.classList.toggle(\"fw-bold\", Boolean(component.state.cell.row.isSection));\n        },\n        () => [component.props.reactive.cell]\n    );\n}\n", "/** @odoo-module */\n\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\n\nimport { useEffect, useRef } from \"@odoo/owl\";\n\nexport function useInputHook(params) {\n    const inputRef = params.ref || useRef(params.refName || \"input\");\n\n    /*\n     * A field is dirty if it is no longer sync with the model\n     * More specifically, a field is no longer dirty after it has *tried* to update the value in the model.\n     * An invalid value will therefore not be dirty even if the model will not actually store the invalid value.\n     */\n    let isDirty = false;\n\n    /**\n     * The last value that has been committed to the model.\n     * Not changed in case of invalid field value.\n     */\n    let lastSetValue = null;\n\n    /**\n     * When a user types, we need to set the field as dirty.\n     */\n    function onInput(ev) {\n        isDirty = ev.target.value !== lastSetValue;\n        if (params.setDirty) {\n            params.setDirty(isDirty);\n        }\n    }\n\n    /**\n     * On blur, we consider the field no longer dirty, even if it were to be invalid.\n     * However, if the field is invalid, the new value will not be committed to the model.\n     */\n    function onChange(ev) {\n        if (isDirty) {\n            isDirty = false;\n            let isInvalid = false;\n            let val = ev.target.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    if (params.setInvalid) {\n                        params.setInvalid();\n                    }\n                    isInvalid = true;\n                }\n            }\n\n            if (!isInvalid) {\n                params.notifyChange(val);\n                lastSetValue = ev.target.value;\n            }\n\n            if (params.setDirty) {\n                params.setDirty(isDirty);\n            }\n        }\n    }\n    function onKeydown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (params.discard && hotkey === \"escape\") {\n            params.discard();\n        } else if (params.commitChanges && [\"enter\", \"tab\", \"shift+tab\"].includes(hotkey)) {\n            commitChanges();\n        }\n        if (params.onKeyDown) {\n            params.onKeyDown(ev);\n        }\n    }\n\n    useEffect(\n        (inputEl) => {\n            if (inputEl) {\n                inputEl.addEventListener(\"input\", onInput);\n                inputEl.addEventListener(\"change\", onChange);\n                inputEl.addEventListener(\"keydown\", onKeydown);\n                return () => {\n                    inputEl.removeEventListener(\"input\", onInput);\n                    inputEl.removeEventListener(\"change\", onChange);\n                    inputEl.removeEventListener(\"keydown\", onKeydown);\n                };\n            }\n        },\n        () => [inputRef.el]\n    );\n\n    /**\n     * Sometimes, a patch can happen with possible a new value for the field\n     * If the user was typing a new value (isDirty) or the field is still invalid,\n     * we need to do nothing.\n     * If it is not such a case, we update the field with the new value.\n     */\n    useEffect(() => {\n        const isInvalid = params.isInvalid ? params.isInvalid() : false;\n        if (inputRef.el && !isDirty && !isInvalid) {\n            inputRef.el.value = params.getValue();\n            lastSetValue = inputRef.el.value;\n        }\n    });\n\n    function isUrgentSaved(urgent) {\n        if (params.isUrgentSaved) {\n            return params.isUrgentSaved(urgent);\n        }\n        return urgent;\n    }\n\n    /**\n     * Roughly the same as onChange, but called at more specific / critical times. (See bus events)\n     */\n    async function commitChanges(urgent) {\n        if (!inputRef.el) {\n            return;\n        }\n\n        isDirty = inputRef.el.value !== lastSetValue;\n        if (isDirty || isUrgentSaved(urgent)) {\n            let isInvalid = false;\n            isDirty = false;\n            let val = inputRef.el.value;\n            if (params.parse) {\n                try {\n                    val = params.parse(val);\n                } catch {\n                    isInvalid = true;\n                    if (urgent) {\n                        return;\n                    } else {\n                        params.setInvalid();\n                    }\n                }\n            }\n\n            if (isInvalid) {\n                return;\n            }\n\n            const result = params.commitChanges(val); // means change has been committed\n            if (result) {\n                lastSetValue = inputRef.el.value;\n                if (params.setDirty) {\n                    params.setDirty(isDirty);\n                }\n            }\n        }\n    }\n\n    return inputRef;\n}\n", "/** @odoo-module */\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { exprToBoolean } from \"@web/core/utils/strings\";\nimport { visitXML } from \"@web/core/utils/xml\";\nimport { getActiveActions, processButton } from \"@web/views/utils\";\n\nexport class GridArchParser {\n    parse(xmlDoc, models, modelName) {\n        const archInfo = {\n            activeActions: getActiveActions(xmlDoc),\n            hideLineTotal: false,\n            hideColumnTotal: false,\n            hasBarChartTotal: false,\n            createInline: false,\n            displayEmpty: false,\n            buttons: [],\n            activeRangeName: \"\",\n            ranges: {},\n            sectionField: null,\n            rowFields: [],\n            columnFieldName: \"\",\n            measureField: {\n                name: \"__count\",\n                aggregator: \"sum\",\n                readonly: true,\n                string: _t(\"Count\"),\n            },\n            readonlyField: null,\n            widgetPerFieldName: {},\n            editable: false,\n            formViewId: false,\n        };\n        let buttonId = 0;\n\n        visitXML(xmlDoc, (node) => {\n            if (node.tagName === \"grid\") {\n                if (node.hasAttribute(\"hide_line_total\")) {\n                    archInfo.hideLineTotal = exprToBoolean(node.getAttribute(\"hide_line_total\"));\n                }\n                if (node.hasAttribute(\"hide_column_total\")) {\n                    archInfo.hideColumnTotal = exprToBoolean(\n                        node.getAttribute(\"hide_column_total\")\n                    );\n                }\n                if (node.hasAttribute(\"barchart_total\")) {\n                    archInfo.hasBarChartTotal = exprToBoolean(\n                        node.getAttribute(\"barchart_total\")\n                    );\n                }\n                if (node.hasAttribute(\"create_inline\")) {\n                    archInfo.createInline = exprToBoolean(node.getAttribute(\"create_inline\"));\n                }\n                if (node.hasAttribute(\"display_empty\")) {\n                    archInfo.displayEmpty = exprToBoolean(node.getAttribute(\"display_empty\"));\n                }\n                if (node.hasAttribute(\"action\") && node.hasAttribute(\"type\")) {\n                    archInfo.openAction = {\n                        name: node.getAttribute(\"action\"),\n                        type: node.getAttribute(\"type\"),\n                    };\n                }\n                if (node.hasAttribute(\"editable\")) {\n                    archInfo.editable = exprToBoolean(node.getAttribute(\"editable\"));\n                }\n                if (node.hasAttribute(\"form_view_id\")) {\n                    archInfo.formViewId = parseInt(node.getAttribute(\"form_view_id\"), 10);\n                }\n            } else if (node.tagName === \"field\") {\n                const fieldName = node.getAttribute(\"name\"); // exists (rng validation)\n                const fieldInfo = models[modelName].fields[fieldName];\n                const type = node.getAttribute(\"type\") || \"row\";\n                const string = node.getAttribute(\"string\") || fieldInfo.string;\n                let invisible = node.getAttribute(\"invisible\") || 'False';\n                switch (type) {\n                    case \"row\":\n                        if (node.hasAttribute(\"widget\")) {\n                            archInfo.widgetPerFieldName[fieldName] = node.getAttribute(\"widget\");\n                        }\n                        if (\n                            node.hasAttribute(\"section\") &&\n                            exprToBoolean(node.getAttribute(\"section\")) &&\n                            !archInfo.sectionField\n                        ) {\n                            archInfo.sectionField = {\n                                name: fieldName,\n                                invisible,\n                            };\n                        } else {\n                            archInfo.rowFields.push({\n                                name: fieldName,\n                                invisible,\n                            });\n                        }\n                        break;\n                    case \"col\":\n                        archInfo.columnFieldName = fieldName;\n                        const { ranges, activeRangeName } = this._extractRanges(node);\n                        archInfo.ranges = ranges;\n                        archInfo.activeRangeName = activeRangeName;\n                        break;\n                    case \"measure\":\n                        if (node.hasAttribute(\"widget\")) {\n                            archInfo.widgetPerFieldName[fieldName] = node.getAttribute(\"widget\");\n                        }\n                        archInfo.measureField = {\n                            name: fieldName,\n                            aggregator: node.getAttribute(\"operator\") || fieldInfo.aggregator,\n                            string,\n                            readonly: exprToBoolean(node.getAttribute(\"readonly\")) || fieldInfo.readonly,\n                        };\n                        break;\n                    case \"readonly\":\n                        let groupOperator = fieldInfo.aggregator;\n                        if (node.hasAttribute(\"operator\")) {\n                            groupOperator = node.getAttribute(\"operator\");\n                        }\n                        archInfo.readonlyField = {\n                            name: fieldName,\n                            aggregator: groupOperator,\n                            string,\n                        };\n                        break;\n                }\n            } else if (node.tagName === \"button\") {\n                archInfo.buttons.push({\n                    ...processButton(node),\n                    type: \"button\",\n                    id: buttonId++,\n                });\n            }\n        });\n        archInfo.editable =\n            archInfo.editable &&\n            archInfo.measureField &&\n            !archInfo.measureField.readonly &&\n            archInfo.measureField.aggregator === \"sum\";\n        return archInfo;\n    }\n\n    /**\n     * Extract the range to display on the view, and filter\n     * them according they should be visible or not (attribute 'invisible')\n     *\n     * @private\n     * @param {Element} colNode - the node of 'col' in grid view arch definition\n     * @returns {\n     *      Object<{\n     *          ranges: {\n     *              name: {name: string, label: string, span: string, step: string, hotkey?: string}\n     *          },\n     *          activeRangeName: string,\n     *      }>\n     *  } list of ranges to apply in the grid view.\n     */\n    _extractRanges(colNode) {\n        const ranges = {};\n        let activeRangeName;\n        let firstRangeName = \"\";\n        for (const rangeNode of colNode.children) {\n            const rangeName = rangeNode.getAttribute(\"name\");\n            if (!firstRangeName.length) {\n                firstRangeName = rangeName;\n            }\n            ranges[rangeName] = {\n                name: rangeName,\n                description: rangeNode.getAttribute(\"string\"),\n                span: rangeNode.getAttribute(\"span\"),\n                step: rangeNode.getAttribute(\"step\"),\n                hotkey: rangeNode.getAttribute(\"hotkey\"),\n                default: exprToBoolean(rangeNode.getAttribute(\"default\")),\n            };\n            if (ranges[rangeName].default) {\n                activeRangeName = rangeName;\n            }\n        }\n        return { ranges: ranges, activeRangeName: activeRangeName || firstRangeName };\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dropdown } from \"@web/core/dropdown/dropdown\";\nimport { DropdownItem } from \"@web/core/dropdown/dropdown_item\";\nimport { serializeDate, deserializeDate } from \"@web/core/l10n/dates\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Layout } from \"@web/search/layout\";\nimport { useModelWithSampleData } from \"@web/model/model\";\nimport { standardViewProps } from \"@web/views/standard_view_props\";\nimport { useViewButtons } from \"@web/views/view_button/view_button_hook\";\nimport { FormViewDialog } from \"@web/views/view_dialogs/form_view_dialog\";\nimport { ViewButton } from \"@web/views/view_button/view_button\";\nimport { useSetupAction } from \"@web/search/action_hook\";\nimport { CogMenu } from \"@web/search/cog_menu/cog_menu\";\nimport { SearchBar } from \"@web/search/search_bar/search_bar\";\nimport { useSearchBarToggler } from \"@web/search/search_bar/search_bar_toggler\";\nimport { browser } from \"@web/core/browser/browser\";\n\nimport { Component, useState, onWillUnmount, useRef } from \"@odoo/owl\";\n\nconst { DateTime } = luxon;\n\nexport class GridController extends Component {\n    static components = {\n        Layout,\n        Dropdown,\n        DropdownItem,\n        ViewButton,\n        CogMenu,\n        SearchBar,\n    };\n\n    static props = {\n        ...standardViewProps,\n        archInfo: Object,\n        buttonTemplate: String,\n        Model: Function,\n        Renderer: Function,\n    };\n\n    static template = \"web_grid.GridView\";\n\n    setup() {\n        const state = this.props.state || {};\n        let activeRangeName = this.props.archInfo.activeRangeName;\n        let defaultAnchor;\n        if (state.activeRangeName) {\n            activeRangeName = state.activeRangeName;\n        } else if (this.isMobile && \"day\" in this.props.archInfo.ranges) {\n            activeRangeName = \"day\";\n        }\n        if (state.anchor) {\n            defaultAnchor = state.anchor;\n        } else if (this.props.context.grid_anchor) {\n            defaultAnchor = deserializeDate(this.props.context.grid_anchor);\n        }\n        this.dialogService = useService(\"dialog\");\n        this.model = useModelWithSampleData(this.props.Model, {\n            resModel: this.props.resModel,\n            sectionField: this.props.archInfo.sectionField,\n            rowFields: this.props.archInfo.rowFields,\n            columnFieldName: this.props.archInfo.columnFieldName,\n            measureField: this.props.archInfo.measureField,\n            readonlyField: this.props.archInfo.readonlyField,\n            fieldsInfo: this.props.relatedModels[this.props.resModel].fields,\n            activeRangeName,\n            ranges: this.props.archInfo.ranges,\n            defaultAnchor,\n        });\n        const rootRef = useRef(\"root\");\n        useSetupAction({\n            rootRef: rootRef,\n            getLocalState: () => {\n                const { anchor, range } = this.model.navigationInfo;\n                return {\n                    anchor,\n                    activeRangeName: range?.name,\n                };\n            }\n        })\n        const isWeekendVisible = browser.localStorage.getItem(\"grid.isWeekendVisible\");\n        this.state = useState({\n            activeRangeName: this.model.navigationInfo.range?.name,\n            isWeekendVisible: isWeekendVisible !== null && isWeekendVisible !== undefined\n                ? JSON.parse(isWeekendVisible)\n                : true,\n        });\n        useViewButtons(rootRef, {\n            beforeExecuteAction: this.beforeExecuteActionButton.bind(this),\n            afterExecuteAction: this.afterExecuteActionButton.bind(this),\n            reload: this.reload.bind(this),\n        });\n        onWillUnmount(() => this.closeDialog?.());\n        this.searchBarToggler = useSearchBarToggler();\n    }\n\n    get isMobile() {\n        return this.env.isSmall;\n    }\n\n    get isEditable() {\n        return (\n            this.props.archInfo.activeActions.edit &&\n            this.props.archInfo.editable\n        );\n    }\n\n    get displayNoContent() {\n        return (\n            !(this.props.archInfo.displayEmpty || this.model.hasData()) || this.model.useSampleModel\n        );\n    }\n\n    get displayAddALine() {\n        return this.props.archInfo.activeActions.create;\n    }\n\n    get hasDisplayableData() {\n        return true;\n    }\n\n    get options() {\n        const { hideLineTotal, hideColumnTotal, hasBarChartTotal, createInline } =\n            this.props.archInfo;\n        return {\n            hideLineTotal,\n            hideColumnTotal,\n            hasBarChartTotal,\n            createInline,\n        };\n    }\n\n    createRecord(params) {\n        const columnContext = this.model.columnFieldIsDate\n            ? {\n                  [`default_${this.model.columnFieldName}`]: serializeDate(\n                      this.model.navigationInfo.anchor\n                  ),\n              }\n            : {};\n        const context = {\n            ...this.props.context,\n            ...columnContext,\n            ...(params?.context || {}),\n        };\n        this.closeDialog = this.dialogService.add(\n            FormViewDialog,\n            {\n                title: _t(\"New Record\"),\n                resModel: this.model.resModel,\n                viewId: this.props.archInfo.formViewId,\n                onRecordSaved: this.onRecordSaved.bind(this),\n                ...(params || {}),\n                context,\n            },\n            {\n                onClose: () => {\n                    this.closeDialog = null;\n                },\n            }\n        );\n    }\n\n    async beforeExecuteActionButton() {}\n\n    async afterExecuteActionButton() {}\n\n    async reload() {\n        await this.model.fetchData();\n    }\n\n    async onRecordSaved(record) {\n        await this.reload();\n    }\n\n    get columns() {\n        return this.state.isWeekendVisible || this.state.activeRangeName === \"day\" ? this.model.columnsArray : this.model.columnsArray.filter(column => {\n            return DateTime.fromISO(column.value).weekday < 6;\n        });\n    }\n\n    toggleWeekendVisibility() {\n        this.state.isWeekendVisible = !this.state.isWeekendVisible;\n        browser.localStorage.setItem(\"grid.isWeekendVisible\", this.state.isWeekendVisible);\n    }\n}\n", "/** @odoo-module */\n\nimport { KeepLast, Mutex } from \"@web/core/utils/concurrency\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { Domain } from \"@web/core/domain\";\nimport { serializeDate } from \"@web/core/l10n/dates\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Model } from \"@web/model/model\";\nimport { browser } from \"@web/core/browser/browser\";\n\nconst { DateTime, Interval } = luxon;\n\nexport class GridCell {\n    /**\n     * Constructor\n     *\n     * @param dataPoint{GridDataPoint} the grid model.\n     * @param row {GridRow} the grid row linked to the cell.\n     * @param column {GridColumn} the grid column linked to the cell.\n     * @param value {Number} the value of the cell.\n     * @param isHovered {Boolean} is the cell in a hover state?\n     */\n    constructor(dataPoint, row, column, value = 0, isHovered = false) {\n        this._dataPoint = dataPoint;\n        this.row = row;\n        this.column = column;\n        this.model = dataPoint.model;\n        this.value = value;\n        this.isHovered = isHovered;\n        this._readonly = false;\n        this.column.addCell(this);\n    }\n\n    get readonly() {\n        return this._readonly || this.column.readonly;\n    }\n\n    /**\n     * Get the domain of the cell, it will be the domain of row AND the one of the column associated\n     *\n     * @return {Domain} the domain of the cell\n     */\n    get domain() {\n        const domains = [this._dataPoint.searchParams.domain, this.row.domain, this.column.domain];\n        return Domain.and(domains);\n    }\n\n    /**\n     * Get the context to get the default values\n     */\n    get context() {\n        return {\n            ...this.row.section?.context,\n            ...this.row.context,\n            ...this.column.context,\n        };\n    }\n\n    get title() {\n        const rowTitle = !this.row.section || this.row.section.isFake\n            ? this.row.title\n            : `${this.row.section.title} / ${this.row.title}`;\n        const columnTitle = this.column.title;\n        return `${rowTitle} (${columnTitle})`;\n    }\n\n    /**\n     * Update the grid cell according to the value set by the current user.\n     *\n     * @param {Number} value the value entered by the current user.\n     */\n    async update(value) {\n        return this.model.mutex.exec(async () => {\n            await this._update(value);\n        });\n    }\n\n    async _update(value) {\n        const oldValue = this.value;\n        const result = await this.model.orm.call(\n            this.model.resModel,\n            \"grid_update_cell\",\n            [this.domain.toList({}), this.model.measureFieldName, value - oldValue],\n            { context: this.context }\n        );\n        if (result) {\n            this.model.actionService.doAction(result);\n            return;\n        }\n        this.row.updateCell(this.column, value);\n        this.model.notify();\n    }\n}\n\nexport class GridRow {\n    /**\n     * Constructor\n     *\n     * @param domain {Domain} the domain of the row.\n     * @param valuePerFieldName {{string: string}} the list of to display the label of the row.\n     * @param dataPoint {GridDataPoint} the grid model.\n     * @param section {GridSection} the section of the grid.\n     * @param columns {GridColumn[]} the columns of the grid.\n     */\n    constructor(domain, valuePerFieldName, dataPoint, section, isAdditionalRow = false) {\n        this._domain = domain;\n        this._dataPoint = dataPoint;\n        this.cells = {};\n        this.valuePerFieldName = valuePerFieldName;\n        this.id = dataPoint.rowId++;\n        this.model = dataPoint.model;\n        this.section = section;\n        if (section) {\n            this.section.addRow(this);\n        }\n        this.grandTotal = 0;\n        this.grandTotalWeekendHidden = 0;\n        this.isAdditionalRow = isAdditionalRow;\n        this._generateCells();\n    }\n\n    get initialRecordValues() {\n        return this.valuePerFieldName;\n    }\n\n    get title() {\n        const labelArray = [];\n        for (const rowField of this._dataPoint.rowFields) {\n            let title = this.valuePerFieldName[rowField.name];\n            if (this.model.fieldsInfo[rowField.name].type === \"many2one\") {\n                if (title) {\n                    title = title[1];\n                } else if (labelArray.length) {\n                    title = \"\";\n                } else {\n                    title = \"None\";\n                }\n            }\n            if (title) {\n                labelArray.push(title);\n            }\n        }\n        return labelArray.join(\" / \");\n    }\n\n    get domain() {\n        if (this.section.isFake) {\n            return this._domain;\n        }\n        return Domain.and([this.section.domain, this._domain]);\n    }\n\n    get context() {\n        const context = {};\n        const getValue = (fieldName, value) =>\n            this.model.fieldsInfo[fieldName].type === \"many2one\" ? value && value[0] : value;\n        for (const [key, value] of Object.entries(this.valuePerFieldName)) {\n            context[`default_${key}`] = getValue(key, value);\n        }\n        return context;\n    }\n\n    getSection() {\n        return !this.section.isFake && this.section;\n    }\n\n    /**\n     * Generate the cells for each column that is present in the row.\n     * @private\n     */\n    _generateCells() {\n        for (const column of this._dataPoint.columnsArray) {\n            this.cells[column.id] = new this.model.constructor.Cell(\n                this._dataPoint,\n                this,\n                column,\n                0\n            );\n        }\n    }\n\n    _ensureColumnExist(column) {\n        if (!(column.id in this._dataPoint.data.columns)) {\n            throw new Error(\"Unbound index: the columnId is not in the row columns\");\n        }\n        return true;\n    }\n\n    /**\n     * Update the cell value of a cell.\n     * @param {GridColumn} column containing the cell to update.\n     * @param {number} value the value to update\n     */\n    updateCell(column, value) {\n        this._ensureColumnExist(column);\n        const cell = this.cells[column.id];\n        const oldValue = cell.value;\n        cell.value = value;\n        const delta = value - oldValue;\n        this.section.updateGrandTotal(column, delta);\n        this.grandTotal += delta;\n        this.grandTotalWeekendHidden += column.isWeekDay ? delta : 0;\n        column.grandTotal += delta;\n        if (this.isAdditionalRow && delta > 0) {\n            this.isAdditionalRow = false;\n        }\n    }\n\n    setReadonlyCell(column, readonly) {\n        this._ensureColumnExist(column);\n        if (readonly instanceof Array) {\n            readonly = readonly.length > 0;\n        } else if (!(readonly instanceof Boolean)) {\n            readonly = Boolean(readonly);\n        }\n        this.cells[column.id]._readonly = readonly;\n    }\n\n    getGrandTotal(showWeekend) {\n        return showWeekend ? this.grandTotal : this.grandTotalWeekendHidden;\n    }\n}\n\nexport class GridSection extends GridRow {\n    constructor() {\n        super(...arguments);\n        this.sectionId = this._dataPoint.sectionId++;\n        this.rows = {};\n        this.isSection = true;\n        this.lastRow = null;\n    }\n\n    get value() {\n        return this.valuePerFieldName && this.valuePerFieldName[this._dataPoint.sectionField.name];\n    }\n\n    get domain() {\n        let value = this.value;\n        if (this.model.fieldsInfo[this._dataPoint.sectionField.name].type === \"many2one\") {\n            value = value && value[0];\n        }\n        return new Domain([[this._dataPoint.sectionField.name, \"=\", value]]);\n    }\n\n    get title() {\n        let title = this.value;\n        if (\n            this._dataPoint.sectionField &&\n            this._dataPoint.fieldsInfo[this._dataPoint.sectionField.name].type === \"many2one\"\n        ) {\n            title = (title && title[1]) || \"None\";\n        }\n        return title;\n    }\n\n    get initialRecordValues() {\n        return { [this._dataPoint.sectionField.name]: this.value };\n    }\n\n    get isFake() {\n        return this.value == null;\n    }\n\n    get context() {\n        const context = {};\n        const getValue = (fieldName, value) =>\n            this.model.fieldsInfo[fieldName].type === \"many2one\" ? value && value[0] : value;\n\n        if (!this.isFake) {\n            const sectionFieldName = this._dataPoint.sectionField.name;\n            context[`default_${sectionFieldName}`] = getValue(sectionFieldName, this.value);\n        }\n        return context;\n    }\n\n    getSection() {\n        return !this.isFake && this;\n    }\n\n    /**\n     * Add row to the section rows.\n     * @param row {GridRow} the row to add.\n     */\n    addRow(row) {\n        if (row.id in this.rows) {\n            throw new Error(\"Row already added in section\");\n        }\n        this.rows[row.id] = row;\n        this.lastRow = row;\n    }\n\n    /**\n     * Update the grand totals according to the provided column and delta.\n     * @param column {GridColumn} the column the grand total has to be updated for.\n     * @param delta {Number} the delta to apply on the grand totals.\n     */\n    updateGrandTotal(column, delta) {\n        this.cells[column.id].value += delta;\n        this.grandTotal += delta;\n        this.grandTotalWeekendHidden += column.isWeekDay ? delta : 0;\n    }\n}\n\nexport class GridColumn {\n    /**\n     * Constructor\n     *\n     * @param dataPoint {GridDataPoint} dataPoint of the grid.\n     * @param title {string} the title of the column to display.\n     */\n    constructor(dataPoint, title, value, readonly = false) {\n        this._dataPoint = dataPoint;\n        this.model = dataPoint.model;\n        this.title = title;\n        this.value = value;\n        this.cells = [];\n        this.id = dataPoint.columnId++;\n        this.grandTotal = 0;\n        this.readonly = readonly;\n    }\n\n    /**\n     * Add the cell to the column cells.\n     * @param cell {GridCell} the cell to add.\n     */\n    addCell(cell) {\n        if (cell.id in this.cells) {\n            throw new Error(\"Cell already added in column\");\n        }\n        this.cells.push(cell);\n        this.grandTotal += cell.value;\n    }\n\n    get domain() {\n        return new Domain([[this._dataPoint.columnFieldName, \"=\", this.value]]);\n    }\n\n    get context() {\n        return { [`default_${this._dataPoint.columnFieldName}`]: this.value };\n    }\n}\n\nexport class DateGridColumn extends GridColumn {\n    /**\n     * Constructor\n     *\n     * @param dataPoint {GridDataPoint} data point of the grid.\n     * @param title {string} the title of the column to display.\n     * @param dateStart {String} the date start serialized\n     * @param dateEnd {String} the date end serialized\n     * @param isToday {Boolean} is the date column representing today?\n     */\n    constructor(dataPoint, title, dateStart, dateEnd, isToday, isWeekDay, readonly = false) {\n        super(dataPoint, title, dateStart, readonly);\n        this.dateEnd = dateEnd;\n        this.isToday = isToday;\n        this.isWeekDay = isWeekDay;\n    }\n\n    get domain() {\n        return new Domain([\n            \"&\",\n            [this._dataPoint.columnFieldName, \">=\", this.value],\n            [this._dataPoint.columnFieldName, \"<\", this.dateEnd],\n        ]);\n    }\n}\n\nexport class GridDataPoint {\n    constructor(model, params) {\n        this.model = model;\n        const { rowFields, sectionField, searchParams } = params;\n        this.rowFields = rowFields;\n        this.sectionField = sectionField;\n        this.searchParams = searchParams;\n        this.sectionId = 0;\n        this.rowId = 0;\n        this.columnId = 0;\n    }\n\n    get orm() {\n        return this.model.orm;\n    }\n\n    get Section() {\n        return this.model.constructor.Section;\n    }\n\n    get Row() {\n        return this.model.constructor.Row;\n    }\n\n    get Column() {\n        return this.model.constructor.Column;\n    }\n\n    get DateColumn() {\n        return this.model.constructor.DateColumn;\n    }\n\n    get Cell() {\n        return this.model.constructor.Cell;\n    }\n\n    get fieldsInfo() {\n        return this.model.fieldsInfo;\n    }\n\n    get columnFieldName() {\n        return this.model.columnFieldName;\n    }\n\n    get resModel() {\n        return this.model.resModel;\n    }\n\n    get fields() {\n        return this._getFields();\n    }\n\n    get groupByFields() {\n        return this._getFields(true);\n    }\n\n    get navigationInfo() {\n        return this.model.navigationInfo;\n    }\n\n    get dateFormat() {\n        return { day: \"ccc,\\nMMM\\u00A0d\", month: \"MMMM\\nyyyy\" };\n    }\n\n    get columnFieldIsDate() {\n        return this.model.columnFieldIsDate;\n    }\n\n    get columnGroupByFieldName() {\n        return this.columnFieldIsDate\n            ? this.navigationInfo.range.name === 'year' ? `${this.columnFieldName}:month` : `${this.columnFieldName}:day`\n            : this.columnFieldName;\n    }\n\n    get readonlyField() {\n        return this.model.readonlyField;\n    }\n\n    get sectionsArray() {\n        return Object.values(this.data.sections);\n    }\n\n    get rowsArray() {\n        return Object.values(this.data.rows);\n    }\n\n    get columnsArray() {\n        return Object.values(this.data.columns);\n    }\n\n    /**\n     * Get fields to use in the group by or in fields of the read_group\n     * @private\n     * @param grouped true to return the fields for the group by.\n     * @return {string[]} list of fields name.\n     */\n    _getFields(grouped = false) {\n        const fields = [];\n        if (!grouped) {\n            fields.push(\n                this.columnFieldName,\n                this.model.measureGroupByFieldName,\n                \"ids:array_agg(id)\"\n            );\n            if (this.readonlyField) {\n                const aggReadonlyField = `${this.readonlyField.name}:${this.readonlyField.aggregator}`;\n                fields.push(aggReadonlyField);\n            }\n        } else {\n            fields.push(this.columnGroupByFieldName);\n        }\n        fields.push(...this.rowFields.map((r) => r.name));\n        if (this.sectionField) {\n            fields.push(this.sectionField.name);\n        }\n        return fields;\n    }\n\n    _getDateColumnTitle(date) {\n        if (this.navigationInfo.range.step in this.dateFormat) {\n            return date.toFormat(this.dateFormat[this.navigationInfo.range.step]);\n        }\n        return serializeDate(date);\n    }\n\n    /**\n     * Generate the date columns.\n     * @private\n     * @return {GridColumn[]}\n     */\n    _generateDateColumns() {\n        const generateNext = (dateStart) =>\n            dateStart.plus({ [`${this.navigationInfo.range.step}s`]: 1 });\n        for (\n            let currentDate = this.navigationInfo.periodStart;\n            currentDate < this.navigationInfo.periodEnd;\n            currentDate = generateNext(currentDate)\n        ) {\n            const domainStart = currentDate;\n            const domainStop = generateNext(currentDate);\n            const domainStartSerialized = serializeDate(domainStart);\n            const isWeekDay = currentDate.weekday < 6;\n            const column = new this.DateColumn(\n                this,\n                this._getDateColumnTitle(currentDate),\n                domainStartSerialized,\n                serializeDate(domainStop),\n                currentDate.startOf(\"day\").equals(this.model.today.startOf(\"day\")),\n                isWeekDay,\n            );\n            this.data.columns[column.id] = column;\n            this.data.columnsKeyToIdMapping[domainStartSerialized] = column.id;\n        }\n    }\n\n    /**\n     * Search grid columns\n     *\n     * @param {Array} domain domain to filter the result\n     * @param {string} readonlyField field uses to make column readonly if true\n     * @returns {Array} array containing id, display_name and readonly if readonlyField is defined.\n     */\n    async _searchMany2oneColumns(domain, readonlyField) {\n        const fieldsToFetch = [\"id\", \"display_name\"];\n        if (readonlyField) {\n            fieldsToFetch.push(readonlyField);\n        }\n        const columnField = this.fieldsInfo[this.columnFieldName];\n        const columnRecords = await this.orm.searchRead(\n            columnField.relation,\n            domain || [],\n            fieldsToFetch\n        );\n        return columnRecords.map((read) => Object.values(read));\n    }\n\n    /**\n     * Initialize the data.\n     * @private\n     */\n    async _initialiseData() {\n        this.data = {\n            columnsKeyToIdMapping: {},\n            columns: {},\n            rows: {},\n            rowsKeyToIdMapping: {},\n            fieldsInfo: this.fieldsInfo,\n            sections: {},\n            sectionsKeyToIdMapping: {},\n        };\n        this.record = {\n            context: {},\n            resModel: this.resModel,\n            resIds: [],\n        };\n        let columnRecords = [];\n        const columnField = this.fieldsInfo[this.columnFieldName];\n        if (this.columnFieldIsDate) {\n            this._generateDateColumns();\n        } else {\n            if (columnField.type === \"selection\") {\n                const selectionFieldValues = await this.orm.call(\n                    \"ir.model.fields\",\n                    \"get_field_selection\",\n                    [this.resModel, this.columnFieldName]\n                );\n                columnRecords = selectionFieldValues;\n            } else if (columnField.type === \"many2one\") {\n                columnRecords = await this._searchMany2oneColumns();\n            } else {\n                throw new Error(\n                    \"Unmanaged column type. Supported types are date, selection and many2one.\"\n                );\n            }\n            for (const record of columnRecords) {\n                let readonly = false;\n                let key, value;\n                if (record.length === 2) {\n                    [key, value] = record;\n                } else {\n                    [key, value, readonly] = record;\n                }\n                const column = new this.Column(this, value, key, Boolean(readonly));\n                this.data.columns[column.id] = column;\n                this.data.columnsKeyToIdMapping[key] = column.id;\n            }\n        }\n    }\n\n    async fetchData() {\n        const data = await this.orm.webReadGroup(\n            this.resModel,\n            Domain.and([this.searchParams.domain, this.model.generateNavigationDomain()]).toList(\n                {}\n            ),\n            this.fields,\n            this.groupByFields,\n            {\n                lazy: false,\n            }\n        );\n        if (this.orm.isSample) {\n            data.groups = data.groups.filter((group) => {\n                const date = DateTime.fromISO(group[\"__range\"][this.columnGroupByFieldName].from);\n                return (\n                    date >= this.navigationInfo.periodStart && date <= this.navigationInfo.periodEnd\n                );\n            });\n        }\n        return data;\n    }\n\n    /**\n     * Gets additional groups to be added to the grid. The call to this function is made in parallel to the main data\n     * fetching.\n     *\n     * This function is intended to be overriden in modules where we want to display additional sections and/or rows in\n     * the grid than what would be returned by the webReadGroup.\n     * The model `sectionField` and `rowFields` can be used in order to know what need to be returned.\n     *\n     * An example of this is:\n     * - when considering timesheet, we want to ease their encoding by adding (to the data that is fetched for scale),\n     *   the entries that have been entered the week before. That way, the first day of week\n     *   (or month, depending on the scale), a line is already displayed with 0's and can directly been used in the\n     *   grid instead of having to use the create button.\n     *\n     * @return {Array<Promise<Object>>} an array of Promise of Object of type:\n     *                                      {\n     *                                          sectionKey: {\n     *                                              value: Any,\n     *                                              rows: {\n     *                                                  rowKey: {\n     *                                                      domain: Domain,\n     *                                                      values: [Any],\n     *                                                  },\n     *                                              },\n     *                                          },\n     *                                      }\n     * @private\n     */\n    _fetchAdditionalData() {\n        return [];\n    }\n\n    /**\n     * Gets additional groups to be added to the grid. The call to this function is made after the main data fetching\n     * has been processed which allows using `data` in the code.\n     * This function is intended to be overriden in modules where we want to display additional sections and/or rows in\n     * the grid than what would be returned by the webReadGroup.\n     * The model `sectionField`, `rowFields` as well as `data` can be used in order to know what need to be returned.\n     *\n     * @return {Array<Promise<Object>>} an array of Promise of Object of type:\n     *                                      {\n     *                                          sectionKey: {\n     *                                              value: Any,\n     *                                              rows: {\n     *                                                  rowKey: {\n     *                                                      domain: Domain,\n     *                                                      values: [Any],\n     *                                                  },\n     *                                              },\n     *                                          },\n     *                                      }\n     * @private\n     */\n    _postFetchAdditionalData() {\n        return [];\n    }\n\n    _getAdditionalPromises() {\n        return [this._fetchUnavailabilityDays()];\n    }\n\n    async _fetchUnavailabilityDays(args = {}) {\n        if (!this.columnFieldIsDate) {\n            return {};\n        }\n        const result = await this.orm.call(\n            this.resModel,\n            \"grid_unavailability\",\n            [\n                serializeDate(this.navigationInfo.periodStart),\n                serializeDate(this.navigationInfo.periodEnd),\n            ],\n            {\n                ...args,\n            }\n        );\n        this._processUnavailabilityDays(result);\n    }\n\n    _processUnavailabilityDays(result) {\n        return;\n    }\n\n    /**\n     * Generate the row key according to the provided read group result.\n     * @param readGroupResult {Array} the read group result the key has to be generated for.\n     * @private\n     * @return {string}\n     */\n    _generateRowKey(readGroupResult) {\n        let key = \"\";\n        const sectionKey =\n            (this.sectionField && this._generateSectionKey(readGroupResult)) || false;\n        for (const rowField of this.rowFields) {\n            let value = rowField.name in readGroupResult && readGroupResult[rowField.name];\n            if (this.fieldsInfo[rowField.name].type === \"many2one\") {\n                value = value && value[0];\n            }\n            key += `${value}\\\\|/`;\n        }\n        return `${sectionKey}@|@${key}`;\n    }\n\n    /**\n     * Generate the section\n     * @param readGroupResult\n     * @private\n     */\n    _generateSectionKey(readGroupResult) {\n        let value = readGroupResult[this.sectionField.name];\n        if (this.fieldsInfo[this.sectionField.name].type === \"many2one\") {\n            value = value && value[0];\n        }\n        return `/|\\\\${value.toString()}`;\n    }\n\n    /**\n     * Generate the row domain for the provided read group result.\n     * @param readGroupResult {Array} the read group result the domain has to be generated for.\n     * @return {{domain: Domain, values: Object}} the generated domain and values.\n     */\n    _generateRowDomainAndValues(readGroupResult) {\n        let domain = new Domain();\n        const values = {};\n        for (const rowField of this.rowFields) {\n            const result = rowField.name in readGroupResult && readGroupResult[rowField.name];\n            let value = result;\n            if (this.fieldsInfo[rowField.name].type === \"many2one\") {\n                value = value && value[0];\n            }\n            values[rowField.name] = result;\n            domain = Domain.and([domain, [[rowField.name, \"=\", value]]]);\n        }\n        return { domain, values };\n    }\n\n    _generateFakeSection() {\n        const section = new this.Section(null, null, this, null);\n        this.data.sections[section.id] = section;\n        this.data.sectionsKeyToIdMapping[\"false\"] = section.id;\n        this.data.rows[section.id] = section;\n        this.data.rowsKeyToIdMapping[\"false\"] = section.id;\n        return section;\n    }\n\n    async _generateData(readGroupResults) {\n        let section;\n        for (const readGroupResult of readGroupResults.groups) {\n            if (!this.orm.isSample) {\n                this.record.resIds.push(...readGroupResult.ids);\n            }\n            const rowKey = this._generateRowKey(readGroupResult);\n            if (this.sectionField) {\n                const sectionKey = this._generateSectionKey(readGroupResult);\n                if (!(sectionKey in this.data.sectionsKeyToIdMapping)) {\n                    const newSection = new this.Section(\n                        null,\n                        { [this.sectionField.name]: readGroupResult[this.sectionField.name] },\n                        this,\n                        null\n                    );\n                    this.data.sections[newSection.id] = newSection;\n                    this.data.sectionsKeyToIdMapping[sectionKey] = newSection.id;\n                    this.data.rows[newSection.id] = newSection;\n                    this.data.rowsKeyToIdMapping[sectionKey] = newSection.id;\n                }\n                section = this.data.sections[this.data.sectionsKeyToIdMapping[sectionKey]];\n            } else if (Object.keys(this.data.sections).length === 0) {\n                section = this._generateFakeSection();\n            }\n            let row;\n            if (!(rowKey in this.data.rowsKeyToIdMapping)) {\n                const { domain, values } = this._generateRowDomainAndValues(readGroupResult);\n                row = new this.Row(domain, values, this, section);\n                this.data.rows[row.id] = row;\n                this.data.rowsKeyToIdMapping[rowKey] = row.id;\n            } else {\n                row = this.data.rows[this.data.rowsKeyToIdMapping[rowKey]];\n            }\n            let columnKey;\n            if (this.columnFieldIsDate) {\n                columnKey = readGroupResult[\"__range\"][this.columnGroupByFieldName].from;\n            } else {\n                const columnField = this.fieldsInfo[this.columnFieldName];\n                if (columnField.type === \"selection\") {\n                    columnKey = readGroupResult[this.columnFieldName];\n                } else if (columnField.type === \"many2one\") {\n                    columnKey = readGroupResult[this.columnFieldName][0];\n                } else {\n                    throw new Error(\n                        \"Unmanaged column type. Supported types are date, selection and many2one.\"\n                    );\n                }\n            }\n            if (this.data.columnsKeyToIdMapping[columnKey] in this.data.columns) {\n                const column = this.data.columns[this.data.columnsKeyToIdMapping[columnKey]];\n                row.updateCell(column, readGroupResult[this.model.measureFieldName]);\n                if (this.readonlyField && this.readonlyField.name in readGroupResult) {\n                    row.setReadonlyCell(column, readGroupResult[this.readonlyField.name]);\n                }\n            }\n        }\n    }\n\n    /**\n     * Method meant to be overridden whenever an item (row and section) post process is needed.\n     * @param item {GridSection|GridRow}\n     */\n    _itemsPostProcess(item) {}\n\n    async load() {\n        await this._initialiseData();\n\n        const mergeAdditionalData = (fetchedData) => {\n            const additionalData = {};\n            for (const data of fetchedData) {\n                for (const [sectionKey, sectionInfo] of Object.entries(data)) {\n                    if (!(sectionKey in additionalData)) {\n                        additionalData[sectionKey] = sectionInfo;\n                    } else {\n                        for (const [rowKey, rowInfo] of Object.entries(sectionInfo.rows)) {\n                            if (!(rowKey in additionalData[sectionKey].rows)) {\n                                additionalData[sectionKey].rows[rowKey] = rowInfo;\n                            }\n                        }\n                    }\n                }\n            }\n            return additionalData;\n        };\n\n        const appendAdditionData = (additionalData) => {\n            for (const [sectionKey, sectionInfo] of Object.entries(additionalData)) {\n                if (!(sectionKey in this.data.sectionsKeyToIdMapping)) {\n                    if (this.sectionField) {\n                        const newSection = new this.Section(\n                            null,\n                            { [this.sectionField.name]: sectionInfo.value },\n                            this,\n                            null\n                        );\n                        this.data.sections[newSection.id] = newSection;\n                        this.data.sectionsKeyToIdMapping[sectionKey] = newSection.id;\n                        this.data.rows[newSection.id] = newSection;\n                        this.data.rowsKeyToIdMapping[sectionKey] = newSection.id;\n                    } else {\n                        // if no sectionField and the section is not in sectionsKeyToIdMapping then no section is generated\n                        this._generateFakeSection();\n                    }\n                }\n                const section = this.data.sections[this.data.sectionsKeyToIdMapping[sectionKey]];\n                for (const [rowKey, rowInfo] of Object.entries(sectionInfo.rows)) {\n                    if (!(rowKey in this.data.rowsKeyToIdMapping)) {\n                        const newRow = new this.Row(\n                            rowInfo.domain,\n                            rowInfo.values,\n                            this,\n                            section,\n                            true\n                        );\n                        this.data.rows[newRow.id] = newRow;\n                        this.data.rowsKeyToIdMapping[rowKey] = newRow.id;\n                        for (const column of Object.values(this.data.columns)) {\n                            newRow.updateCell(column, 0);\n                        }\n                    }\n                }\n            }\n        };\n\n        const [data, additionalData] = await Promise.all([\n            this.fetchData(),\n            Promise.all(this._fetchAdditionalData()),\n        ]);\n        this._generateData(data);\n        appendAdditionData(mergeAdditionalData(additionalData));\n        if (!this.orm.isSample) {\n            const [, postFetchAdditionalData] = await Promise.all([\n                Promise.all(this._getAdditionalPromises()),\n                Promise.all(this._postFetchAdditionalData()),\n            ]);\n            appendAdditionData(mergeAdditionalData(postFetchAdditionalData));\n        }\n\n        this.data.items = [];\n        for (const section of this.sectionsArray) {\n            this.data.items.push(section);\n            this._itemsPostProcess(section);\n            for (const rowId in section.rows) {\n                const row = section.rows[rowId];\n                this._itemsPostProcess(row);\n                this.data.items.push(row);\n            }\n        }\n    }\n}\n\nexport class GridNavigationInfo {\n    constructor(anchor, model) {\n        this.anchor = anchor;\n        this.model = model;\n    }\n\n    get _targetWeekday() {\n        const firstDayOfWeek = localization.weekStart;\n        return this.anchor.weekday < firstDayOfWeek ? firstDayOfWeek - 7 : firstDayOfWeek;\n    }\n\n    get periodStart() {\n        if (this.range.span !== \"week\") {\n            return this.anchor.startOf(this.range.span);\n        }\n        // Luxon's default is monday to monday week so we need to change its behavior.\n        return this.anchor.set({ weekday: this._targetWeekday }).startOf(\"day\");\n    }\n\n    get periodEnd() {\n        if (this.range.span !== \"week\") {\n            return this.anchor.endOf(this.range.span);\n        }\n        // Luxon's default is monday to monday week so we need to change its behavior.\n        return this.anchor\n            .set({ weekday: this._targetWeekday })\n            .plus({ weeks: 1, days: -1 })\n            .endOf(\"day\");\n    }\n\n    get interval() {\n        return Interval.fromDateTimes(this.periodStart, this.periodEnd);\n    }\n\n    contains(date) {\n        return this.interval.contains(date.startOf(\"day\"));\n    }\n}\n\nexport class GridModel extends Model {\n    static DataPoint = GridDataPoint;\n    static Cell = GridCell;\n    static Column = GridColumn;\n    static DateColumn = DateGridColumn;\n    static Row = GridRow;\n    static Section = GridSection;\n    static NavigationInfo = GridNavigationInfo;\n\n    setup(params) {\n        this.notificationService = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.keepLast = new KeepLast();\n        this.mutex = new Mutex();\n        this.defaultSectionField = params.sectionField;\n        this.defaultRowFields = params.rowFields;\n        this.resModel = params.resModel;\n        this.fieldsInfo = params.fieldsInfo;\n        this.columnFieldName = params.columnFieldName;\n        this.columnFieldIsDate = this.fieldsInfo[params.columnFieldName].type === \"date\";\n        this.measureField = params.measureField;\n        this.readonlyField = params.readonlyField;\n        this.ranges = params.ranges;\n        this.defaultAnchor = params.defaultAnchor || this.today;\n        this.navigationInfo = new this.constructor.NavigationInfo(this.defaultAnchor, this);\n        const activeRangeName =\n            browser.localStorage.getItem(this.storageKey) || params.activeRangeName;\n        if (Object.keys(this.ranges).length && activeRangeName) {\n            this.navigationInfo.range = this.ranges[activeRangeName];\n        }\n    }\n\n    get data() {\n        return this._dataPoint?.data || {};\n    }\n\n    get record() {\n        return this._dataPoint?.record || {};\n    }\n\n    get today() {\n        return DateTime.local().startOf(\"day\");\n    }\n\n    get sectionsArray() {\n        return Object.values(this.data.sections);\n    }\n\n    get itemsArray() {\n        return this.data.items;\n    }\n\n    get columnsArray() {\n        return Object.values(this.data.columns);\n    }\n\n    get maxColumnsTotal() {\n        return Math.max(...this.columnsArray.map((c) => c.grandTotal));\n    }\n\n    get measureFieldName() {\n        return this.measureField.name;\n    }\n\n    get measureGroupByFieldName() {\n        if (this.measureField.aggregator) {\n            return `${this.measureFieldName}:${this.measureField.aggregator}`;\n        }\n        return this.measureFieldName;\n    }\n\n    get storageKey() {\n        return `scaleOf-viewId-${this.env.config.viewId}`;\n    }\n\n    isToday(date) {\n        return date.startOf(\"day\").equals(this.today.startOf(\"day\"));\n    }\n\n    /**\n     * Set the new range according to the range name passed into parameter.\n     * @param rangeName {string} the range name to set.\n     */\n    async setRange(rangeName) {\n        this.navigationInfo.range = this.ranges[rangeName];\n        browser.localStorage.setItem(this.storageKey, rangeName);\n        await this.fetchData();\n    }\n\n    async setAnchor(anchor) {\n        this.navigationInfo.anchor = anchor;\n        await this.fetchData();\n    }\n\n    async setTodayAnchor() {\n        await this.setAnchor(this.today);\n    }\n\n    /**\n     * @override\n     */\n    hasData() {\n        return this.sectionsArray.length;\n    }\n\n    generateNavigationDomain() {\n        if (this.columnFieldIsDate) {\n            return new Domain([\n                \"&\",\n                [this.columnFieldName, \">=\", serializeDate(this.navigationInfo.periodStart)],\n                [this.columnFieldName, \"<=\", serializeDate(this.navigationInfo.periodEnd)],\n            ]);\n        } else {\n            return Domain.TRUE;\n        }\n    }\n\n    /**\n     * Reset the anchor\n     */\n    async resetAnchor() {\n        await this.setAnchor(this.defaultAnchor);\n    }\n\n    /**\n     * Move the anchor to the next/previous step\n     * @param direction {\"forward\"|\"backward\"} the direction to the move the anchor\n     */\n    async moveAnchor(direction) {\n        if (direction == \"forward\") {\n            this.navigationInfo.anchor = this.navigationInfo.anchor.plus({\n                [this.navigationInfo.range.span]: 1,\n            });\n        } else if (direction == \"backward\") {\n            this.navigationInfo.anchor = this.navigationInfo.anchor.minus({\n                [this.navigationInfo.range.span]: 1,\n            });\n        } else {\n            throw Error(\"Invalid argument\");\n        }\n        if (\n            this.navigationInfo.contains(this.today) &&\n            this.navigationInfo.anchor.startOf(\"day\").equals(this.today.startOf(\"day\"))\n        ) {\n            this.navigationInfo.anchor = this.today;\n        }\n        await this.fetchData();\n    }\n\n    /**\n     * Load the model\n     *\n     * @override\n     * @param params {Object} the search parameters (domain, groupBy, etc.)\n     * @return {Promise<void>}\n     */\n    async load(params = {}) {\n        const searchParams = {\n            ...this.searchParams,\n            ...params,\n        };\n        const groupBys = [];\n        let notificationDisplayed = false;\n        for (const groupBy of searchParams.groupBy) {\n            if (groupBy.startsWith(this.columnFieldName)) {\n                if (!notificationDisplayed) {\n                    this.notificationService.add(\n                        _t(\n                            \"Grouping by the field used in the column of the grid view is not possible.\"\n                        ),\n                        { type: \"warning\" }\n                    );\n                    notificationDisplayed = true;\n                }\n            } else {\n                groupBys.push(groupBy);\n            }\n        }\n        if (searchParams.length !== groupBys.length) {\n            searchParams.groupBy = groupBys;\n        }\n        let rowFields = [];\n        let sectionField;\n        if (searchParams.groupBy.length) {\n            if (\n                this.defaultSectionField &&\n                searchParams.groupBy.length > 1 &&\n                searchParams.groupBy[0] === this.defaultSectionField.name\n            ) {\n                sectionField = this.defaultSectionField;\n            }\n            const rowFieldPerFieldName = Object.fromEntries(\n                this.defaultRowFields.map((r) => [r.name, r])\n            );\n            for (const groupBy of searchParams.groupBy) {\n                if (sectionField && groupBy === sectionField.name) {\n                    continue;\n                }\n                if (groupBy in rowFieldPerFieldName) {\n                    rowFields.push({\n                        ...rowFieldPerFieldName[groupBy],\n                        invisible: \"False\",\n                    });\n                } else {\n                    rowFields.push({ name: groupBy });\n                }\n            }\n        } else {\n            if (this.defaultSectionField && (this.defaultSectionField.invisible !== \"True\" && this.defaultSectionField.invisible !== \"1\")) {\n                sectionField = this.defaultSectionField;\n            }\n            rowFields = this.defaultRowFields.filter((r) => (r.invisible !== \"True\" && r.invisible !== \"1\"));\n        }\n\n        const dataPoint = new this.constructor.DataPoint(this, {\n            searchParams,\n            rowFields,\n            sectionField,\n        });\n        await this.keepLast.add(dataPoint.load());\n        this._dataPoint = dataPoint;\n\n        this.searchParams = searchParams;\n        this.rowFields = rowFields;\n        this.sectionField = sectionField;\n    }\n\n    async fetchData(params = {}) {\n        await this.load(params);\n        this.useSampleModel = false;\n        this.notify();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Domain } from \"@web/core/domain\";\nimport { registry } from \"@web/core/registry\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { useDebounced } from \"@web/core/utils/timing\";\nimport { useVirtualGrid } from \"@web/core/virtual_grid_hook\";\nimport { Field } from \"@web/views/fields/field\";\nimport { Record } from \"@web/model/record\";\nimport { getActiveHotkey } from \"@web/core/hotkeys/hotkey_service\";\nimport { ViewScaleSelector } from \"@web/views/view_components/view_scale_selector\";\n\nimport { GridComponent } from \"@web_grid/components/grid_component/grid_component\";\n\nimport {\n    Component,\n    markup,\n    useState,\n    onWillUpdateProps,\n    onMounted,\n    onPatched,\n    reactive,\n    useRef,\n    useExternalListener,\n} from \"@odoo/owl\";\n\nexport class GridRenderer extends Component {\n    static components = {\n        Field,\n        GridComponent,\n        Record,\n        ViewScaleSelector,\n    };\n\n    static template = \"web_grid.Renderer\";\n\n    static props = {\n        sections: { type: Array, optional: true },\n        columns: { type: Array, optional: true },\n        rows: { type: Array, optional: true },\n        model: { type: Object, optional: true },\n        options: Object,\n        sectionField: { type: Object, optional: true },\n        rowFields: Array,\n        measureField: Object,\n        isEditable: Boolean,\n        widgetPerFieldName: Object,\n        openAction: { type: Object, optional: true },\n        contentRef: Object,\n        createInline: Boolean,\n        createRecord: Function,\n        ranges: { type: Object, optional: true },\n        state: Object,\n        toggleWeekendVisibility: Function,\n    };\n\n    static defaultProps = {\n        sections: [],\n        columns: [],\n        rows: [],\n        model: {},\n        ranges: {},\n    };\n\n    setup() {\n        this.rendererRef = useRef(\"renderer\");\n        this.actionService = useService(\"action\");\n        this.editionState = useState({\n            hoveredCellInfo: false,\n            editedCellInfo: false,\n        });\n        this.hoveredElement = null;\n        const measureFieldName = this.props.model.measureFieldName;\n        const fieldInfo = this.props.model.fieldsInfo[measureFieldName];\n        const measureFieldWidget = this.props.widgetPerFieldName[measureFieldName];\n        const widgetName = measureFieldWidget || fieldInfo.type;\n        this.gridCell = registry.category(\"grid_components\").get(widgetName);\n        this.hoveredCellProps = {\n            // props cell hovered\n            name: measureFieldName,\n            type: widgetName,\n            component: this.gridCell.component,\n            reactive: reactive({ cell: null }),\n            fieldInfo,\n            readonly: !this.props.isEditable,\n            openRecords: this.openRecords.bind(this),\n            editMode: false,\n            onEdit: this.onEditCell.bind(this),\n            getCell: this.getCell.bind(this),\n            isMeasure: true,\n        };\n        this.editCellProps = {\n            // props for cell in edit mode\n            name: measureFieldName,\n            type: widgetName,\n            component: this.gridCell.component,\n            reactive: reactive({ cell: null }),\n            fieldInfo,\n            readonly: !this.props.isEditable,\n            openRecords: this.openRecords.bind(this),\n            editMode: true,\n            onEdit: this.onEditCell.bind(this),\n            getCell: this.getCell.bind(this),\n            onKeyDown: this.onCellKeydown.bind(this),\n            isMeasure: true,\n        };\n        this.isEditing = false;\n        onWillUpdateProps(this.onWillUpdateProps);\n        onMounted(this._focusOnToday);\n        onPatched(this._focusOnToday);\n        // This property is used to avoid refocus on today whenever a cell value is updated.\n        this.shouldFocusOnToday = true;\n        this.onMouseOver = useDebounced(this._onMouseOver, 10);\n        this.onMouseOut = useDebounced(this._onMouseOut, 10);\n        this.virtualGrid = useVirtualGrid({\n            scrollableRef: this.props.contentRef,\n            initialScroll: { top: 60 },\n        });\n        useExternalListener(window, \"click\", this.onClick);\n        useExternalListener(window, \"keydown\", this.onKeyDown);\n    }\n\n    getCell(rowId, columnId) {\n        return this.props.model.data.rows[rowId]?.cells[columnId];\n    }\n\n    getItemHeight(item) {\n        let height = this.rowHeight;\n        if (item.isSection && item.isFake) {\n            return 0;\n        }\n        if (this.props.createInline && !item.isSection && item.section.lastRow.id === item.id) {\n            height *= 2; // to include the Add a line row\n        }\n        return height;\n    }\n\n    get isMobile() {\n        return this.env.isSmall;\n    }\n\n    get rowHeight() {\n        return this.isMobile ? 48:32;\n    }\n\n    get virtualRows() {\n        this.virtualGrid.setRowsHeights(this.props.rows.map((row) => this.getItemHeight(row)));\n        const [start, end] = this.virtualGrid.rowsIndexes;\n        return this.props.rows.slice(start, end + 1);\n    }\n\n    getRowPosition(row, isCreateInlineRow = false) {\n        const rowIndex = row ? this.props.rows.findIndex((r) => r.id === row.id) : 0;\n        const section = row && row.getSection();\n        const sectionDisplayed = Boolean(section && (section.value || this.props.sections.length > 1));\n        let rowPosition = this.rowsGap + rowIndex + 1 + (sectionDisplayed ? section.sectionId : 0);\n        if (isCreateInlineRow) {\n            rowPosition += 1;\n        }\n        if (!sectionDisplayed) {\n            rowPosition -= 1;\n        }\n        return rowPosition;\n    }\n\n    getTotalRowPosition() {\n        let sectionIndex = 0;\n        if (this.props.model.sectionField && this.props.sections.length) {\n            if (this.props.sections.length > 1 || this.props.sections[0].value) {\n                sectionIndex = this.props.sections.length;\n            }\n        }\n        return (\n            (this.props.rows.length || 1) +\n            sectionIndex +\n            (this.props.createInline ? 1 : 0) +\n            this.rowsGap\n        );\n    }\n\n    onWillUpdateProps(nextProps) {}\n\n    formatValue(value) {\n        return this.gridCell.formatter(value);\n    }\n\n    /**\n     * @deprecated\n     * TODO: [XBO] remove me in master\n     * @param {*} data\n     */\n    getDefaultState(data) {\n        return {};\n    }\n\n    get rowsCount() {\n        const addLineRows = this.props.createInline ? this.props.sections.length || 1 : 0;\n        return this.props.rows.length - (this.props.model.sectionField ? 0 : 1) + addLineRows;\n    }\n\n    get gridTemplateRows() {\n        let totalRows = 0;\n        if (!this.props.options.hideColumnTotal) {\n            totalRows += 1;\n            if (this.props.options.hasBarChartTotal) {\n                totalRows += 1;\n            }\n        }\n        // Row height must be hard-coded for the virtual hook to work properly.\n        return `auto repeat(${this.rowsCount + totalRows}, ${this.rowHeight}px)`;\n    }\n\n    get gridTemplateColumns() {\n        return `auto repeat(${this.props.columns.length}, ${\n            this.props.columns.length > 7 ? \"minmax(8ch, auto)\" : \"minmax(10ch, 1fr)\"\n        }) minmax(10ch, 10em)`;\n    }\n\n    get measureLabel() {\n        const measureFieldName = this.props.model.measureFieldName;\n        if (measureFieldName === \"__count\") {\n            return _t(\"Total\");\n        }\n        return (\n            this.props.measureField.string || this.props.model.fieldsInfo[measureFieldName].string\n        );\n    }\n\n    get rowsGap() {\n        return 1;\n    }\n\n    get columnsGap() {\n        return 1;\n    }\n\n    get displayAddLine() {\n        return this.props.createInline && this.row.id === this.row.section.lastRow.id;\n    }\n\n    getCellColorClass(column) {\n        return \"text-900\";\n    }\n\n    getSectionColumnsClasses(column, row) {\n        const isToday = column.isToday;\n        return {\n            'bg-info bg-opacity-50': isToday,\n            'bg-200 border-top': !isToday,\n            'bg-opacity-75': this.getUnavailableClass(column) === 'o_grid_unavailable' && row.cells[column.id].value === 0,\n        }\n    }\n\n    getSectionCellsClasses(column, row) {\n        return {\n            'text-opacity-25' : row.cells[column.id].value === 0 || this.getUnavailableClass(column) === 'o_grid_unavailable',\n        };\n    }\n\n    isTextDanger() {\n        return false;\n    }\n\n    getTextColorClasses(column, row, isEven) {\n        const value = row.cells[column.id].value;\n        const isTextDanger = this.isTextDanger(row, column);\n        return {\n            'text-bg-view': isEven && value >= 0 && !isTextDanger,\n            'text-900': !isEven && value >= 0 && !isTextDanger,\n            'text-danger': value < 0 || isTextDanger,\n        }\n    }\n\n    getCellsClasses(column, row, section, isEven) {\n        return {\n            ...this.getTextColorClasses(column, row, isEven),\n            'o_grid_cell_today': column.isToday,\n            'fst-italic': row.isAdditionalRow,\n        };\n    }\n\n    _getSectionTotalCellBgColor(section) {\n        return 'text-bg-800';\n    }\n\n    getSectionTotalRowClass(section, grandTotal) {\n        return {\n            [this._getSectionTotalCellBgColor(section)]: true,\n            'text-opacity-25': grandTotal === 0,\n        };\n    }\n\n    getColumnBarChartHeightStyle(column) {\n        let heightPercentage = 0;\n        if (this.props.model.maxColumnsTotal !== 0) {\n            heightPercentage = (column.grandTotal / this.props.model.maxColumnsTotal) * 100;\n        }\n        return `height: ${heightPercentage}%; bottom: 0;`;\n    }\n\n    getFooterTotalCellClasses(grandTotal) {\n        if (grandTotal < 0) {\n            return \"bg-danger text-bg-danger\";\n        }\n\n        return \"bg-400\";\n    }\n\n    getUnavailableClass(column, section = undefined) {\n        return \"\";\n    }\n\n    getFieldAdditionalProps(fieldName) {\n        return {\n            name: fieldName,\n            type: this.props.widgetPerFieldName[fieldName] || this.props.model.fieldsInfo[fieldName].type,\n        };\n    }\n\n    onCreateInlineClick(section) {\n        const context = {\n            ...(section?.context || {}),\n        };\n        const title = _t(\"Add a Line\");\n        this.props.createRecord({ context, title });\n    }\n\n    _focusOnToday() {\n        if (!this.shouldFocusOnToday) {\n            return;\n        }\n        this.shouldFocusOnToday = false;\n        const { navigationInfo, columnFieldIsDate } = this.props.model;\n        if (this.isMobile || !columnFieldIsDate || navigationInfo.range.name != \"month\"){\n            return;\n        }\n        const rendererEl = this.rendererRef.el;\n        const todayEl = rendererEl.querySelector(\"div.o_grid_column_title.fw-bolder\");\n        if (todayEl) {\n            rendererEl.parentElement.scrollLeft = todayEl.offsetLeft - rendererEl.offsetWidth / 2 + todayEl.offsetWidth / 2;\n        }\n    }\n\n    _onMouseOver(ev) {\n        if (this.hoveredElement || ev.fromElement?.classList.contains(\"dropdown-item\")) {\n            // As mouseout is call prior to mouseover, if hoveredElement is set this means\n            // that we haven't left it. So it's a mouseover inside it.\n            return;\n        }\n        const highlightableElement = ev.target.closest(\".o_grid_highlightable\");\n        if (!highlightableElement) {\n            // We are not in an element that should trigger a highlight.\n            return;\n        }\n        const { column, gridRow, gridColumn, row } = highlightableElement.dataset;\n        const isCellInColumnTotalHighlighted =\n            highlightableElement.classList.contains(\"o_grid_row_total\");\n        const elementsToHighlight = this.rendererRef.el.querySelectorAll(\n            `.o_grid_highlightable[data-grid-row=\"${gridRow}\"]:not(.o_grid_add_line):not(.o_grid_column_title), .o_grid_highlightable[data-grid-column=\"${gridColumn}\"]:not(.o_grid_row_timer):not(.o_grid_section_title):not(.o_grid_row_title${\n                isCellInColumnTotalHighlighted ? \",.o_grid_row_total\" : \"\"\n            })`\n        );\n        for (const node of elementsToHighlight) {\n            if (node.classList.contains(\"o_grid_bar_chart_container\")) {\n                node.classList.add(\"o_grid_highlighted\");\n            }\n            if (node.dataset.gridRow === gridRow) {\n                node.classList.add(\"o_grid_highlighted\");\n                if (node.dataset.gridColumn === gridColumn) {\n                    node.classList.add(\"o_grid_cell_highlighted\");\n                } else {\n                    node.classList.add(\"o_grid_row_highlighted\");\n                }\n            }\n        }\n        this.hoveredElement = highlightableElement;\n        const cell = this.editCellProps.reactive.cell;\n        if (\n            row &&\n            column &&\n            !(cell && cell.dataset.row === row && cell.dataset.column === column)\n        ) {\n            this.hoveredCellProps.reactive.cell = highlightableElement;\n        }\n    }\n\n    /**\n     * Mouse out handler\n     *\n     * @param {MouseEvent} ev\n     */\n    _onMouseOut(ev) {\n        if (!this.hoveredElement) {\n            // If hoveredElement is not set this means were not in a o_grid_highlightable. So ignore it.\n            return;\n        }\n        /** @type {HTMLElement | null} */\n        let relatedTarget = ev.relatedTarget;\n        const gridCell = relatedTarget?.closest(\".o_grid_cell\");\n        if (\n            gridCell &&\n            gridCell.dataset.gridRow === this.hoveredElement.dataset.gridRow &&\n            gridCell.dataset.gridColumn === this.hoveredElement.dataset.gridColumn &&\n            gridCell !== this.editCellProps.reactive.cell\n        ) {\n            return;\n        }\n        while (relatedTarget) {\n            // Go up the parent chain\n            if (relatedTarget === this.hoveredElement) {\n                // Check that we are still inside hoveredConnector.\n                // If so it means it is a transition between child elements so ignore it.\n                return;\n            }\n            relatedTarget = relatedTarget.parentElement;\n        }\n        const { gridRow, gridColumn } = this.hoveredElement.dataset;\n        const elementsHighlighted = this.rendererRef.el.querySelectorAll(\n            `.o_grid_highlightable[data-grid-row=\"${gridRow}\"], .o_grid_highlightable[data-grid-column=\"${gridColumn}\"]`\n        );\n        for (const node of elementsHighlighted) {\n            node.classList.remove(\n                \"o_grid_highlighted\",\n                \"o_grid_row_highlighted\",\n                \"o_grid_cell_highlighted\"\n            );\n        }\n        this.hoveredElement = null;\n        if (this.hoveredCellProps.reactive.cell) {\n            this.hoveredCellProps.reactive.cell\n                .querySelector(\".o_grid_cell_readonly\")\n                .classList.remove(\"d-none\");\n            this.hoveredCellProps.reactive.cell = null;\n        }\n    }\n\n    onEditCell(value) {\n        if (this.editCellProps.reactive.cell) {\n            this.editCellProps.reactive.cell\n                .querySelector(\".o_grid_cell_readonly\")\n                .classList.remove(\"d-none\");\n        }\n        if (value) {\n            this.editCellProps.reactive.cell = this.hoveredCellProps.reactive.cell;\n            this.hoveredCellProps.reactive.cell = null;\n        } else {\n            this.editCellProps.reactive.cell = null;\n        }\n    }\n\n    _onKeyDown(ev) {\n        const hotkey = getActiveHotkey(ev);\n        if (hotkey === \"escape\" && this.editCellProps.reactive.cell) {\n            this.onEditCell(false);\n        }\n    }\n\n    /**\n     * Handle click on any element in the grid\n     *\n     * @param {MouseEvent} ev\n     */\n    onClick(ev) {\n        if (\n            !this.editCellProps.reactive.cell ||\n            ev.target.closest(\".o_grid_highlighted\") ||\n            ev.target.closest(\".o_grid_cell\")\n        ) {\n            return;\n        }\n        this.onEditCell(false);\n    }\n\n    onKeyDown(ev) {\n        this._onKeyDown(ev);\n    }\n\n    /**\n     * Handle the click on a cell in mobile\n     *\n     * @param {MouseEvent} ev\n     */\n    onCellClick(ev) {\n        ev.stopPropagation();\n        const cell = ev.target.closest(\".o_grid_highlightable\");\n        const { row, column } = cell.dataset;\n        if (row && column) {\n            if (this.editCellProps.reactive.cell) {\n                this.editCellProps.reactive.cell\n                    .querySelector(\".o_grid_cell_readonly\")\n                    .classList.remove(\"d-none\");\n            }\n            this.editCellProps.reactive.cell = cell;\n        }\n    }\n\n    /**\n     * Handle keydown when cell is edited in the grid view.\n     *\n     * @param {KeyboardEvent} ev\n     * @param {import(\"./grid_model\").GridCell | null} cell\n     */\n    onCellKeydown(ev, cell) {\n        const hotkey = getActiveHotkey(ev);\n        if (!this.rendererRef.el || !cell || ![\"tab\", \"shift+tab\", \"enter\"].includes(hotkey)) {\n            this._onKeyDown(ev);\n            return;\n        }\n        // Purpose: prevent browser defaults\n        ev.preventDefault();\n        // Purpose: stop other window keydown listeners (e.g. home menu)\n        ev.stopImmediatePropagation();\n        let rowId = cell.row.id;\n        let columnId = cell.column.id;\n        const columnIds = this.props.columns.map((c) => c.id);\n        const rowIds = [];\n        for (const item of this.props.rows) {\n            if (!item.isSection) {\n                rowIds.push(item.id);\n            }\n        }\n        let columnIndex = columnIds.indexOf(columnId);\n        let rowIndex = rowIds.indexOf(rowId);\n        if (hotkey === \"tab\") {\n            columnIndex += 1;\n            rowIndex += 1;\n            if (columnIndex < columnIds.length) {\n                columnId = columnIds[columnIndex];\n            } else {\n                columnId = columnIds[0];\n                if (rowIndex < rowIds.length) {\n                    rowId = rowIds[rowIndex];\n                } else {\n                    rowId = rowIds[0];\n                }\n            }\n        } else if (hotkey === \"shift+tab\") {\n            columnIndex -= 1;\n            rowIndex -= 1;\n            if (columnIndex >= 0) {\n                columnId = columnIds[columnIndex];\n            } else {\n                columnId = columnIds[columnIds.length - 1];\n                if (rowIndex >= 0) {\n                    rowId = rowIds[rowIndex];\n                } else {\n                    rowId = rowIds[rowIds.length - 1];\n                }\n            }\n        } else if (hotkey === \"enter\") {\n            rowIndex += 1;\n            if (rowIndex >= rowIds.length) {\n                columnIndex = (columnIndex + 1) % columnIds.length;\n                columnId = columnIds[columnIndex];\n            }\n            rowIndex = rowIndex % rowIds.length;\n            rowId = rowIds[rowIndex];\n        }\n        this.onEditCell(false);\n        this.hoveredCellProps.reactive.cell = this.rendererRef.el.querySelector(\n            `.o_grid_highlightable[data-row=\"${rowId}\"][data-column=\"${columnId}\"]`\n        );\n        this.onEditCell(true);\n    }\n\n    async openRecords(actionTitle, domain, context) {\n        const resModel = this.props.model.resModel;\n        if (this.props.openAction) {\n            const resIds = await this.props.model.orm.search(resModel, domain);\n            this.actionService.doActionButton({\n                ...this.props.openAction,\n                resModel,\n                resIds,\n                context,\n            });\n        } else {\n            const noActivitiesFound = _t(\"No activities found\");\n            // retrieve form and list view ids from the action\n            const { views = [] } = this.env.config;\n            const openRecordsViews = [\"list\", \"form\"].map((viewType) => {\n                const view = views.find((view) => view[1] === viewType);\n                return [view ? view[0] : false, viewType];\n            });\n            this.actionService.doAction({\n                type: \"ir.actions.act_window\",\n                name: actionTitle,\n                res_model: resModel,\n                views: openRecordsViews,\n                domain,\n                context,\n                help: markup(\n                    `<p class='o_view_nocontent_smiling_face'>${escape(noActivitiesFound)}</p>`\n                ),\n            });\n        }\n    }\n\n    onMagnifierGlassClick(section, column) {\n        const title = `${section.title} (${column.title})`;\n        const domain = Domain.and([section.domain, column.domain]).toList();\n        this.openRecords(title, domain, section.context);\n    }\n\n    get rangesArray() {\n        return Object.values(this.props.ranges);\n    }\n\n    async onRangeClick(name) {\n        await this.props.model.setRange(name);\n        this.props.state.activeRangeName = name;\n        this.shouldFocusOnToday = true;\n    }\n\n    async onTodayButtonClick() {\n        await this.props.model.setTodayAnchor();\n        this.shouldFocusOnToday = true;\n    }\n\n    async onPreviousButtonClick() {\n        await this.props.model.moveAnchor(\"backward\");\n        this.shouldFocusOnToday = true;\n    }\n\n    async onNextButtonClick() {\n        await this.props.model.moveAnchor(\"forward\");\n        this.shouldFocusOnToday = true;\n    }\n}\n", "import { registry } from \"@web/core/registry\";\nimport { GridArchParser } from \"@web_grid/views/grid_arch_parser\";\nimport { GridController } from \"@web_grid/views/grid_controller\";\nimport { GridModel } from \"@web_grid/views/grid_model\";\nimport { GridRenderer } from \"@web_grid/views/grid_renderer\";\n\nexport const gridView = {\n    type: \"grid\",\n    ArchParser: GridArchParser,\n    Controller: GridController,\n    Model: GridModel,\n    Renderer: GridRenderer,\n    buttonTemplate: \"web_grid.Buttons\",\n\n    props: (genericProps, view) => {\n        const { ArchParser, Model, Renderer, buttonTemplate: viewButtonTemplate } = view;\n        const { arch, relatedModels, resModel, buttonTemplate } = genericProps;\n        return {\n            ...genericProps,\n            archInfo: new ArchParser().parse(arch, relatedModels, resModel),\n            buttonTemplate: buttonTemplate || viewButtonTemplate,\n            Model,\n            Renderer,\n        };\n    }\n};\n\nregistry.category('views').add('grid', gridView);\n", "/** @odoo-module */\n\nimport { serializeDate, deserializeDate } from \"@web/core/l10n/dates\";\nimport { GridNavigationInfo, GridModel, GridDataPoint } from \"@web_grid/views/grid_model\";\n\nexport class AnalyticLineGridDataPoint extends GridDataPoint {\n    async _initialiseData() {\n        if (this.navigationInfo.range.span === \"year\") {\n            await this.navigationInfo.fetchPeriod();\n        }\n        await super._initialiseData();\n    }\n}\n\nexport class AnalyticLineGridNavigationInfo extends GridNavigationInfo {\n    get periodStart() {\n        if (this.range.span !== \"year\" || !this._periodStart) {\n            return super.periodStart;\n        }\n        return this._periodStart;\n    }\n\n    get periodEnd() {\n        if (this.range.span !== \"year\" || !this._periodEnd) {\n            return super.periodEnd;\n        }\n        return this._periodEnd;\n    }\n\n    async fetchPeriod() {\n        const { date_from, date_to } = await this.model.orm.call(\n            this.model.resModel,\n            \"grid_compute_year_range\",\n            [serializeDate(this.anchor)]\n        );\n        this._periodStart = deserializeDate(date_from);\n        this._periodEnd = deserializeDate(date_to);\n    }\n}\n\nexport class AnalyticLineGridModel extends GridModel {\n    static DataPoint = AnalyticLineGridDataPoint;\n    static NavigationInfo = AnalyticLineGridNavigationInfo;\n}\n", "/** @odoo-module */\n\nimport { registry } from \"@web/core/registry\";\n\nimport { gridView } from \"@web_grid/views/grid_view\";\n\nimport { AnalyticLineGridModel } from \"./analytic_line_grid_model\";\n\nexport const analyticLineGridView = {\n    ...gridView,\n    Model: AnalyticLineGridModel,\n}\n\nregistry.category(\"views\").add(\"analytic_line_grid\", analyticLineGridView)\n", "import { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\nimport { AvatarCardEmployeePopover } from \"@hr/components/avatar_card_employee/avatar_card_employee_popover\";\nimport { usePopover } from \"@web/core/popover/popover_hook\";\n\n\nexport class GanttEmployeeAvatar extends Avatar {\n    static template = \"hr.GanttEmployeeAvatar\";\n\n    setup() {\n        super.setup();\n        this.avatarCard = usePopover(AvatarCardEmployeePopover);\n    }\n\n    openCard(ev) {\n        if (this.env.isSmall || !this.props.resId) {\n            return;\n        }\n        const target = ev.currentTarget;\n        if (!this.avatarCard.isOpen) {\n            this.avatarCard.open(target, {\n                id: this.props.resId,\n            });\n        }\n    }\n}\n", "import { GanttEmployeeAvatar } from \"./hr_gantt_employee_avatar\";\nimport { GanttRenderer } from \"@web_gantt/gantt_renderer\";\n\nconst { DateTime } = luxon;\n\nexport class HrGanttRenderer extends GanttRenderer {\n    static rowHeaderTemplate = \"hr.HrGanttRenderer.RowHeader\";\n    static components = { ...GanttRenderer.components, Avatar: GanttEmployeeAvatar };\n    computeDerivedParams() {\n        this.rowsWithAvatar = {};\n        super.computeDerivedParams();\n    }\n\n    getAvatarProps(row) {\n        return this.rowsWithAvatar[row.id];\n    }\n\n    hasAvatar(row) {\n        return row.id in this.rowsWithAvatar;\n    }\n\n    processRow(row) {\n        const { groupedByField, name, resId } = row;\n        if (groupedByField === \"employee_id\" && Boolean(resId)) {\n            const { fields } = this.model.metaData;\n            const relation = fields.employee_id.relation;\n            const resModel = relation === 'hr.employee' ? 'hr.employee.public' : relation;\n            this.rowsWithAvatar[row.id] = { resModel, resId, displayName: name };\n        }\n        return super.processRow(...arguments);\n    }\n\n    /**\n     * Override to factor in lunch brakes between 11:00 to 14:00.\n     * Stops morning or afternoon shift pills from spanning an entire day.\n     *\n     * @param {RelationalRecord} record\n     * @returns {Partial<Pill>}\n     */\n    getPill(record) {\n        const pill = super.getPill(record);\n        const { unit } = this.model.metaData.scale;\n        const [startIndex, endIndex] = pill.grid.column;\n        // only check pills that span 2 half-days for in week or month views\n        if ([\"week\", \"month\"].includes(unit) && (endIndex - startIndex === 2)) {\n            const {\n                dateStartField,\n                dateStopField,\n                globalStart,\n                globalStop,\n            } = this.model.metaData;\n            const start = DateTime.max(globalStart, record[dateStartField]);\n            const stop = DateTime.min(globalStop, record[dateStopField]);\n            if (start.day === stop.day) {\n                const startTime = start.hour + (start.minute / 60);\n                const stopTime = stop.hour + (stop.minute / 60);\n                // we can assume startTime < 12:00 and stopTime > 12:00\n                const closestToNoon = 12 - startTime < stopTime - 12 ? startTime : stopTime;\n                if (startTime >= 11 && startTime === closestToNoon) {\n                    // most of pill is placed in afternoon, so round off first half\n                    pill.grid.column = [startIndex + 1, endIndex];\n                } else if (stopTime <= 14) {\n                    // start time is before 11:00 or most of pill is before noon\n                    // so round off second half\n                    pill.grid.column = [startIndex, endIndex - 1];\n                }\n            }\n        }\n        return pill;\n    }\n}\n", "import { ganttView } from \"@web_gantt/gantt_view\";\nimport { HrGanttRenderer } from \"./hr_gantt_renderer\";\nimport { registry } from \"@web/core/registry\";\n\nconst viewRegistry = registry.category(\"views\");\n\nexport const hrGanttView = {\n    ...ganttView,\n    Renderer: HrGanttRenderer,\n};\n\nviewRegistry.add(\"hr_gantt\", hrGanttView);\n", "/** @odoo-module */\n\nimport { HierarchyCard } from \"@web_hierarchy/hierarchy_card\";\n\nexport class HrEmployeeHierarchyCard extends HierarchyCard {\n    static template = \"hr_org_chart.HrEmployeeHierarchyCard\";\n}\n", "/** @odoo-module **/\n\nimport { Avatar } from \"@mail/views/web/fields/avatar/avatar\";\n\nimport { HierarchyRenderer } from \"@web_hierarchy/hierarchy_renderer\";\nimport { HrEmployeeHierarchyCard } from \"./hr_employee_hierarchy_card\";\n\nexport class HrEmployeeHierarchyRenderer extends HierarchyRenderer {\n    static template = \"hr_org_chart.HrEmployeeHierarchyRenderer\";\n    static components = {\n        ...HierarchyRenderer.components,\n        HierarchyCard: HrEmployeeHierarchyCard,\n        Avatar,\n    };\n}\n", "/** @odoo-module **/\n\nimport { registry } from \"@web/core/registry\";\nimport { hierarchyView } from \"@web_hierarchy/hierarchy_view\";\nimport { HrEmployeeHierarchyRenderer } from \"./hr_employee_hierarchy_renderer\";\nimport { HierarchyController } from \"@web_hierarchy/hierarchy_controller\";\nimport { HrActionHelper } from \"@hr/views/hr_action_helper\";\n\nexport class HrEmployeeHierarchyController extends HierarchyController {\n    static template = \"hr_org_chart.HierarchyView\";\n    static components = { ...HierarchyController.components, HrActionHelper };\n}\n\nexport const hrEmployeeHierarchyView = {\n    ...hierarchyView,\n    Controller: HrEmployeeHierarchyController,\n    Renderer: HrEmployeeHierarchyRenderer,\n};\n\nregistry.category(\"views\").add(\"hr_employee_hierarchy\", hrEmployeeHierarchyView);\n", "import { GraphRenderer } from \"@web/views/graph/graph_renderer\";\nimport { user } from \"@web/core/user\";\nimport { session } from \"@web/session\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\nimport { omit } from \"@web/core/utils/objects\";\n\nexport const patchGraphSpreadsheet = () => ({\n    setup() {\n        super.setup(...arguments);\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.menu = useService(\"menu\");\n        this.canInsertChart = session.can_insert_in_spreadsheet;\n    },\n\n    async onInsertInSpreadsheet() {\n        const { actionId } = this.env.config;\n        const { xml_id } = actionId ? await this.actionService.loadAction(actionId) : {};\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertChart\",\n            preProcessingAsyncActionData: {\n                metaData: this.model.metaData,\n                searchParams: {\n                    ...this.model.searchParams,\n                    domain: this.env.searchModel.domainString,\n                    context: omit(\n                        this.model.searchParams.context,\n                        ...Object.keys(user.context),\n                        \"graph_measure\",\n                        \"graph_order\"\n                    ),\n                },\n                actionXmlId: xml_id,\n            },\n        };\n        const params = {\n            type: \"GRAPH\",\n            name: this.model.metaData.title,\n            actionOptions,\n        };\n        this.env.services.dialog.add(SpreadsheetSelectorDialog, params);\n    },\n});\n\n/**\n * This patch is a little trick, which require a little explanation:\n *\n * In this patch, we add some dependencies to the graph view (menu service,\n * router service, ...).\n * To test it, we add these dependencies in our tests, but these dependencies\n * are not added in the tests of the base graph view (in web/). The same thing\n * occurs for the button \"Insert in spreadsheet\".\n * As we do not want to modify tests in web/ in order to integrate a behavior\n * defined in another module, we disable this patch in a file that is only\n * loaded in test assets (disable_patch.js), and re-active it in our tests.\n */\nexport const unpatchGraphSpreadsheet = patch(GraphRenderer.prototype, patchGraphSpreadsheet());\n", "import { PivotRenderer } from \"@web/views/pivot/pivot_renderer\";\nimport { user } from \"@web/core/user\";\nimport { intersection, unique } from \"@web/core/utils/arrays\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { omit } from \"@web/core/utils/objects\";\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { SpreadsheetSelectorDialog } from \"@spreadsheet_edition/assets/components/spreadsheet_selector_dialog/spreadsheet_selector_dialog\";\n\nimport { session } from \"@web/session\";\n\n/**\n * This const is defined in o-spreadsheet library, but has to be redefined here\n * because o-spreadsheet is lazy loaded in another bundle than this file is.\n */\nconst ALL_PERIODS = {\n    quarter: _t(\"Quarter & Year\"),\n    month: _t(\"Month & Year\"),\n    week: _t(\"Week & Year\"),\n    day: _t(\"Day\"),\n    year: _t(\"Year\"),\n    quarter_number: _t(\"Quarter\"),\n    month_number: _t(\"Month\"),\n    iso_week_number: _t(\"Week\"),\n    day_of_month: _t(\"Day of Month\"),\n};\n\npatch(PivotRenderer.prototype, {\n    setup() {\n        super.setup(...arguments);\n        this.notification = useService(\"notification\");\n        this.actionService = useService(\"action\");\n        this.canInsertPivot = session.can_insert_in_spreadsheet;\n    },\n\n    async onInsertInSpreadsheet() {\n        let name = this.model.metaData.title;\n        const groupBy =\n            this.model.metaData.fullColGroupBys[0] || this.model.metaData.fullRowGroupBys[0];\n        if (groupBy) {\n            let [field, period] = groupBy.split(\":\");\n            period = ALL_PERIODS[period];\n            if (period) {\n                name = _t(\"%(pivot_title)s by %(group_by)s (%(granularity)s)\", {\n                    pivot_title: name,\n                    group_by: this.model.metaData.fields[field].string,\n                    granularity: period,\n                });\n            } else {\n                name = _t(\"%(pivot_title)s by %(group_by)s\", {\n                    pivot_title: name,\n                    group_by: this.model.metaData.fields[field].string,\n                });\n            }\n        }\n        const { actionId } = this.env.config;\n        const { xml_id } = actionId\n            ? await this.actionService.loadAction(actionId, this.env.searchModel.context)\n            : {};\n\n        const actionOptions = {\n            preProcessingAsyncAction: \"insertPivot\",\n            preProcessingAsyncActionData: {\n                metaData: this.model.metaData,\n                searchParams: {\n                    ...this.model.searchParams,\n                    domain: this.env.searchModel.domainString,\n                    context: omit(\n                        this.model.searchParams.context,\n                        ...Object.keys(user.context),\n                        \"pivot_measures\",\n                        \"pivot_row_groupby\",\n                        \"pivot_column_groupby\"\n                    ),\n                },\n                name,\n                actionXmlId: xml_id,\n            },\n        };\n        const params = {\n            type: \"PIVOT\",\n            name,\n            actionOptions,\n        };\n        this.env.services.dialog.add(SpreadsheetSelectorDialog, params);\n    },\n\n    hasDuplicatedGroupbys() {\n        const fullColGroupBys = this.model.metaData.fullColGroupBys;\n        const fullRowGroupBys = this.model.metaData.fullRowGroupBys;\n        // without aggregator\n        const colGroupBys = fullColGroupBys.map((el) => el.split(\":\")[0]);\n        const rowGroupBys = fullRowGroupBys.map((el) => el.split(\":\")[0]);\n        return (\n            unique([...fullColGroupBys, ...fullRowGroupBys]).length <\n                fullColGroupBys.length + fullRowGroupBys.length ||\n            // can group by the same field with different aggregator in the same dimension\n            intersection(colGroupBys, rowGroupBys).length\n        );\n    },\n\n    isInsertButtonDisabled() {\n        return (\n            !this.model.hasData() ||\n            this.model.metaData.activeMeasures.length === 0 ||\n            this.model.useSampleModel ||\n            this.hasDuplicatedGroupbys()\n        );\n    },\n\n    getInsertButtonTooltip() {\n        return this.hasDuplicatedGroupbys() ? _t(\"Pivot contains duplicate groupbys\") : undefined;\n    },\n});\n", "import { MapModel } from \"@web_map/map_view/map_model\";\n\nexport class StockMapModel extends MapModel {\n    _getRecordSpecification(metaData, data) {\n        return {\n            ...super._getRecordSpecification(metaData, data),\n            warehouse_address_id: {\n                fields: {\n                    display_name: {},\n                    contact_address_complete: {},\n                }\n            }\n        }\n    }\n}", "import { MapRenderer } from \"@web_map/map_view/map_renderer\";\n\nexport class StockMapRenderer extends MapRenderer {\n    get googleMapUrl() {\n        let url = super.googleMapUrl;\n        const warehouseAddress = this.props.model.data.records[0].warehouse_address_id;\n        let multiAddresses = false;\n        for (const record of this.props.model.data.records) {\n            if (record.warehouse_address_id.id !== warehouseAddress.id) {\n                multiAddresses = true;\n                break;\n            }\n        }\n        if (multiAddresses) {\n            return url;\n        }\n        url += `&origin=${warehouseAddress.contact_address_complete}`;\n        url += `&destination=${warehouseAddress.contact_address_complete}`;\n        return url;\n    }\n}", "import { registry } from \"@web/core/registry\";\nimport { mapView } from \"@web_map/map_view/map_view\";\nimport { StockMapModel } from \"./map_model\";\nimport { StockMapRenderer } from \"./map_renderer\";\n\nexport const stockMapView = {\n    ...mapView,\n    Model: StockMapModel,\n    Renderer: StockMapRenderer,\n};\n\nregistry.category(\"views\").add(\"stock_map\", stockMapView);"], "file": "/web/assets/a2d1a35/web.assets_backend_lazy.js", "sourceRoot": "../../../"}