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 @@
-
-
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': '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 @@| - Hours - | +
|---|
| + Hours + | +
+ |
+ Timesheets | ++ Rates + | +
+ |
+ Timesheets | ++ Profitability + | +
+ |
+ Invoiced | +|||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
+ |
+ Fixed | +
+ |
+ Fixed | +
+ |
+ To invoice | +||||||||
|
+ |
+ No task found | +
+ |
+ No task found | +
+ |
+ Timesheet costs | +||||||||
|
+ |
+ Non billable | +
+ |
+ Non billable | +
- |
- Timesheets | -- Rates - | -
- |
- Timesheets | -- Profitability - | -
- |
- Invoiced | -||
|
- |
- Fixed | -
- |
- Fixed | -
- |
- To invoice | -||||||||
|
- |
- No task found | -
- |
- No task found | -
- |
- Timesheet costs | -||||||||
|
- |
- Non billable | -
- |
- Non billable | -
- |
- Other costs | -
- |
- Total | -||||||
| Other costs | + +
- |
Total | +|||||||||||
|
+ |
+ Total | ++ | + |
+ |
+ Re-invoiced costs | +- |
- |
- Re-invoiced costs | -- | - | |||
| + | + | + | + |
+ |
+ Total | +- | - | - |
- |
- Total | -- | - |