[IMP] hr_timesheet, sale_timesheet: log timesheet on employee
This commits mainly switches the semantic of timesheeting from users to employees. Main points of this commit are : * add an employee_id field on timesheet; * replace user_id field by employee_id on timesheet views; * provide a stat button on employee form view to access its timesheets; * use timesheet cost as a real monetary field and handle currency conversion; The purpose is to allow timesheet manager to record timesheet for employees that are not users. All employees do not necessarily have an user account in the system. Now you can record timesheet for your employee. Hooray ! In sale_timesheet timesheet_cost is now a monetary field. Currency convertion is also better taken into account when computing UPDATE ME Timesheet details * add an invisible timesheet group on employee form view allowing to hook on it. Indeed sale_timesheet and timesheet_grid modules override this view to add information but have no other common module that hr_timesheet. Using an invisible group set as visible in those modules eases inheritance. Sale timesheet details * timesheet_cost is now a monetary field * better display of timesheet_cost on employee form view, notably telling it is per hour * remove account_id field on employee as its only use was buggy and was mixing account.account and analytic.account records.
This commit is contained in:
committed by
Thibault Delavallée
parent
a806f96fa2
commit
5f997fc2ef
@@ -31,6 +31,7 @@ up a management by affair.
|
||||
'report/report_timesheet_templates.xml',
|
||||
'hr_timesheet_installer.xml',
|
||||
'hr_dashboard.xml',
|
||||
'views/hr_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/hr_timesheet_demo.xml',
|
||||
|
||||
@@ -7,24 +7,38 @@ from odoo import api, fields, models
|
||||
class AccountAnalyticLine(models.Model):
|
||||
_inherit = 'account.analytic.line'
|
||||
|
||||
def _default_employee_id(self):
|
||||
return self.env.user.employee_ids[0]
|
||||
|
||||
task_id = fields.Many2one('project.task', 'Task')
|
||||
project_id = fields.Many2one('project.project', 'Project', domain=[('allow_timesheets', '=', True)])
|
||||
department_id = fields.Many2one('hr.department', "Department", related='user_id.employee_ids.department_id', store=True, readonly=True)
|
||||
|
||||
employee_id = fields.Many2one('hr.employee', "Employee", default=_default_employee_id)
|
||||
department_id = fields.Many2one('hr.department', "Department", related='employee_id.department_id', store=True, readonly=True)
|
||||
|
||||
@api.onchange('project_id')
|
||||
def onchange_project_id(self):
|
||||
self.task_id = False
|
||||
|
||||
@api.onchange('employee_id')
|
||||
def _onchange_employee_id(self):
|
||||
self.user_id = self.employee_id.user_id
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('project_id'):
|
||||
project = self.env['project.project'].browse(vals.get('project_id'))
|
||||
vals['account_id'] = project.analytic_account_id.id
|
||||
vals = self._update_timesheet_values(vals)
|
||||
return super(AccountAnalyticLine, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
vals = self._update_timesheet_values(vals)
|
||||
return super(AccountAnalyticLine, self).write(vals)
|
||||
|
||||
def _update_timesheet_values(self, vals):
|
||||
if vals.get('project_id'):
|
||||
project = self.env['project.project'].browse(vals.get('project_id'))
|
||||
vals['account_id'] = project.analytic_account_id.id
|
||||
return super(AccountAnalyticLine, self).write(vals)
|
||||
if vals.get('employee_id'):
|
||||
employee = self.env['hr.employee'].browse(vals['employee_id'])
|
||||
vals['user_id'] = employee.user_id.id
|
||||
return vals
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='date']" position="after">
|
||||
<field name="user_id" required="1" options='{"no_open": True}'/>
|
||||
<field name="employee_id" required="1" options='{"no_open": True}'/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -47,7 +48,7 @@
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Timesheet">
|
||||
<field name="user_id" type="row"/>
|
||||
<field name="employee_id" type="row"/>
|
||||
<field name="date" interval="month" type="col"/>
|
||||
<field name="unit_amount" type="measure"/>
|
||||
</pivot>
|
||||
@@ -89,7 +90,8 @@
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='company_id']" position="before">
|
||||
<field name="user_id" required="1" options='{"no_open": True}'/>
|
||||
<field name="employee_id" required="1" options='{"no_open": True}'/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
@@ -98,7 +100,7 @@
|
||||
<field name="name">account.analytic.line.calendar</field>
|
||||
<field name="model">account.analytic.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<calendar string="Timesheets" date_start="date" color="user_id">
|
||||
<calendar string="Timesheets" date_start="date" color="employee_id">
|
||||
<field name="name"/>
|
||||
<field name="project_id"/>
|
||||
</calendar>
|
||||
@@ -111,7 +113,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<search string="Timesheet">
|
||||
<field name="date"/>
|
||||
<field name="user_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="project_id"/>
|
||||
<field name="task_id"/>
|
||||
<field name="name"/>
|
||||
@@ -130,7 +132,7 @@
|
||||
('date', '<=', (context_today() + relativedelta(months=1, day=1, days=-1)).strftime('%Y-%m-%d')),
|
||||
]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Users" name="groupby_user" domain="[]" context="{'group_by':'user_id'}"/>
|
||||
<filter string="Employee" name="groupby_employee" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||
<filter string="Project" name="groupby_project" domain="[]" context="{'group_by':'project_id'}"/>
|
||||
<filter string="Task" name="groupby_task" domain="[]" context="{'group_by':'task_id'}"/>
|
||||
<filter string="Timesheet Month" name="groupby_date" domain="[]" context="{'group_by':'date'}" help="Timesheet by Month"/>
|
||||
@@ -191,7 +193,7 @@
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="search_view_id" ref="hr_timesheet_line_search"/>
|
||||
<field name="domain">[('project_id', '!=', False)]</field>
|
||||
<field name="context">{'search_default_week':1, 'search_default_groupby_user':1}</field>
|
||||
<field name="context">{'search_default_week':1, 'search_default_groupby_employee':1}</field>
|
||||
</record>
|
||||
|
||||
<record id="timesheet_action_view_all_tree" model="ir.actions.act_window.view">
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
<field name="timesheet_ids" context="{'default_project_id': project_id}">
|
||||
<tree editable="bottom" string="Timesheet Activities" default_order="date">
|
||||
<field name="date"/>
|
||||
<field name="user_id" required="1"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="employee_id" required="1"/>
|
||||
<field name="name"/>
|
||||
<field name="unit_amount" string="Duration" widget="float_time"/>
|
||||
<field name="project_id" invisible="1"/>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="timesheet_action_from_employee" model="ir.actions.act_window">
|
||||
<field name="name">Timesheets</field>
|
||||
<field name="res_model">account.analytic.line</field>
|
||||
<field name="search_view_id" ref="hr_timesheet_line_search"/>
|
||||
<field name="domain">[('project_id', '!=', False)]</field>
|
||||
<field name="context">{
|
||||
'search_default_month':1,
|
||||
'search_default_employee_id': [active_id],
|
||||
'default_employee_id': active_id
|
||||
}</field>
|
||||
</record>
|
||||
|
||||
<record id="hr_employee_view_form_inherit_timesheet" model="ir.ui.view">
|
||||
<field name="name">hr.employee.form.timesheet</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='active_group']" position="before">
|
||||
<!-- Empty invisible group to be filled by timesheet_grid and sale_timesheet -->
|
||||
<group string="Timesheets" name="timesheet" invisible="1">
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button class="oe_stat_button" type="action" name="%(timesheet_action_from_employee)d" icon="fa-calendar">
|
||||
<div class="o_stat_info">
|
||||
<span class="o_stat_text">Timesheets</span>
|
||||
</div>
|
||||
</button>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -37,16 +37,28 @@ class AccountAnalyticLine(models.Model):
|
||||
if values.get('amount'):
|
||||
return {}
|
||||
unit_amount = values.get('unit_amount', 0.0) or self.unit_amount
|
||||
user_id = values.get('user_id') or self.user_id.id or self._default_user()
|
||||
user = self.env['res.users'].browse([user_id])
|
||||
emp = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1)
|
||||
cost = emp and emp.timesheet_cost or 0.0
|
||||
uom = (emp or user).company_id.project_time_mode_id
|
||||
# Nominal employee cost = 1 * company project UoM (project_time_mode_id)
|
||||
|
||||
# find the employee
|
||||
employee_id = values.get('employee_id') or self.employee_id.id or self._default_employee_id().id
|
||||
employee = self.env['hr.employee'].browse(employee_id) if employee_id else self.env['hr.employee']
|
||||
if not employee:
|
||||
user_id = values.get('user_id') or self.user_id.id or self._default_user()
|
||||
employee = self.env['hr.employee'].search([('user_id', '=', user_id)], limit=1)
|
||||
|
||||
# find the analytic account
|
||||
account_id = values.get('account_id') or self.account_id.id
|
||||
analytic_account = self.env['account.analytic.account'].browse(account_id) if account_id else self.env['account.analytic.account']
|
||||
|
||||
# convert employee cost into timesheet (analytic account) currency
|
||||
uom = employee.company_id.project_time_mode_id
|
||||
cost = employee.timesheet_cost or 0.0
|
||||
amount = -unit_amount * cost
|
||||
amount_converted = employee.currency_id.compute(amount, analytic_account.currency_id)
|
||||
|
||||
return {
|
||||
'amount': -unit_amount * cost,
|
||||
'amount': amount_converted,
|
||||
'product_uom_id': uom.id,
|
||||
'account_id': values.get('account_id') or self.account_id.id or emp.account_id.id,
|
||||
'account_id': account_id,
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@ class HrEmployee(models.Model):
|
||||
_inherit = 'hr.employee'
|
||||
|
||||
# FIXME: this field should be in module hr_timesheet, not sale_timesheet
|
||||
timesheet_cost = fields.Float('Timesheet Cost', default=0.0)
|
||||
account_id = fields.Many2one('account.account', string='Account')
|
||||
timesheet_cost = fields.Monetary('Timesheet Cost', currency_field='currency_id', default=0.0)
|
||||
currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True)
|
||||
|
||||
@@ -6,6 +6,19 @@ from odoo.addons.sale.tests.test_sale_common import TestSale
|
||||
|
||||
|
||||
class TestSaleTimesheet(TestSale):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSaleTimesheet, self).setUp()
|
||||
# link hr employee to res.users of sale tests
|
||||
self.employee_user = self.env['hr.employee'].create({
|
||||
'name': 'Employee User',
|
||||
'user_id': self.user.id,
|
||||
})
|
||||
self.employee_manager = self.env['hr.employee'].create({
|
||||
'name': 'Employee Manager',
|
||||
'user_id': self.manager.id,
|
||||
})
|
||||
|
||||
def test_timesheet_order(self):
|
||||
""" Test timesheet invoicing with 'invoice on order' timetracked products """
|
||||
# intial so
|
||||
@@ -26,7 +39,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 10.5,
|
||||
'user_id': self.manager.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(so.order_line.qty_delivered, 10.5, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
|
||||
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
@@ -35,7 +48,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 39.5,
|
||||
'user_id': self.user.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(so.order_line.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
|
||||
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
@@ -44,7 +57,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 10,
|
||||
'user_id': self.user.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(so.order_line.qty_delivered, 60, 'Sale Timesheet: timesheet does not increase delivered quantity on so line')
|
||||
self.assertEqual(so.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so')
|
||||
@@ -69,7 +82,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 10.5,
|
||||
'user_id': self.manager.id,
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged')
|
||||
inv_id = so.action_invoice_create()
|
||||
@@ -80,7 +93,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 39.5,
|
||||
'user_id': self.user.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
so.action_invoice_create()
|
||||
@@ -90,7 +103,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 10,
|
||||
'user_id': self.user.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
self.assertEqual(so.invoice_status, 'to invoice', 'Sale Timesheet: supplementary timesheets do not change the status of the SO')
|
||||
|
||||
@@ -113,7 +126,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 16,
|
||||
'user_id': self.manager.id,
|
||||
'employee_id': self.employee_manager.id,
|
||||
})
|
||||
self.assertEqual(so.order_line.qty_delivered, 2, 'Sale: uom conversion of timesheets is wrong')
|
||||
|
||||
@@ -121,7 +134,7 @@ class TestSaleTimesheet(TestSale):
|
||||
'name': 'Test Line',
|
||||
'project_id': so.project_project_id.id,
|
||||
'unit_amount': 24,
|
||||
'user_id': self.user.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
})
|
||||
so.action_invoice_create()
|
||||
self.assertEqual(so.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so')
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
<record id="hr_timesheet_employee_extd_form" model="ir.ui.view">
|
||||
<field name="name">hr.timesheet.employee.extd_form</field>
|
||||
<field name="model">hr.employee</field>
|
||||
<field name="inherit_id" ref="hr.view_employee_form"/>
|
||||
<field name="inherit_id" ref="hr_timesheet.hr_employee_view_form_inherit_timesheet"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='active_group']" position="before">
|
||||
<group string="Timesheets">
|
||||
<field name="timesheet_cost"/>
|
||||
<field name="account_id"/>
|
||||
</group>
|
||||
<xpath expr="//group[@name='timesheet']" position="inside">
|
||||
<label for="timesheet_cost"/>
|
||||
<div>
|
||||
<field name="timesheet_cost" class="oe_inline"/> per hour
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='timesheet']" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
Reference in New Issue
Block a user