[IMP] sale_timesheet: revamp project overview
The project overview is kind of badly design. This commit does not solve all the issue but tries to make it simpler and more efficient: - Does not depends on AAL anymore: the overview search is now about projects. You can see the overview of multiple projects. Even when no timesheet are encoded, the plan will not be empty. - "Time by people" displays each employee assigned to task, even if no timesheet are recorded. - Code now use reporting view to keep consistency and simple code in overview controller.
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import babel
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import http, _
|
||||
from odoo import http, fields, _
|
||||
from odoo.http import request
|
||||
from odoo.osv import expression
|
||||
|
||||
from odoo.tools import float_round
|
||||
|
||||
@@ -12,47 +13,49 @@ class SaleTimesheetController(http.Controller):
|
||||
|
||||
@http.route('/timesheet/plan', type='json', auth="user")
|
||||
def plan(self, domain):
|
||||
domain = expression.AND([domain, [('project_id', '!=', False)]]) # force timesheet and not AAL
|
||||
values = self._prepare_plan_values(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)
|
||||
}
|
||||
|
||||
def _prepare_plan_values(self, domain):
|
||||
def _plan_prepare_values(self, projects):
|
||||
|
||||
timesheet_lines = request.env['account.analytic.line'].search(domain)
|
||||
currency = request.env.user.company_id.currency_id
|
||||
|
||||
values = {
|
||||
'currency': currency,
|
||||
'timesheet_lines': timesheet_lines,
|
||||
'domain': domain,
|
||||
}
|
||||
hour_rounding = request.env.ref('uom.product_uom_hour').rounding
|
||||
hour_rounding = request.env.ref('product.product_uom_hour').rounding
|
||||
billable_types = ['non_billable', 'non_billable_project', 'billable_time', 'billable_fixed']
|
||||
|
||||
# -- Stat Buttons
|
||||
values['stat_buttons'] = self._plan_get_stat_button(timesheet_lines)
|
||||
values = {
|
||||
'projects': projects,
|
||||
'currency': currency,
|
||||
'timesheet_domain': [('project_id', 'in', projects.ids)],
|
||||
'stat_buttons': self._plan_get_stat_button(projects),
|
||||
}
|
||||
|
||||
# -- Dashboard (per billable type)
|
||||
#
|
||||
# Hours, Rates and Profitability
|
||||
#
|
||||
dashboard_values = {
|
||||
'hours': dict.fromkeys(billable_types + ['total'], 0.0),
|
||||
'rates': dict.fromkeys(billable_types + ['total'], 0.0),
|
||||
'money_amount': {
|
||||
'profit': {
|
||||
'invoiced': 0.0,
|
||||
'to_invoiced': 0.0,
|
||||
'to_invoice': 0.0,
|
||||
'cost': 0.0,
|
||||
'total': 0.0,
|
||||
}
|
||||
}
|
||||
dashboard_domain = domain + [('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
dashboard_data = request.env['account.analytic.line'].read_group(dashboard_domain, ['unit_amount', 'timesheet_revenue', 'timesheet_invoice_type'], ['timesheet_invoice_type'])
|
||||
|
||||
# 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_revenue', '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']
|
||||
# hours
|
||||
dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
|
||||
dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
|
||||
# rates
|
||||
@@ -60,26 +63,37 @@ class SaleTimesheetController(http.Controller):
|
||||
dashboard_values['rates'][billable_type] = rate
|
||||
dashboard_values['rates']['total'] += rate
|
||||
|
||||
# profitability
|
||||
project_ids = values['timesheet_lines'].mapped('project_id').ids
|
||||
money_amount = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'total'], 0.0)
|
||||
|
||||
profitability_raw_data = request.env['project.profitability.report'].read_group([('project_id', 'in', project_ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost'], ['project_id'])
|
||||
# profitability, using profitability SQL report
|
||||
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', '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'], ['project_id'])
|
||||
for data in profitability_raw_data:
|
||||
money_amount['invoiced'] += data.get('amount_untaxed_invoiced', 0.0)
|
||||
money_amount['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0)
|
||||
money_amount['cost'] += data.get('timesheet_cost', 0.0)
|
||||
money_amount['total'] = sum([money_amount[item] for item in money_amount.keys()])
|
||||
dashboard_values['money_amount'] = money_amount
|
||||
profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0)
|
||||
profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0)
|
||||
profit['cost'] += data.get('timesheet_cost', 0.0)
|
||||
profit['total'] = sum([profit[item] for item in profit.keys()])
|
||||
dashboard_values['profit'] = profit
|
||||
|
||||
values['dashboard'] = dashboard_values
|
||||
|
||||
# -- Time Repartition (per employee)
|
||||
repartition_domain = domain + [('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
#
|
||||
# Time Repartition (per employee per billable types)
|
||||
#
|
||||
employees = projects.mapped('tasks.user_id.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)
|
||||
|
||||
# set repartition per type per employee
|
||||
repartition_employee = {}
|
||||
for employee in employees:
|
||||
repartition_employee[employee.id] = dict(
|
||||
employee_id=employee.id,
|
||||
employee_name=employee.name,
|
||||
non_billable_project=0.0,
|
||||
non_billable=0.0,
|
||||
billable_time=0.0,
|
||||
billable_fixed=0.0,
|
||||
total=0.0,
|
||||
)
|
||||
for data in repartition_data:
|
||||
employee_id = data['employee_id'][0]
|
||||
repartition_employee.setdefault(employee_id, dict(
|
||||
@@ -91,7 +105,7 @@ class SaleTimesheetController(http.Controller):
|
||||
billable_fixed=0.0,
|
||||
total=0.0,
|
||||
))[data['timesheet_invoice_type']] = float_round(data.get('unit_amount', 0.0), precision_rounding=hour_rounding)
|
||||
repartition_employee[employee_id]['__domain_'+data['timesheet_invoice_type']] = data['__domain']
|
||||
repartition_employee[employee_id]['__domain_' + data['timesheet_invoice_type']] = data['__domain']
|
||||
|
||||
# compute total
|
||||
for employee_id, vals in repartition_employee.items():
|
||||
@@ -103,21 +117,102 @@ class SaleTimesheetController(http.Controller):
|
||||
|
||||
return values
|
||||
|
||||
def _plan_get_stat_button(self, timesheet_lines):
|
||||
def _table_header(self, projects):
|
||||
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]
|
||||
|
||||
header = [_('Name'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Done'), _('Sold'), _('Remaining')]
|
||||
return header
|
||||
|
||||
def _table_row_default(self, projects):
|
||||
lenght = self._table_header(projects)
|
||||
return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining
|
||||
|
||||
def _table_rows_sql_query(self, projects):
|
||||
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
|
||||
query = """
|
||||
SELECT
|
||||
'timesheet' AS type,
|
||||
date_trunc('month', date)::date AS month_date,
|
||||
E.id AS employee_id,
|
||||
S.order_id AS sale_order_id,
|
||||
A.so_line AS sale_line_id,
|
||||
SUM(A.unit_amount) AS number_hours
|
||||
FROM account_analytic_line A
|
||||
JOIN hr_employee E ON E.id = A.employee_id
|
||||
LEFT JOIN sale_order_line S ON S.id = A.so_line
|
||||
WHERE A.project_id IS NOT NULL
|
||||
AND A.project_id IN %s
|
||||
AND A.date < %s
|
||||
GROUP BY date_trunc('month', date)::date, S.order_id, A.so_line, E.id
|
||||
"""
|
||||
|
||||
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)
|
||||
return query, query_params
|
||||
|
||||
def _table_rows_get_employee_lines(self, projects, data_from_db):
|
||||
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
|
||||
default_row_vals = self._table_row_default(projects)
|
||||
|
||||
# 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)}
|
||||
|
||||
# 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]
|
||||
for data in data_from_db:
|
||||
sale_line_id = data['sale_line_id']
|
||||
sale_order_id = data['sale_order_id']
|
||||
# employee row
|
||||
row_key = (data['sale_order_id'], sale_line_id, data['employee_id'])
|
||||
if row_key not in rows_employee:
|
||||
meta_vals = {
|
||||
'label': map_empl_names.get(row_key[2]),
|
||||
'sale_line_id': sale_line_id,
|
||||
'sale_order_id': sale_order_id,
|
||||
'res_id': row_key[2],
|
||||
'res_model': 'hr.employee',
|
||||
'type': 'hr_employee'
|
||||
}
|
||||
rows_employee[row_key] = [meta_vals] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
|
||||
|
||||
index = False
|
||||
if data['type'] == 'timesheet':
|
||||
if data['month_date'] in ts_months:
|
||||
index = ts_months.index(data['month_date']) + 2
|
||||
elif data['month_date'] < ts_months[0]:
|
||||
index = 1
|
||||
rows_employee[row_key][index] += data['number_hours']
|
||||
rows_employee[row_key][5] += data['number_hours']
|
||||
return rows_employee
|
||||
|
||||
# --------------------------------------------------
|
||||
# Actions: Stat buttons, ...
|
||||
# --------------------------------------------------
|
||||
|
||||
def _plan_get_stat_button(self, projects):
|
||||
stat_buttons = []
|
||||
stat_buttons.append({
|
||||
'name': _('Timesheets'),
|
||||
'res_model': 'account.analytic.line',
|
||||
'domain': [('id', 'in', timesheet_lines.ids)],
|
||||
'domain': [('project_id', 'in', projects.ids)],
|
||||
'icon': 'fa fa-calendar',
|
||||
})
|
||||
stat_project_ids = timesheet_lines.mapped('project_id').ids
|
||||
stat_task_domain = [('project_id', 'in', stat_project_ids), '|', ('stage_id', '=', False), ('stage_id.fold', '=', False)]
|
||||
stat_buttons.append({
|
||||
'name': _('Tasks'),
|
||||
'count': request.env['project.task'].search_count(stat_task_domain),
|
||||
'count': sum(projects.mapped('task_count')),
|
||||
'res_model': 'project.task',
|
||||
'domain': stat_task_domain,
|
||||
'domain': [('project_id', 'in', projects.ids)],
|
||||
'icon': 'fa fa-tasks',
|
||||
})
|
||||
return stat_buttons
|
||||
|
||||
@@ -41,16 +41,16 @@ class Project(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_view_timesheet_plan(self):
|
||||
return {
|
||||
'name': _('Overview'),
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'timesheet.plan',
|
||||
'context': {
|
||||
'active_id': self.id,
|
||||
'active_ids': self.ids,
|
||||
'search_default_project_id': self.id,
|
||||
}
|
||||
action = self.env.ref('sale_timesheet.project_timesheet_action_client_timesheet_plan').read()[0]
|
||||
action['params'] = {
|
||||
'project_ids': self.ids,
|
||||
}
|
||||
action['context'] = {
|
||||
'active_id': self.id,
|
||||
'active_ids': self.ids,
|
||||
'search_default_display_name': self.name,
|
||||
}
|
||||
return action
|
||||
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
|
||||
@@ -33,7 +33,7 @@ var ProjectPlan = AbstractAction.extend(ControlPanelMixin, {
|
||||
var self = this;
|
||||
var view_id = this.action && this.action.search_view_id && this.action.search_view_id[0];
|
||||
var def = this
|
||||
.loadViews('account.analytic.line', this.action.context || {}, [[view_id, 'search']])
|
||||
.loadViews('project.project', this.action.context || {}, [[view_id, 'search']])
|
||||
.then(function (result) {
|
||||
self.fields_view = result.search;
|
||||
});
|
||||
@@ -63,7 +63,7 @@ var ProjectPlan = AbstractAction.extend(ControlPanelMixin, {
|
||||
search_defaults: search_defaults,
|
||||
};
|
||||
|
||||
var dataset = new data.DataSetSearch(this, 'account.analytic.line');
|
||||
var dataset = new data.DataSetSearch(this, 'project.project');
|
||||
this.searchview = new SearchView(this, dataset, this.fields_view, options);
|
||||
this.searchview.on('search', this, this._onSearch);
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ function createPlan(params) {
|
||||
QUnit.module('Timesheet Plan', {
|
||||
beforeEach: function () {
|
||||
this.data = {
|
||||
'account.analytic.line': {
|
||||
'project.project': {
|
||||
fields: {
|
||||
account_id: {string: "Analytic account", type: "many2one", relation: "account.analytic.account"},
|
||||
},
|
||||
@@ -41,7 +41,7 @@ QUnit.module('Timesheet Plan', {
|
||||
|
||||
var plan = createPlan({
|
||||
archs: {
|
||||
'account.analytic.line,false,search': '<search></search>',
|
||||
'project.project,false,search': '<search></search>',
|
||||
},
|
||||
data: this.data,
|
||||
action: {},
|
||||
@@ -61,7 +61,7 @@ QUnit.module('Timesheet Plan', {
|
||||
},
|
||||
});
|
||||
assert.verifySteps([
|
||||
"/web/dataset/call_kw/account.analytic.line",
|
||||
"/web/dataset/call_kw/project.project",
|
||||
"/timesheet/plan"
|
||||
]);
|
||||
assert.strictEqual(plan.$el.text(), 'Banach-Tarski', 'should have rendered html content');
|
||||
@@ -75,7 +75,7 @@ QUnit.module('Timesheet Plan', {
|
||||
|
||||
var plan = createPlan({
|
||||
archs: {
|
||||
'account.analytic.line,false,search': '<search></search>',
|
||||
'project.project,false,search': '<search></search>',
|
||||
},
|
||||
data: this.data,
|
||||
action: {name: 'Fibonacci'},
|
||||
|
||||
@@ -43,24 +43,24 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<th rowspan="5">
|
||||
<a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['date:month'],'pivot_column_groupby': ['timesheet_invoice_type'], 'pivot_measures': ['unit_amount']}">Hours</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['date:month'],'pivot_column_groupby': ['timesheet_invoice_type'], 'pivot_measures': ['unit_amount']}">Hours</a>
|
||||
</th>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['hours']['billable_time']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td>Timesheets</td>
|
||||
<th rowspan="5">
|
||||
<a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['date:month', 'employee_id'], 'pivot_column_groupby': ['timesheet_invoice_type'],'pivot_measures': ['unit_amount']}">Rates</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['date:month', 'employee_id'], 'pivot_column_groupby': ['timesheet_invoice_type'],'pivot_measures': ['unit_amount']}">Rates</a>
|
||||
</th>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['rates']['billable_time']"/> %
|
||||
</td>
|
||||
<td>Timesheets</td>
|
||||
<th rowspan="5">
|
||||
<a type="action" t-att-domain="json.dumps(domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a>
|
||||
</th>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['money_amount']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<t t-esc="dashboard['profit']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td>Invoiced</td>
|
||||
</tr>
|
||||
@@ -74,7 +74,7 @@
|
||||
</td>
|
||||
<td>Fixed</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['money_amount']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<t t-esc="dashboard['profit']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td>To invoice</td>
|
||||
</tr>
|
||||
@@ -88,19 +88,19 @@
|
||||
</td>
|
||||
<td>No task found</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['money_amount']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td>Cost</td>
|
||||
<td>Timesheet costs</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['hours']['non_billable']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td>Non Billable</td>
|
||||
<td>Non billable</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['rates']['non_billable']"/> %
|
||||
</td>
|
||||
<td>Non Billable</td>
|
||||
<td>Non billable</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell"></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
@@ -112,7 +112,7 @@
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td class="o_timesheet_plan_dashboard_total">
|
||||
<t t-esc="dashboard['money_amount']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td><b>Total</b></td>
|
||||
</tr>
|
||||
@@ -128,16 +128,16 @@
|
||||
<t t-if="repartition_employee">
|
||||
<div class="pull-right o_timesheet_plan_badge">
|
||||
<span class="badge o_progress_billable_fixed">
|
||||
<a type="action" t-att-domain="json.dumps(domain + [('timesheet_invoice_type','=','billable_fixed')])">Billable fixed</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_fixed')])">Billable fixed</a>
|
||||
</span>
|
||||
<span class="badge o_progress_billable_time">
|
||||
<a type="action" t-att-domain="json.dumps(domain + [('timesheet_invoice_type','=','billable_time')])">Billable time</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_time')])">Billable time</a>
|
||||
</span>
|
||||
<span class="badge o_progress_non_billable_project">
|
||||
<a type="action" t-att-domain="json.dumps(domain + [('timesheet_invoice_type','=','non_billable_project')])">No task found</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_project')])">No task found</a>
|
||||
</span>
|
||||
<span class="badge o_progress_non_billable">
|
||||
<a type="action" t-att-domain="json.dumps(domain + [('timesheet_invoice_type','=','non_billable')])">Non billable</a>
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable')])">Non billable</a>
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
@@ -145,7 +145,7 @@
|
||||
<t t-foreach="repartition_employee" t-as="employee_id">
|
||||
<tr>
|
||||
<td style="width: 15%">
|
||||
<a type="action" t-att-domain="json.dumps(domain)" t-att-context="{'search_default_employee_id': employee_id}" views="[[0, 'list']]">
|
||||
<a type="action" t-att-domain="json.dumps(timesheet_domain)" t-att-context="{'search_default_employee_id': employee_id}" views="[[0, 'list']]">
|
||||
<t t-esc="repartition_employee[employee_id]['employee_name']"/>
|
||||
</a>
|
||||
</td>
|
||||
@@ -153,7 +153,7 @@
|
||||
<t t-esc="repartition_employee[employee_id]['total']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="progress" t-att-style="'width: ' + str(repartition_employee[employee_id]['total'] / repartition_employee_max * 100) +'%'">
|
||||
<div t-if="repartition_employee_max" class="progress" t-att-style="'width: ' + str(repartition_employee[employee_id]['total'] / repartition_employee_max * 100) +'%'">
|
||||
|
||||
<t t-set="total" t-value="repartition_employee[employee_id]['total']" />
|
||||
<t t-set="billable_fixed" t-value="repartition_employee[employee_id]['billable_fixed']" />
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_project_view_search" model="ir.ui.view">
|
||||
<field name="name">project.project.search.inherit</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="project.view_project_project_filter"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='name']" position="after">
|
||||
<field name="display_name" filter_domain="[('name','=',self)]" string="Exact name"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sale_service_inherit_form2" model="ir.ui.view">
|
||||
<field name="name">sale.service.form.view.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
@@ -47,4 +58,12 @@
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_timesheet_action_client_timesheet_plan" model="ir.actions.client">
|
||||
<field name="name">Overview</field>
|
||||
<field name="tag">timesheet.plan</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="params" eval="{}"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user