Merge branch 'development' of http://103.74.223.20:8085/prakash.jain/cor-odoo into development
This commit is contained in:
commit
aa9daafabe
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -18,14 +18,7 @@ class Project(models.Model):
|
|||
_inherit = 'project.project'
|
||||
|
||||
|
||||
def _qweb_prepare_qcontext(self, view_id, domain):
|
||||
values = super()._qweb_prepare_qcontext(view_id, domain)
|
||||
|
||||
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.company.currency_id
|
||||
|
@ -181,384 +174,6 @@ class Project(models.Model):
|
|||
values['timesheet_forecast_table'] = timesheet_forecast_table_rows
|
||||
return values
|
||||
|
||||
def _table_get_line_values(self, employees=None):
|
||||
""" return the header and the rows informations of the table """
|
||||
if not self:
|
||||
return False
|
||||
|
||||
uom_hour = self.env.ref('uom.product_uom_hour')
|
||||
company_uom = self.env.company.timesheet_encode_uom_id
|
||||
is_uom_day = company_uom and company_uom == self.env.ref('uom.product_uom_day')
|
||||
|
||||
# build SQL query and fetch raw data
|
||||
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()
|
||||
|
||||
# extract row labels
|
||||
sale_line_ids = set()
|
||||
sale_order_ids = set()
|
||||
for key_tuple, row in rows_employee.items():
|
||||
if row[0]['sale_line_id']:
|
||||
sale_line_ids.add(row[0]['sale_line_id'])
|
||||
if row[0]['sale_order_id']:
|
||||
sale_order_ids.add(row[0]['sale_order_id'])
|
||||
|
||||
sale_orders = self.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 sale_orders}
|
||||
map_so_cancel = {so.id: so.state == 'cancel' for so in sale_orders}
|
||||
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}
|
||||
|
||||
rows_sale_line = {} # (so, sol) -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted]
|
||||
for sale_line_id in empty_line_ids: # add service SO line having no timesheet
|
||||
sale_line_row_key = (map_sol_so.get(sale_line_id), sale_line_id)
|
||||
sale_line = map_sol.get(sale_line_id)
|
||||
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, _('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[:]
|
||||
if not is_milestone:
|
||||
rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0
|
||||
|
||||
rows_sale_line_all_data = {}
|
||||
if not employees:
|
||||
employees = self.env['hr.employee'].sudo().search(self.env['account.analytic.line']._domain_employee_id())
|
||||
for row_key, row_employee in rows_employee.items():
|
||||
sale_order_id, sale_line_id, employee_id = row_key
|
||||
# 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, 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:
|
||||
rows_sale_line[sale_line_row_key][-2] = sale_line.product_uom._compute_quantity(sale_line.product_uom_qty, uom_hour, raise_if_failure=False) if sale_line else 0.0
|
||||
|
||||
if sale_line_row_key not in rows_sale_line_all_data:
|
||||
rows_sale_line_all_data[sale_line_row_key] = [0] * len(row_employee)
|
||||
for index in range(1, len(row_employee)):
|
||||
if employee_id in employees.ids:
|
||||
rows_sale_line[sale_line_row_key][index] += row_employee[index]
|
||||
rows_sale_line_all_data[sale_line_row_key][index] += row_employee[index]
|
||||
if not rows_sale_line[sale_line_row_key][0].get('is_milestone'):
|
||||
rows_sale_line[sale_line_row_key][-1] = rows_sale_line[sale_line_row_key][-2] - rows_sale_line_all_data[sale_line_row_key][5]
|
||||
else:
|
||||
rows_sale_line[sale_line_row_key][-1] = 0
|
||||
|
||||
rows_sale_order = {} # so -> [INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted]
|
||||
for row_key, row_sale_line in rows_sale_line.items():
|
||||
sale_order_id = row_key[0]
|
||||
# sale order row
|
||||
if sale_order_id not in rows_sale_order:
|
||||
rows_sale_order[sale_order_id] = [{'label': map_so_names.get(sale_order_id, _('No Sales Order')), 'canceled': map_so_cancel.get(sale_order_id, False), 'res_id': sale_order_id, 'res_model': 'sale.order', 'type': 'sale_order'}] + default_row_vals[:] # INFO, before, M1, M2, M3, Done, M3, M4, M5, After, Forecasted
|
||||
|
||||
for index in range(1, len(row_sale_line)):
|
||||
rows_sale_order[sale_order_id][index] += row_sale_line[index]
|
||||
|
||||
# group rows SO, SOL and their related employee rows.
|
||||
timesheet_forecast_table_rows = []
|
||||
for sale_order_id, sale_order_row in rows_sale_order.items():
|
||||
timesheet_forecast_table_rows.append(sale_order_row)
|
||||
for sale_line_row_key, sale_line_row in rows_sale_line.items():
|
||||
if sale_order_id == sale_line_row_key[0]:
|
||||
sale_order_row[0]['has_children'] = True
|
||||
timesheet_forecast_table_rows.append(sale_line_row)
|
||||
for employee_row_key, employee_row in rows_employee.items():
|
||||
if sale_order_id == employee_row_key[0] and sale_line_row_key[1] == employee_row_key[1] and employee_row_key[2] in employees.ids:
|
||||
sale_line_row[0]['has_children'] = True
|
||||
timesheet_forecast_table_rows.append(employee_row)
|
||||
|
||||
if is_uom_day:
|
||||
# convert all values from hours to days
|
||||
for row in timesheet_forecast_table_rows:
|
||||
for index in range(1, len(row)):
|
||||
row[index] = round(uom_hour._compute_quantity(row[index], company_uom, raise_if_failure=False), 2)
|
||||
# complete table data
|
||||
return {
|
||||
'header': self._table_header(),
|
||||
'rows': timesheet_forecast_table_rows
|
||||
}
|
||||
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=get_lang(self.env).code)[month_index]
|
||||
|
||||
header_names = [_('Sales Order'), _('Before')] + [_to_short_month_name(date) for date in ts_months] + [_('Total'), _('Sold'), _('Remaining')]
|
||||
|
||||
result = []
|
||||
for name in header_names:
|
||||
result.append({
|
||||
'label': name,
|
||||
'tooltip': '',
|
||||
})
|
||||
# add tooltip for reminaing
|
||||
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):
|
||||
lenght = len(self._table_header())
|
||||
return [0.0] * (lenght - 1) # before, M1, M2, M3, Done, Sold, Remaining
|
||||
|
||||
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
|
||||
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(self.ids), last_ts_month)
|
||||
return query, query_params
|
||||
|
||||
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()
|
||||
|
||||
# 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 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]
|
||||
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
|
||||
|
||||
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 = self.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense and not sol.is_downpayment)
|
||||
# include the service SO line of SO sharing the same project
|
||||
sale_order = self.env['sale.order'].search([('project_id', 'in', self.ids)])
|
||||
return set(so_lines.ids) | set(sale_order.mapped('order_line').filtered(lambda sol: sol.is_service and not sol.is_expense).ids), set(so_lines.mapped('order_id').ids) | set(sale_order.ids)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Actions: Stat buttons, ...
|
||||
# --------------------------------------------------
|
||||
|
||||
def _plan_prepare_actions(self, values):
|
||||
actions = []
|
||||
if len(self) == 1:
|
||||
task_order_line_ids = []
|
||||
# retrieve all the sale order line that we will need later below
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id'])
|
||||
task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids]
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman'):
|
||||
if self.bill_type == 'customer_project' and self.allow_billable and not self.sale_order_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': self.id, 'active_model': 'project.project'}),
|
||||
})
|
||||
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_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id'])
|
||||
sale_order_ids = [s['order_id'][0] for s in sale_order_ids]
|
||||
sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id'])
|
||||
sale_order_ids = list(map(lambda x: x['id'], sale_order_ids))
|
||||
|
||||
if to_invoice_amount and sale_order_ids:
|
||||
if len(sale_order_ids) == 1:
|
||||
actions.append({
|
||||
'label': _("Create Invoice"),
|
||||
'type': 'action',
|
||||
'action_id': 'sale.action_view_sale_advance_payment_inv',
|
||||
'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}),
|
||||
})
|
||||
else:
|
||||
actions.append({
|
||||
'label': _("Create Invoice"),
|
||||
'type': 'action',
|
||||
'action_id': 'sale_timesheet.project_project_action_multi_create_invoice',
|
||||
'context': json.dumps({'active_id': self.id, 'active_model': 'project.project'}),
|
||||
})
|
||||
return actions
|
||||
|
||||
def _plan_get_stat_button(self):
|
||||
stat_buttons = []
|
||||
num_projects = len(self)
|
||||
if num_projects == 1:
|
||||
action_data = _to_action_data('project.project', res_id=self.id,
|
||||
views=[[self.env.ref('project.edit_project').id, 'form']])
|
||||
else:
|
||||
action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(),
|
||||
domain=[('id', 'in', self.ids)])
|
||||
|
||||
stat_buttons.append({
|
||||
'name': _('Project') if num_projects == 1 else _('Projects'),
|
||||
'count': num_projects,
|
||||
'icon': 'fa fa-puzzle-piece',
|
||||
'action': action_data
|
||||
})
|
||||
|
||||
# if only one project, add it in the context as default value
|
||||
tasks_domain = [('project_id', 'in', self.ids)]
|
||||
tasks_context = self.env.context.copy()
|
||||
tasks_context.pop('search_default_name', False)
|
||||
late_tasks_domain = [('project_id', 'in', self.ids), ('date_deadline', '<', fields.Date.to_string(fields.Date.today())), ('date_end', '=', False)]
|
||||
overtime_tasks_domain = [('project_id', 'in', self.ids), ('overtime', '>', 0), ('planned_hours', '>', 0)]
|
||||
|
||||
# filter out all the projects that have no tasks
|
||||
task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id'])
|
||||
task_projects_ids = [p['project_id'][0] for p in task_projects_ids]
|
||||
|
||||
if len(task_projects_ids) == 1:
|
||||
tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]}
|
||||
stat_buttons.append({
|
||||
'name': _('Tasks'),
|
||||
'count': sum(self.mapped('task_count')),
|
||||
'icon': 'fa fa-tasks',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('project.action_view_task').sudo(),
|
||||
domain=tasks_domain,
|
||||
context=tasks_context
|
||||
)
|
||||
})
|
||||
stat_buttons.append({
|
||||
'name': [_("Tasks"), _("Late")],
|
||||
'count': self.env['project.task'].search_count(late_tasks_domain),
|
||||
'icon': 'fa fa-tasks',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('project.action_view_task').sudo(),
|
||||
domain=late_tasks_domain,
|
||||
context=tasks_context,
|
||||
),
|
||||
})
|
||||
stat_buttons.append({
|
||||
'name': [_("Tasks"), _("in Overtime")],
|
||||
'count': self.env['project.task'].search_count(overtime_tasks_domain),
|
||||
'icon': 'fa fa-tasks',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('project.action_view_task').sudo(),
|
||||
domain=overtime_tasks_domain,
|
||||
context=tasks_context,
|
||||
),
|
||||
})
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
# read all the sale orders linked to the projects' tasks
|
||||
task_so_ids = self.env['project.task'].search_read([
|
||||
('project_id', 'in', self.ids), ('sale_order_id', '!=', False)
|
||||
], ['sale_order_id'])
|
||||
task_so_ids = [o['sale_order_id'][0] for o in task_so_ids]
|
||||
|
||||
sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids)
|
||||
if sale_orders:
|
||||
stat_buttons.append({
|
||||
'name': _('Sales Orders'),
|
||||
'count': len(sale_orders),
|
||||
'icon': 'fa fa-dollar',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('sale.action_orders').sudo(),
|
||||
domain=[('id', 'in', sale_orders.ids)],
|
||||
context={'create': False, 'edit': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
|
||||
invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids'])
|
||||
invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids]))
|
||||
invoice_ids = self.env['account.move'].search_read([('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')], ['id'])
|
||||
invoice_ids = list(map(lambda x: x['id'], invoice_ids))
|
||||
|
||||
if invoice_ids:
|
||||
stat_buttons.append({
|
||||
'name': _('Invoices'),
|
||||
'count': len(invoice_ids),
|
||||
'icon': 'fa fa-pencil-square-o',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('account.action_move_out_invoice_type').sudo(),
|
||||
domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')],
|
||||
context={'create': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
|
||||
ts_tree = self.env.ref('hr_timesheet.hr_timesheet_line_tree')
|
||||
ts_form = self.env.ref('hr_timesheet.hr_timesheet_line_form')
|
||||
if self.env.company.timesheet_encode_uom_id == self.env.ref('uom.product_uom_day'):
|
||||
timesheet_label = [_('Days'), _('Recorded')]
|
||||
else:
|
||||
timesheet_label = [_('Hours'), _('Recorded')]
|
||||
|
||||
stat_buttons.append({
|
||||
'name': timesheet_label,
|
||||
'count': sum(self.mapped('total_timesheet_time')),
|
||||
'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')],
|
||||
)
|
||||
})
|
||||
|
||||
return stat_buttons
|
||||
|
||||
|
||||
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], env=action.env)
|
||||
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
|
||||
|
||||
|
|
|
@ -1,467 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="sale_timesheet.timesheet_plan" model="ir.ui.view">
|
||||
<record id="inherit_timesheet_plan" model="ir.ui.view">
|
||||
<field name="name">Timesheet Plan</field>
|
||||
<field name="type">qweb</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="inherit_id" ref="sale_timesheet.timesheet_plan"/>
|
||||
<field name="arch" type="xml">
|
||||
<qweb js_class="project_overview">
|
||||
<nav class="o_qweb_cp_buttons" t-if="actions">
|
||||
<button t-foreach="actions" t-as="action"
|
||||
type="action" class="btn btn-primary"
|
||||
t-att-name="action['action_id']"
|
||||
t-att-data-context="action.get('context')"
|
||||
>
|
||||
<t t-esc="action['label']"/>
|
||||
</button>
|
||||
</nav>
|
||||
<div class="o_form_view o_form_readonly o_project_plan">
|
||||
<div class="o_form_sheet_bg">
|
||||
<div class="o_form_sheet o_timesheet_plan_content">
|
||||
<div class="o_timesheet_plan_sale_timesheet">
|
||||
<div class="o_timesheet_plan_sale_timesheet_dashboard">
|
||||
|
||||
<div class="o_timesheet_plan_stat_buttons oe_button_box o_not_full">
|
||||
<t t-foreach="stat_buttons" t-as="stat_button">
|
||||
<a class="btn oe_stat_button"
|
||||
type="action"
|
||||
t-att="stat_button['action']"
|
||||
>
|
||||
<div t-attf-class="fa fa-fw o_button_icon #{stat_button['icon']}" role="img" aria-label="Statistics" title="Statistics"></div>
|
||||
<div class="o_field_widget o_stat_info o_readonly_modifier" t-att-title="stat_button['name']">
|
||||
<t t-if="not isinstance(stat_button['name'], list)">
|
||||
<span class="o_stat_value" t-if="'count' in stat_button">
|
||||
<t t-esc="stat_button['count']"/>
|
||||
</span>
|
||||
<span class="o_stat_text">
|
||||
<t t-esc="stat_button['name']"/>
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="isinstance(stat_button['name'], list)">
|
||||
<div class="oe_inline">
|
||||
<span class="o_stat_value mr-1">
|
||||
<t t-esc="stat_button.get('count')"/>
|
||||
</span>
|
||||
<span class="o_stat_value">
|
||||
<t t-esc="stat_button['name'][0]"/>
|
||||
</span>
|
||||
</div>
|
||||
<span class="o_stat_text">
|
||||
<t t-esc="stat_button['name'][1]"/>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="o_title">
|
||||
<h2 t-if="is_uom_day">Recorded Days and Profitability</h2>
|
||||
<h2 t-else="">Recorded Hours and Profitability</h2>
|
||||
</div>
|
||||
|
||||
<t t-set="display_cost" t-value="dashboard['profit']['expense_cost'] != 0.0"/>
|
||||
<div class="o_profitability_wrapper">
|
||||
<div class="o_profitability_section">
|
||||
<div>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<th>
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)"
|
||||
data-context='{"pivot_row_groupby": ["date:month"],"pivot_column_groupby": ["timesheet_invoice_type"], "pivot_measures": ["unit_amount"]}'
|
||||
data-views='[[0, "pivot"], [0, "list"]]' tabindex="-1">
|
||||
<span t-if="is_uom_day">Days recorded</span>
|
||||
<span t-else="">Hours recorded</span>
|
||||
</a>
|
||||
</th>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['billable_time']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['billable_time']"/> %)
|
||||
</td>
|
||||
<td title="Includes the time logged into tasks for which you invoice based on timesheets on tasks.">
|
||||
Billed on Timesheets
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['billable_fixed']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['billable_fixed']"/> %)
|
||||
</td>
|
||||
<td title="Includes the time logged into tasks for which you invoice based on ordered quantities or on milestones.">
|
||||
Billed at a Fixed price
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="dashboard['time']['non_billable_project'] != 0">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['non_billable_project']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['non_billable_project']"/> %)
|
||||
</td>
|
||||
<td title="Includes the time logged from the Timesheet module that is linked to a project, but not to a task.">
|
||||
No task found
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['non_billable']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['non_billable']"/> %)
|
||||
</td>
|
||||
<td>
|
||||
<a type="action"
|
||||
data-model="project.task"
|
||||
data-views='[[false, "list"], [false, "form"]]'
|
||||
t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])"
|
||||
>
|
||||
<span class="btn-link"
|
||||
style="font-weight:normal;"
|
||||
title="Includes the time logged into a task which is not linked to any Sales Order.">
|
||||
Non Billable Tasks
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="dashboard['time']['non_billable_timesheet'] > 0">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['non_billable_timesheet']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['non_billable_timesheet']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['non_billable_timesheet']"/> %)
|
||||
</td>
|
||||
<td>
|
||||
<a type="action"
|
||||
data-model="account.analytic.line"
|
||||
data-views='[[false, "list"], [false, "form"]]'
|
||||
t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_timesheet')])"
|
||||
>
|
||||
<span class="btn-link"
|
||||
style="font-weight:normal;">
|
||||
Non Billable Timesheets
|
||||
</span>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="dashboard['time']['canceled'] > 0">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['canceled']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
(<t t-esc="dashboard['rates']['canceled']"/> %)
|
||||
</td>
|
||||
<td title="Includes the time logged into a task which is linked to a cancelled Sales Order.">
|
||||
Cancelled
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_total">
|
||||
<b>
|
||||
<t t-if="is_uom_day" t-esc="dashboard['time']['total']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="dashboard['time']['total']" t-options="{'widget': 'float_time'}"/>
|
||||
</b>
|
||||
</td>
|
||||
<td><b>Total</b></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_profitability_section">
|
||||
<div>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<th>
|
||||
<a type="action" data-model="project.profitability.report" t-att-data-domain="json.dumps(profitability_domain)" data-context="{'group_by_no_leaf':1, 'group_by':[], 'sale_show_order_product_name': 1}" data-views='[[0, "pivot"], [0, "graph"]]' tabindex="-1">Profitability</a>
|
||||
</th>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="Revenues linked to Timesheets already invoiced.">
|
||||
Invoiced
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="Revenues linked to Timesheets not yet invoiced.">
|
||||
To invoice
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="dashboard['profit']['other_revenues'] > 0">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['other_revenues']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="All revenues that are not from timesheets and that are linked to the analytic account of the project.">Other Revenues</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="This cost is based on the "Timesheet cost" set in the HR Settings of your employees.">
|
||||
Timesheet costs
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="display_cost">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['expense_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="Any cost linked to the Analytic Account of the Project.">
|
||||
Other costs
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="display_cost & (dashboard['profit']['expense_amount_untaxed_invoiced'] != 0)">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['expense_amount_untaxed_invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="Costs from expenses that were reinvoiced to your customer (provided that the Analytic Account of the Project was set on the Expense).">
|
||||
Re-invoiced costs
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_total">
|
||||
<b>
|
||||
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</b>
|
||||
</td>
|
||||
<td><b>Total</b></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="o_title">
|
||||
<h2>Time by people</h2>
|
||||
</div>
|
||||
|
||||
<div class="o_timesheet_plan_sale_timesheet_people_time">
|
||||
<t t-if="not repartition_employee">
|
||||
<p>There are no timesheets for now.</p>
|
||||
</t>
|
||||
<t t-if="repartition_employee">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Employee</th>
|
||||
<th>Hourly Rate</th>
|
||||
<th t-if="is_uom_day" class="text-nowrap text-right pr-5">Days Spent</th>
|
||||
<th t-else="" class="text-nowrap text-right pr-5">Hours Spent</th>
|
||||
<td>
|
||||
<div class="float-right o_timesheet_plan_badge">
|
||||
<span class="badge badge-pill o_progress_billable_time">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_time')])" tabindex="-1">Billed on Timesheets</a>
|
||||
</span>
|
||||
<span class="badge badge-pill o_progress_billable_fixed">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','billable_fixed')])" tabindex="-1">Billed at a Fixed price</a>
|
||||
</span>
|
||||
<span t-if="dashboard['time']['non_billable_project'] != 0" class="badge badge-pill o_progress_non_billable_project">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_project')])" tabindex="-1">No task found</a>
|
||||
</span>
|
||||
<span t-if="dashboard['time']['non_billable_timesheet'] != 0" class="badge badge-pill o_progress_non_billable_timesheet">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable_timesheet')])" tabindex="-1">Non billable timesheets</a>
|
||||
</span>
|
||||
<span class="badge badge-pill o_progress_non_billable">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('timesheet_invoice_type','=','non_billable')])" tabindex="-1">Non billable tasks</a>
|
||||
</span>
|
||||
<!-- only show the canceled pill if there were timesheets on canceled so -->
|
||||
<t t-if="sum([employee.get('canceled', 0.0) for employee in repartition_employee.values()]) > 0">
|
||||
<span class="badge badge-pill o_progress_canceled">
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain + [('so_line.state', '=', 'cancel')])" tabindex="-1">Cancelled</a>
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="repartition_employee" t-as="employee_id">
|
||||
<t t-set="employee" t-value="repartition_employee[employee_id]"/>
|
||||
<tr>
|
||||
<td style="width: 3%; vertical-align: middle;">
|
||||
<img class="img rounded-circle mr-2 mb-2" t-attf-src="/web/image?model=hr.employee&field=image_128&id=#{employee['employee_id']}" t-att-title="employee['employee_name']" t-att-alt="employee['employee_name']" width="25" height="25"/>
|
||||
</td>
|
||||
<td style="width: 15%; vertical-align: middle;" >
|
||||
<a type="action" data-model="account.analytic.line" t-att-data-domain="json.dumps(timesheet_domain)" t-att-data-context="json.dumps({'search_default_employee_id': employee_id})" data-views="[[0, "list"]]" tabindex="-1">
|
||||
<t t-esc="employee['employee_name']"/>
|
||||
</a>
|
||||
</td>
|
||||
<td style="width: 15%; vertical-align: middle;" >
|
||||
<t t-esc="employee['employee_price']"/>
|
||||
</td>
|
||||
<td class="text-right pr-5" style="width: 10%; vertical-align: middle;">
|
||||
<t t-if="is_uom_day" t-esc="employee['total']" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="employee['total']" t-options="{'widget': 'float_time'}"/>
|
||||
</td>
|
||||
<td style="vertical-align:middle">
|
||||
<div class="border rounded">
|
||||
<div t-if="repartition_employee_max" class="progress" t-attf-style="width: {{max(0, employee['total'] / repartition_employee_max * 100)}}%; margin-bottom: 0em;">
|
||||
|
||||
<t t-set="total" t-value="employee['total'] or 1.0" />
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">Billed on Timesheets</t>
|
||||
<t t-set="key" t-translation="off">billable_time</t>
|
||||
</t>
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">Billed at a Fixed price</t>
|
||||
<t t-set="key" t-translation="off">billable_fixed</t>
|
||||
</t>
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">No task found</t>
|
||||
<t t-set="key" t-translation="off">non_billable_project</t>
|
||||
</t>
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">Non billable timesheets</t>
|
||||
<t t-set="key" t-translation="off">non_billable_timesheet</t>
|
||||
</t>
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">Non billable tasks</t>
|
||||
<t t-set="key" t-translation="off">non_billable</t>
|
||||
</t>
|
||||
<t t-call="sale_timesheet.progressbar">
|
||||
<t t-set="label">Cancelled</t>
|
||||
<t t-set="key" t-translation="off">canceled</t>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<div class="o_title">
|
||||
<h2>Timesheets</h2>
|
||||
</div>
|
||||
|
||||
<!-- NOTE: this template to display a table works whatever the length of the rows, as project_timesheet_forecast_sale extends the table to add forecasts -->
|
||||
<div class="o_project_plan_project_timesheet_forecast">
|
||||
<t t-if="timesheet_forecast_table and timesheet_forecast_table['rows']">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th colspan="5" id="table_plan_title" class="o_right_bordered"><h3>Timesheets</h3></th>
|
||||
<th colspan="2" id="table_plan_total"></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<t t-foreach="timesheet_forecast_table['header']" t-as="header_val">
|
||||
<th t-att-class="'o_right_bordered' if header_val_index in [5,10] else ''">
|
||||
<span t-att-title="header_val['tooltip']"><t t-esc="header_val['label']"/></span>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-set="row_is_milestone" t-value="False"/>
|
||||
<t t-set="current_order" t-value="False"/>
|
||||
<t t-set="current_order_line" t-value="False"/>
|
||||
<t t-foreach="timesheet_forecast_table['rows']" t-as="row">
|
||||
<t t-set="row_type" t-value="row[0].get('type')"/>
|
||||
<t t-if="row_type == 'sale_order_line'">
|
||||
<t t-set="row_is_milestone" t-value="row[0].get('is_milestone')"/>
|
||||
</t>
|
||||
<t t-if="row_type == 'sale_order'">
|
||||
<t t-set="current_order" t-value="False"/>
|
||||
<t t-set="current_order_line" t-value="False"/>
|
||||
</t>
|
||||
<t t-if="row_type == 'sale_order_line'">
|
||||
<t t-set="current_order_line" t-value="False"/>
|
||||
</t>
|
||||
<t t-set="foldable" t-value="row[0].get('has_children')"/>
|
||||
<tr t-att-class="'o_timesheet_forecast_' + row_type + ' sale_order_' + str(current_order) + ' sale_order_line_' + str(current_order_line)"
|
||||
t-att-style="'display: none;' if row_type not in ('sale_order', 'sale_order_line') else ''">
|
||||
<t t-foreach="row" t-as="row_value">
|
||||
<td t-att-class="'o_right_bordered' if row_value_index in [5,10] else '' + ' text-center' if row_value_index != 0 else ''">
|
||||
<t t-if="row_value_index == 0">
|
||||
<span t-if="foldable" t-att-class="('fa fa-caret-down' if row_type == 'sale_order' else 'fa fa-caret-right') + (' project_overview_foldable' if foldable else '')"
|
||||
style="cursor: pointer;" t-att-data-model="row[0].get('res_model')" t-att-data-res-id="row[0].get('res_id')"/>
|
||||
<t t-if="row_type == 'sale_order'">
|
||||
<a type="action" t-att-data-model="row_value['res_model']" t-att-data-res-id="row_value['res_id']" t-att-class="'o_timesheet_plan_redirect' if row_value['res_id'] else ''">
|
||||
<t t-esc="row_value.get('label')"/>
|
||||
</a>
|
||||
<span t-if="row_value.get('canceled')" class="badge badge-pill o_canceled_tag">
|
||||
Cancelled
|
||||
</span>
|
||||
</t>
|
||||
<t t-if="row_type != 'sale_order'">
|
||||
<t t-if="not row_is_milestone">
|
||||
<span><t t-esc="row_value.get('label')"/></span>
|
||||
</t>
|
||||
<t t-if="row_is_milestone">
|
||||
<span><i><t t-esc="row_value.get('label')"/></i></span>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="row_value_index != 0">
|
||||
<t t-if="row_value_index < len(row)-2">
|
||||
<t t-if="row_is_milestone">
|
||||
<i t-att-class="'text-muted' if not row_value else ''">
|
||||
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
|
||||
</i>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-att-class="'text-muted' if not row_value else ''">
|
||||
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="not row_is_milestone and not row[0].get('type') == 'hr_employee'">
|
||||
<span t-att-class="'text-muted' if not row_value else ''">
|
||||
<t t-if="is_uom_day" t-esc="row_value" t-options="{'widget': 'timesheet_uom'}"/>
|
||||
<t t-else="" t-esc="row_value" t-options="{'widget': 'float_time'}"/>
|
||||
</span>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
<t t-if="row_type == 'sale_order_line'">
|
||||
<t t-set="current_order_line" t-value="row[0].get('res_id')"/>
|
||||
</t>
|
||||
<t t-if="row_type == 'sale_order'">
|
||||
<t t-set="current_order" t-value="row[0].get('res_id')"/>
|
||||
</t>
|
||||
</t>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</qweb>
|
||||
<xpath expr="//div[@class='o_timesheet_plan_sale_timesheet_people_time']/t/div/table/thead/tr/th[2]"
|
||||
position="after">
|
||||
<th>Hourly Rate</th>
|
||||
</xpath>
|
||||
<xpath expr="//div[@class='o_timesheet_plan_sale_timesheet_people_time']/t/div/table/tbody/t/tr/td[2]"
|
||||
position="after">
|
||||
<td style="width: 15%; vertical-align: middle;">
|
||||
<t t-esc="employee['employee_price']"/>
|
||||
</td>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
Loading…
Reference in New Issue