From f5438289bdaa5b3fb6940fc0268c046dbf448efb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 2 Nov 2018 12:47:26 +0000 Subject: [PATCH] [CHG] sale_timesheet: convert project overview to qweb view Note: conversion was performed with as little code changes as feasible, mostly moving code from the controller to the model & inlining some parameters --- addons/sale_timesheet/__init__.py | 1 - addons/sale_timesheet/__manifest__.py | 3 - addons/sale_timesheet/controllers/__init__.py | 4 - addons/sale_timesheet/models/__init__.py | 1 + .../main.py => models/project_overview.py} | 250 +++++----- .../static/src/js/timesheet_plan.js | 271 ---------- .../static/src/xml/timesheet_plan.xml | 15 - .../static/tests/timesheet_plan_tests.js | 94 ---- .../views/hr_timesheet_templates.xml | 462 +++++++++--------- .../views/project_task_views.xml | 5 +- 10 files changed, 351 insertions(+), 755 deletions(-) delete mode 100644 addons/sale_timesheet/controllers/__init__.py rename addons/sale_timesheet/{controllers/main.py => models/project_overview.py} (66%) delete mode 100644 addons/sale_timesheet/static/src/js/timesheet_plan.js delete mode 100644 addons/sale_timesheet/static/src/xml/timesheet_plan.xml delete mode 100644 addons/sale_timesheet/static/tests/timesheet_plan_tests.js diff --git a/addons/sale_timesheet/__init__.py b/addons/sale_timesheet/__init__.py index eca4161450d..845bbae3709 100644 --- a/addons/sale_timesheet/__init__.py +++ b/addons/sale_timesheet/__init__.py @@ -5,7 +5,6 @@ from odoo import api, SUPERUSER_ID from . import models from . import wizard -from . import controllers from . import report def uninstall_hook(cr, registry): diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py index 03d26f2b3fb..beb318b3ad9 100644 --- a/addons/sale_timesheet/__manifest__.py +++ b/addons/sale_timesheet/__manifest__.py @@ -33,9 +33,6 @@ have real delivered quantities in sales orders. 'demo': [ 'data/sale_service_demo.xml', ], - 'qweb': [ - 'static/src/xml/timesheet_plan.xml', - ], 'auto_install': True, 'uninstall_hook': 'uninstall_hook', } diff --git a/addons/sale_timesheet/controllers/__init__.py b/addons/sale_timesheet/controllers/__init__.py deleted file mode 100644 index 5d4b25db9c0..00000000000 --- a/addons/sale_timesheet/controllers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import main diff --git a/addons/sale_timesheet/models/__init__.py b/addons/sale_timesheet/models/__init__.py index 60c83adf30d..7b8dab836ce 100644 --- a/addons/sale_timesheet/models/__init__.py +++ b/addons/sale_timesheet/models/__init__.py @@ -5,5 +5,6 @@ from . import account from . import account_invoice from . import product from . import project +from . import project_overview from . import sale_order from . import project_sale_line_employee_map diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/models/project_overview.py similarity index 66% rename from addons/sale_timesheet/controllers/main.py rename to addons/sale_timesheet/models/project_overview.py index 48596805796..86a70fd30d2 100644 --- a/addons/sale_timesheet/controllers/main.py +++ b/addons/sale_timesheet/models/project_overview.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from ast import literal_eval import babel from dateutil.relativedelta import relativedelta import itertools import json -from odoo import http, fields, _ -from odoo.http import request +from odoo import fields, _, models from odoo.tools import float_round from odoo.addons.web.controllers.main import clean_action @@ -15,34 +12,30 @@ from odoo.addons.web.controllers.main import clean_action DEFAULT_MONTH_RANGE = 3 -class SaleTimesheetController(http.Controller): +class Project(models.Model): + _inherit = 'project.project' - @http.route('/timesheet/plan', type='json', auth="user") - def plan(self, domain): - """ Get the HTML of the project plan for projects matching the given domain - :param domain: a domain for project.project - """ - projects = request.env['project.project'].search(domain) - values = self._plan_prepare_values(projects) - view = request.env.ref('sale_timesheet.timesheet_plan') - return { - 'html_content': view.render(values), - 'project_ids': projects.ids, - 'actions': self._plan_prepare_actions(projects, values), - } - def _plan_prepare_values(self, projects): + def _qweb_prepare_qcontext(self, view_id, domain): + values = super()._qweb_prepare_qcontext(view_id, domain) - currency = request.env.user.company_id.currency_id - uom_hour = request.env.ref('uom.product_uom_hour') + projects = self.search(domain) + values.update(projects._plan_prepare_values()) + values['actions'] = projects._plan_prepare_actions(values) + + return values + + def _plan_prepare_values(self): + currency = self.env.user.company_id.currency_id + uom_hour = self.env.ref('uom.product_uom_hour') hour_rounding = uom_hour.rounding billable_types = ['non_billable', 'non_billable_project', 'billable_time', 'billable_fixed'] values = { - 'projects': projects, + 'projects': self, 'currency': currency, - 'timesheet_domain': [('project_id', 'in', projects.ids)], - 'stat_buttons': self._plan_get_stat_button(projects), + 'timesheet_domain': [('project_id', 'in', self.ids)], + 'stat_buttons': self._plan_get_stat_button(), } # @@ -60,8 +53,8 @@ class SaleTimesheetController(http.Controller): } # hours (from timesheet) and rates (by billable type) - dashboard_domain = [('project_id', 'in', projects.ids), ('timesheet_invoice_type', '!=', False)] # force billable type - dashboard_data = request.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type']) + dashboard_domain = [('project_id', 'in', self.ids), ('timesheet_invoice_type', '!=', False)] # force billable type + dashboard_data = self.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_invoice_type'], ['timesheet_invoice_type']) dashboard_total_hours = sum([data['unit_amount'] for data in dashboard_data]) for data in dashboard_data: billable_type = data['timesheet_invoice_type'] @@ -74,7 +67,7 @@ class SaleTimesheetController(http.Controller): # profitability, using profitability SQL report profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0) - profitability_raw_data = request.env['project.profitability.report'].read_group([('project_id', 'in', projects.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id']) + profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id']) for data in profitability_raw_data: profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0) profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0) @@ -89,14 +82,14 @@ class SaleTimesheetController(http.Controller): # # Time Repartition (per employee per billable types) # - user_ids = request.env['project.task'].sudo().search_read([('project_id', 'in', projects.ids), ('user_id', '!=', False)], ['user_id']) + user_ids = self.env['project.task'].sudo().search_read([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id']) user_ids = [user_id['user_id'][0] for user_id in user_ids] - employee_ids = request.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids']) + employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids']) # flatten the list of list employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids])) - employees = request.env['hr.employee'].sudo().browse(employee_ids) | request.env['account.analytic.line'].search([('project_id', 'in', projects.ids)]).mapped('employee_id') - repartition_domain = [('project_id', 'in', projects.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type - repartition_data = request.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False) + employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id') + repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type + repartition_data = self.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False) # set repartition per type per employee repartition_employee = {} @@ -134,26 +127,26 @@ class SaleTimesheetController(http.Controller): # # Table grouped by SO / SOL / Employees # - timesheet_forecast_table_rows = self._table_get_line_values(projects) + timesheet_forecast_table_rows = self._table_get_line_values() if timesheet_forecast_table_rows: values['timesheet_forecast_table'] = timesheet_forecast_table_rows return values - def _table_get_line_values(self, projects): + def _table_get_line_values(self): """ return the header and the rows informations of the table """ - if not projects: + if not self: return False - uom_hour = request.env.ref('uom.product_uom_hour') + uom_hour = self.env.ref('uom.product_uom_hour') # build SQL query and fetch raw data - query, query_params = self._table_rows_sql_query(projects) - request.env.cr.execute(query, query_params) - raw_data = request.env.cr.dictfetchall() - rows_employee = self._table_rows_get_employee_lines(projects, raw_data) - default_row_vals = self._table_row_default(projects) + query, query_params = self._table_rows_sql_query() + self.env.cr.execute(query, query_params) + raw_data = self.env.cr.dictfetchall() + rows_employee = self._table_rows_get_employee_lines(raw_data) + default_row_vals = self._table_row_default() - empty_line_ids, empty_order_ids = self._table_get_empty_so_lines(projects) + empty_line_ids, empty_order_ids = self._table_get_empty_so_lines() # extract row labels sale_line_ids = set() @@ -164,8 +157,8 @@ class SaleTimesheetController(http.Controller): if row[0]['sale_order_id']: sale_order_ids.add(row[0]['sale_order_id']) - sale_order_lines = request.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) - map_so_names = {so.id: so.name for so in request.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)} + sale_order_lines = self.env['sale.order.line'].sudo().browse(sale_line_ids | empty_line_ids) + map_so_names = {so.id: so.name for so in self.env['sale.order'].sudo().browse(sale_order_ids | empty_order_ids)} map_sol = {sol.id: sol for sol in sale_order_lines} map_sol_names = {sol.id: sol.name.split('\n')[0] if sol.name else _('No Sales Order Line') for sol in sale_order_lines} map_sol_so = {sol.id: sol.order_id.id for sol in sale_order_lines} @@ -185,7 +178,7 @@ class SaleTimesheetController(http.Controller): # sale line row sale_line_row_key = (sale_order_id, sale_line_id) if sale_line_row_key not in rows_sale_line: - sale_line = map_sol.get(sale_line_id, request.env['sale.order.line']) + sale_line = map_sol.get(sale_line_id, self.env['sale.order.line']) is_milestone = sale_line.product_id.invoice_policy == 'delivery' and sale_line.product_id.service_type == 'manual' if sale_line else False rows_sale_line[sale_line_row_key] = [{'label': map_sol_names.get(sale_line.id) if sale_line else _('No Sales Order Line'), 'res_id': sale_line_id, 'res_model': 'sale.order.line', 'type': 'sale_order_line', 'is_milestone': is_milestone}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted if not is_milestone: @@ -234,17 +227,16 @@ class SaleTimesheetController(http.Controller): # complete table data return { - 'header': self._table_header(projects), + 'header': self._table_header(), 'rows': timesheet_forecast_table_rows } - - def _table_header(self, projects): + def _table_header(self): initial_date = fields.Date.from_string(fields.Date.today()) ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 def _to_short_month_name(date): month_index = fields.Date.from_string(date).month - return babel.dates.get_month_names('abbreviated', locale=request.env.context.get('lang', 'en_US'))[month_index] + return babel.dates.get_month_names('abbreviated', locale=self.env.context.get('lang', 'en_US'))[month_index] header_names = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Done'), _('Sold'), _('Remaining')] @@ -258,11 +250,11 @@ class SaleTimesheetController(http.Controller): result[-1]['tooltip'] = _('What is still to deliver based on sold hours and hours already done. Equals to sold hours - done hours.') return result - def _table_row_default(self, projects): - lenght = len(self._table_header(projects)) + def _table_row_default(self): + lenght = len(self._table_header()) return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining - def _table_rows_sql_query(self, projects): + def _table_rows_sql_query(self): initial_date = fields.Date.from_string(fields.Date.today()) ts_months = sorted([fields.Date.to_string(initial_date - relativedelta(months=i, day=1)) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 # build query @@ -284,19 +276,19 @@ class SaleTimesheetController(http.Controller): """ last_ts_month = fields.Date.to_string(fields.Date.from_string(ts_months[-1]) + relativedelta(months=1)) - query_params = (tuple(projects.ids), last_ts_month) + query_params = (tuple(self.ids), last_ts_month) return query, query_params - def _table_rows_get_employee_lines(self, projects, data_from_db): + def _table_rows_get_employee_lines(self, data_from_db): initial_date = fields.Date.today() ts_months = sorted([initial_date - relativedelta(months=i, day=1) for i in range(0, DEFAULT_MONTH_RANGE)]) # M1, M2, M3 - default_row_vals = self._table_row_default(projects) + default_row_vals = self._table_row_default() # extract employee names employee_ids = set() for data in data_from_db: employee_ids.add(data['employee_id']) - map_empl_names = {empl.id: empl.name for empl in request.env['hr.employee'].sudo().browse(employee_ids)} + map_empl_names = {empl.id: empl.name for empl in self.env['hr.employee'].sudo().browse(employee_ids)} # extract rows data for employee, sol and so rows rows_employee = {} # (so, sol, employee) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted] @@ -326,29 +318,29 @@ class SaleTimesheetController(http.Controller): rows_employee[row_key][5] += data['number_hours'] return rows_employee - def _table_get_empty_so_lines(self, projects): + def _table_get_empty_so_lines(self): """ get the Sale Order Lines having no timesheet but having generated a task or a project """ - so_lines = projects.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense) + so_lines = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense) return set(so_lines.ids), set(so_lines.mapped('order_id').ids) # -------------------------------------------------- # Actions: Stat buttons, ... # -------------------------------------------------- - def _plan_prepare_actions(self, projects, values): + def _plan_prepare_actions(self, values): actions = [] - if len(projects) == 1: - if request.env.user.has_group('sales_team.group_sale_salesman'): - if not projects.sale_line_id and not projects.tasks.mapped('sale_line_id'): + if len(self) == 1: + if self.env.user.has_group('sales_team.group_sale_salesman'): + if not self.sale_line_id and not self.tasks.mapped('sale_line_id'): actions.append({ 'label': _("Create a Sales Order"), 'type': 'action', 'action_id': 'sale_timesheet.project_project_action_multi_create_sale_order', - 'context': json.dumps({'active_id': projects.id, 'active_model': 'project.project'}), + 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), }) - if request.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account - sale_orders = projects.tasks.mapped('sale_line_id.order_id').filtered(lambda so: so.invoice_status == 'to invoice') + sale_orders = self.tasks.mapped('sale_line_id.order_id').filtered(lambda so: so.invoice_status == 'to invoice') if to_invoice_amount and sale_orders: if len(sale_orders) == 1: actions.append({ @@ -362,102 +354,94 @@ class SaleTimesheetController(http.Controller): 'label': _("Create Invoice"), 'type': 'action', 'action_id': 'sale_timesheet.project_project_action_multi_create_invoice', - 'context': json.dumps({'active_id': projects.id, 'active_model': 'project.project'}), + 'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}), }) return actions - def _plan_get_stat_button(self, projects): + def _plan_get_stat_button(self): stat_buttons = [] - if len(projects) == 1: + if len(self) == 1: + edit_project = self.env.ref('project.edit_project') stat_buttons.append({ 'name': _('Project'), - 'res_model': 'project.project', - 'res_id': projects.id, 'icon': 'fa fa-puzzle-piece', + 'action': _to_action_data('project.project', res_id=self.id, + views=[[edit_project.id, 'form']]) }) + ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree') + ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form') stat_buttons.append({ 'name': _('Timesheets'), - 'res_model': 'account.analytic.line', - 'domain': [('project_id', 'in', projects.ids)], 'icon': 'fa fa-calendar', + 'action': _to_action_data( + 'account.analytic.line', + domain=[('project_id', 'in', self.ids)], + views=[(ts_tree.id, 'list'), (ts_form.id, 'form')], + ) }) + + # if only one project, add it in the context as default value + tasks_domain = [('project_id', 'in', self.ids)] + tasks_context = self.env.context + tasks_projects = self.env['project.task'].sudo().search(tasks_domain).mapped('project_id') + if len(tasks_projects) == 1: + tasks_context = {**tasks_context, 'default_project_id': tasks_projects.id} stat_buttons.append({ 'name': _('Tasks'), - 'count': sum(projects.mapped('task_count')), - 'res_model': 'project.task', - 'domain': [('project_id', 'in', projects.ids)], + 'count': sum(self.mapped('task_count')), 'icon': 'fa fa-tasks', + 'action': _to_action_data( + action=self.env.ref('project.action_view_task'), + domain=tasks_domain, + context=tasks_context + ) }) - if request.env.user.has_group('sales_team.group_sale_salesman_all_leads'): - sale_orders = projects.mapped('sale_line_id.order_id') | projects.mapped('tasks.sale_order_id') + + if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'): + sale_orders = self.mapped('sale_line_id.order_id') | self.mapped( + 'tasks.sale_order_id') if sale_orders: stat_buttons.append({ 'name': _('Sales Orders'), 'count': len(sale_orders), - 'res_model': 'sale.order', - 'domain': [('id', 'in', sale_orders.ids)], 'icon': 'fa fa-dollar', + 'action': _to_action_data( + action=self.env.ref('sale.action_orders'), + domain=[('id', 'in', sale_orders.ids)], + context={'create': False, 'edit': False, 'delete': False} + ) }) invoices = sale_orders.mapped('invoice_ids').filtered(lambda inv: inv.type == 'out_invoice') if invoices: stat_buttons.append({ 'name': _('Invoices'), 'count': len(invoices), - 'res_model': 'account.invoice', - 'domain': [('id', 'in', invoices.ids), ('type', '=', 'out_invoice')], 'icon': 'fa fa-pencil-square-o', + 'action': _to_action_data( + action=self.env.ref('account.action_invoice_tree1'), + domain=[('id', 'in', invoices.ids), ('type', '=', 'out_invoice')], + context={'create': False, 'delete': False} + ) }) return stat_buttons - @http.route('/timesheet/plan/action', type='json', auth="user") - def plan_stat_button(self, domain=[], res_model='account.analytic.line', res_id=False): - action = { - 'type': 'ir.actions.act_window', - 'view_id': False, - 'view_mode': 'tree,form', - 'view_type': 'list', - 'domain': domain, - } - if res_model == 'project.project': - view_form_id = request.env.ref('project.edit_project').id - action = { - 'name': _('Project'), - 'type': 'ir.actions.act_window', - 'res_model': res_model, - 'view_mode': 'form', - 'view_type': 'form', - 'views': [[view_form_id, 'form']], - 'res_id': res_id, - } - elif res_model == 'account.analytic.line': - ts_view_tree_id = request.env.ref('hr_timesheet.hr_timesheet_line_tree').id - ts_view_form_id = request.env.ref('hr_timesheet.hr_timesheet_line_form').id - action = { - 'name': _('Timesheets'), - 'type': 'ir.actions.act_window', - 'res_model': res_model, - 'view_mode': 'tree,form', - 'view_type': 'form', - 'views': [[ts_view_tree_id, 'list'], [ts_view_form_id, 'form']], - 'domain': domain, - } - elif res_model == 'project.task': - action = request.env.ref('project.action_view_task').read()[0] - action.update({ - 'name': _('Tasks'), - 'domain': domain, - 'context': dict(request.env.context), # erase original context to avoid default filter - }) - # if only one project, add it in the context as default value - tasks = request.env['project.task'].sudo().search(literal_eval(domain)) - if len(tasks.mapped('project_id')) == 1: - action['context']['default_project_id'] = tasks.mapped('project_id')[0].id - elif res_model == 'sale.order': - action = clean_action(request.env.ref('sale.action_orders').read()[0]) - action['domain'] = domain - action['context'] = {'create': False, 'edit': False, 'delete': False} # No CRUD operation when coming from overview - elif res_model == 'account.invoice': - action = clean_action(request.env.ref('account.action_invoice_tree1').read()[0]) - action['domain'] = domain - action['context'] = {'create': False, 'delete': False} # only edition of invoice from overview - return action + +def _to_action_data(model=None, *, action=None, views=None, res_id=None, domain=None, context=None): + # pass in either action or (model, views) + if action: + assert model is None and views is None + act = clean_action(action.read()[0]) + model = act['res_model'] + views = act['views'] + # FIXME: search-view-id, possibly help? + descr = { + 'data-model': model, + 'data-views': json.dumps(views), + } + if context is not None: # otherwise copy action's? + descr['data-context'] = json.dumps(context) + if res_id: + descr['data-res-id'] = res_id + elif domain: + descr['data-domain'] = json.dumps(domain) + return descr diff --git a/addons/sale_timesheet/static/src/js/timesheet_plan.js b/addons/sale_timesheet/static/src/js/timesheet_plan.js deleted file mode 100644 index 954ac594262..00000000000 --- a/addons/sale_timesheet/static/src/js/timesheet_plan.js +++ /dev/null @@ -1,271 +0,0 @@ -odoo.define('sale_timesheet.ProjectPlan', function (require) { -'use strict'; - -var AbstractAction = require('web.AbstractAction'); -var core = require('web.core'); - -var _t = core._t; -var QWeb = core.qweb; - -var ProjectPlan = AbstractAction.extend({ - custom_events: { - search: '_onSearch', - }, - events: { - "click a[type='action']": "_onClickAction", - "click .o_timesheet_plan_redirect": '_onRedirect', - "click .oe_stat_button": "_onClickStatButton", - "click .o_timesheet_plan_non_billable_task": "_onClickNonBillableTask", - "click .o_timesheet_plan_sale_timesheet_people_time .progress-bar": '_onClickEmployeeProgressbar', - }, - hasControlPanel: true, - loadControlPanel: true, - withSearchBar: true, - searchMenuTypes: ['filter', 'favorite'], - /** - * @override - */ - init: function (parent, action) { - this._super.apply(this, arguments); - this.action = action; - this.context = action.context; - this.action_manager = parent; - this._title = action.name || _t('Overview'); - this.project_ids = []; - - this.controlPanelParams.modelName = 'project.project'; - }, - /** - * @override - */ - start: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - // TODO: maybe not the correct way - return self._controlPanel._reportNewQueryAndRender(); - }); - }, - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - do_show: function () { - this._super.apply(this, arguments); - this._updateControlPanel(); - this.action_manager.do_push_state({ - action: this.action.id, - active_id: this.action.context.active_id, - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Refresh the DOM html - * - * @private - * @param {string|html} dom - */ - _refreshPlan: function (dom) { - this.$('.o_content').html(dom); - }, - - /** - * Call controller to get the html content - * - * @private - * @param {string[]} - * @returns {Deferred} - */ - _fetchPlan: function (domain) { - var self = this; - return this._rpc({ - route:"/timesheet/plan", - params: {domain: domain}, - }).then(function(result){ - self._refreshPlan(result.html_content); - self._updateControlPanel(result.actions); - self.project_ids = result.project_ids; - }); - }, - /** - * @private - */ - _updateControlPanel: function (buttons) { - // set actions button - if (this.$buttons) { - this.$buttons.off().remove(); - } - buttons = buttons || []; - this.$buttons = $(QWeb.render("project.plan.ControlButtons", {'buttons': buttons})); - this.$buttons.on('click', '.o_timesheet_plan_btn_action', this._onClickControlButton.bind(this)); - // refresh control panel - this.updateControlPanel({ - cp_content: { - $buttons: this.$buttons, - }, - }); - }, - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Generate the action to execute based on the clicked target - * - * @private - * @param {MouseEvent} event - */ - _onClickAction: function (ev) { - var $target = this.$(ev.currentTarget); - - var action = false; - if($target.attr('name')){ // classical case : name="action_id" type="action" - action = $target.attr('name'); - } else { // custom case : build custom action dict - action = { - 'name': _t('Timesheet'), - 'type': 'ir.actions.act_window', - 'target': 'current', - 'res_model': 'account.analytic.line', - }; - // find action views - var views = [[false, 'pivot'], [false, 'list']]; - if($target.attr('views')){ - views = JSON.parse($target.attr('views').replace(/\'/g, '"')); - } - action.views = views; - action.view_mode = _.map(views, function(view_array){return view_array[1];}); - // custom domain - var domain = []; - if($target.attr('domain')){ - domain = JSON.parse($target.attr('domain').replace(/\'/g, '"')); - } - action.domain = domain; - } - - // additionnal context - var context = { - active_id: this.action.context.active_id, - active_ids: this.action.context.active_ids, - active_model: this.action.context.active_model, - }; - - if($target.attr('context')){ - var ctx_str = $target.attr('context').replace(/\'/g, '"'); - context = _.extend(context, JSON.parse(ctx_str)); - } - - this.do_action(action, { - additional_context: context - }); - }, - /** - * Call the action of the action button from control panel, based on the data attribute on the button DOM - * - * @param {MouseEvent} event - * @private - */ - _onClickControlButton: function (ev) { - var $target = $(ev.target); - var action_id = $target.data('action-id'); - var context = $target.data('context'); - - return this.do_action(action_id, { - 'additional_context': context, - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickEmployeeProgressbar: function (event) { - var domain = $(event.currentTarget).data('domain'); - this.do_action({ - name: 'Timesheets', - type: 'ir.actions.act_window', - res_model: 'account.analytic.line', - views: [[false, 'list'], [false, 'form']], - view_type: 'list', - view_mode: 'form', - domain: domain || [], - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickStatButton: function (event) { - var self = this; - var data = $(event.currentTarget).data(); - var parameters = { - domain: data.domain || [], - res_model: data.resModel, - }; - if (data.resId) { - parameters.res_id = data.resId; - } - return this._rpc({ - route:"/timesheet/plan/action", - params: parameters, - }).then(function(action){ - self.do_action(action); - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onClickNonBillableTask: function () { - this.do_action({ - name: _t('Non Billable Tasks'), - type: 'ir.actions.act_window', - view_type: 'form', - view_mode: 'form', - res_model: 'project.task', - views: [[false, 'list'], [false, 'form']], - domain: [['project_id', 'in', this.project_ids || []], ['sale_line_id', '=', false]] - }); - }, - /** - * @private - * @param {MouseEvent} event - */ - _onRedirect: function (event) { - event.preventDefault(); - var $target = $(event.target); - this.do_action({ - type: 'ir.actions.act_window', - view_type: 'form', - view_mode: 'form', - res_model: $target.data('oe-model'), - views: [[false, 'form']], - res_id: $target.data('oe-id'), - }); - }, - /** - * This client action has its own search view and already listen on it. If - * we let the event pass through, it will be caught by the action manager - * which will do its work. This may crash the web client, if the action - * manager tries to notify the previous action. - * - * @private - * @param {OdooEvent} event - */ - _onSearch: function (event) { - event.stopPropagation(); - this._fetchPlan(event.data.domain); - }, -}); - -core.action_registry.add('timesheet.plan', ProjectPlan); - -return ProjectPlan; -}); diff --git a/addons/sale_timesheet/static/src/xml/timesheet_plan.xml b/addons/sale_timesheet/static/src/xml/timesheet_plan.xml deleted file mode 100644 index b127f433735..00000000000 --- a/addons/sale_timesheet/static/src/xml/timesheet_plan.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - -
- - - - -
-
- -
diff --git a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js b/addons/sale_timesheet/static/tests/timesheet_plan_tests.js deleted file mode 100644 index 17e9377afb7..00000000000 --- a/addons/sale_timesheet/static/tests/timesheet_plan_tests.js +++ /dev/null @@ -1,94 +0,0 @@ -odoo.define('sale_timesheet.timesheet_plan_tests', function (require) { -"use strict"; - -var ProjectPlan = require('sale_timesheet.ProjectPlan'); -var testUtils = require('web.test_utils'); -var Widget = require('web.Widget'); - -function createPlan(params) { - var Parent = Widget.extend({ - do_push_state: function () {}, - }); - var parent = new Parent(); - testUtils.mock.addMockEnvironment(parent, params); - var plan = new ProjectPlan(parent, params.action); - var selector = params.debug ? 'body' : '#qunit-fixture'; - plan.appendTo($(selector)); - - plan.destroy = function () { - delete plan.destroy; - parent.destroy(); - }; - - return plan; -} - -QUnit.module('Timesheet Plan', { - beforeEach: function () { - this.data = { - 'project.project': { - fields: { - account_id: {string: "Analytic account", type: "many2one", relation: "account.analytic.account"}, - }, - records: [] - }, - }; - } -}, function () { - - QUnit.test('basic timesheet plan rendering', function (assert) { - assert.expect(5); - - var plan = createPlan({ - archs: { - 'project.project,false,search': '', - }, - data: this.data, - action: {}, - mockRPC: function (route) { - assert.step(route); - if (route === '/timesheet/plan') { - return $.when({html_content: '

Banach-Tarski

'}); - } - return this._super.apply(this, arguments); - }, - intercepts: { - search: function () { - // when the search event get up to the action manager, - // bad things can happen - throw new Error('Search event should not get out of client action'); - }, - }, - }); - assert.verifySteps([ - "/web/dataset/call_kw/project.project", - "/timesheet/plan" - ]); - assert.strictEqual(plan.$('.o_content').text(), 'Banach-Tarski', 'should have rendered html content'); - assert.strictEqual(plan.$('.o_control_panel .breadcrumb-item').text(), 'Overview', - 'default title should be set'); - plan.destroy(); - }); - - QUnit.test('timesheet action takes action name into account', function (assert) { - assert.expect(1); - - var plan = createPlan({ - archs: { - 'project.project,false,search': '', - }, - data: this.data, - action: {name: 'Fibonacci'}, - mockRPC: function (route) { - if (route === '/timesheet/plan') { - return $.when({html_content: '

Banach-Tarski

'}); - } - return this._super.apply(this, arguments); - }, - }); - assert.strictEqual(plan.$('.o_control_panel .breadcrumb-item').text(), 'Fibonacci', - 'title should be set from the action name'); - plan.destroy(); - }); -}); -}); diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml index cd3037c4443..7f00a03ee19 100644 --- a/addons/sale_timesheet/views/hr_timesheet_templates.xml +++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml @@ -3,20 +3,24 @@ - - - - -