From 5f997fc2ef8e3111ec6b0622dee8286e6de3b21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9rome=20Maes?= Date: Wed, 15 Mar 2017 16:31:00 +0100 Subject: [PATCH] [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. --- addons/hr_timesheet/__manifest__.py | 1 + addons/hr_timesheet/hr_timesheet.py | 24 ++++++++++--- addons/hr_timesheet/hr_timesheet_view.xml | 16 +++++---- .../hr_timesheet/project_timesheet_view.xml | 3 +- addons/hr_timesheet/views/hr_views.xml | 36 +++++++++++++++++++ addons/sale_timesheet/models/account.py | 28 ++++++++++----- addons/sale_timesheet/models/hr.py | 4 +-- .../tests/test_sale_timesheet.py | 29 ++++++++++----- addons/sale_timesheet/views/hr_views.xml | 16 +++++---- 9 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 addons/hr_timesheet/views/hr_views.xml diff --git a/addons/hr_timesheet/__manifest__.py b/addons/hr_timesheet/__manifest__.py index efedc7384f0..177797d13a0 100644 --- a/addons/hr_timesheet/__manifest__.py +++ b/addons/hr_timesheet/__manifest__.py @@ -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', diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py index c8b7241e5c5..3c3cdecff9e 100644 --- a/addons/hr_timesheet/hr_timesheet.py +++ b/addons/hr_timesheet/hr_timesheet.py @@ -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 diff --git a/addons/hr_timesheet/hr_timesheet_view.xml b/addons/hr_timesheet/hr_timesheet_view.xml index 95f3d8617d0..d2bac3c2ac0 100644 --- a/addons/hr_timesheet/hr_timesheet_view.xml +++ b/addons/hr_timesheet/hr_timesheet_view.xml @@ -37,7 +37,8 @@ 10 - + + @@ -47,7 +48,7 @@ account.analytic.line - + @@ -89,7 +90,8 @@ 10 - + + @@ -98,7 +100,7 @@ account.analytic.line.calendar account.analytic.line - + @@ -111,7 +113,7 @@ - + @@ -130,7 +132,7 @@ ('date', '<=', (context_today() + relativedelta(months=1, day=1, days=-1)).strftime('%Y-%m-%d')), ]"/> - + @@ -191,7 +193,7 @@ account.analytic.line [('project_id', '!=', False)] - {'search_default_week':1, 'search_default_groupby_user':1} + {'search_default_week':1, 'search_default_groupby_employee':1} diff --git a/addons/hr_timesheet/project_timesheet_view.xml b/addons/hr_timesheet/project_timesheet_view.xml index b7f1b87bab4..f799cc5b76e 100644 --- a/addons/hr_timesheet/project_timesheet_view.xml +++ b/addons/hr_timesheet/project_timesheet_view.xml @@ -102,7 +102,8 @@ - + + diff --git a/addons/hr_timesheet/views/hr_views.xml b/addons/hr_timesheet/views/hr_views.xml new file mode 100644 index 00000000000..6d500f22fa4 --- /dev/null +++ b/addons/hr_timesheet/views/hr_views.xml @@ -0,0 +1,36 @@ + + + + + Timesheets + account.analytic.line + + [('project_id', '!=', False)] + { + 'search_default_month':1, + 'search_default_employee_id': [active_id], + 'default_employee_id': active_id + } + + + + hr.employee.form.timesheet + hr.employee + + + + + + + + + + + + + + diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py index 812b9b09b1b..7f319ead699 100644 --- a/addons/sale_timesheet/models/account.py +++ b/addons/sale_timesheet/models/account.py @@ -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 {} diff --git a/addons/sale_timesheet/models/hr.py b/addons/sale_timesheet/models/hr.py index 5964b77ab04..a9f94f57bb9 100644 --- a/addons/sale_timesheet/models/hr.py +++ b/addons/sale_timesheet/models/hr.py @@ -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) diff --git a/addons/sale_timesheet/tests/test_sale_timesheet.py b/addons/sale_timesheet/tests/test_sale_timesheet.py index 309650aee2f..d75991411cb 100644 --- a/addons/sale_timesheet/tests/test_sale_timesheet.py +++ b/addons/sale_timesheet/tests/test_sale_timesheet.py @@ -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') diff --git a/addons/sale_timesheet/views/hr_views.xml b/addons/sale_timesheet/views/hr_views.xml index 7b17e2dc67e..ce032cc01e0 100644 --- a/addons/sale_timesheet/views/hr_views.xml +++ b/addons/sale_timesheet/views/hr_views.xml @@ -3,13 +3,17 @@ hr.timesheet.employee.extd_form hr.employee - + - - - - - + + + + 0