[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:
Jérome Maes
2017-04-27 10:34:51 +02:00
committed by Thibault Delavallée
parent a806f96fa2
commit 5f997fc2ef
9 changed files with 120 additions and 37 deletions
+1
View File
@@ -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',
+19 -5
View File
@@ -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
+9 -7
View File
@@ -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', '&lt;=', (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"/>
+36
View File
@@ -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>
+20 -8
View File
@@ -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 {}
+2 -2
View File
@@ -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')
+10 -6
View File
@@ -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>