diff --git a/addons/hr_expense/models/hr_employee.py b/addons/hr_expense/models/hr_employee.py index c4388789e15..4a2b7dcb0a5 100644 --- a/addons/hr_expense/models/hr_employee.py +++ b/addons/hr_expense/models/hr_employee.py @@ -8,5 +8,5 @@ class Employee(models.Model): expense_manager_id = fields.Many2one( 'res.users', string='Expense Responsible', - domain=lambda self: [('groups_id', 'in', self.env.ref('hr_expense.group_hr_expense_manager').id)], + domain=lambda self: [('groups_id', 'in', self.env.ref('hr_expense.group_hr_expense_user').id)], help="User responsible of expense approval. Should be Expense Manager.") diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index dad6b3261b5..0e5d1339312 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -29,9 +29,25 @@ class HrExpense(models.Model): def _default_account_id(self): return self.env['ir.property'].get('property_account_expense_categ_id', 'product.category') + @api.model + def _get_employee_id_domain(self): + res = [(1, '=', 0)] + + if self.user_has_groups('hr_expense.group_hr_expense_manager') or self.user_has_groups('account.group_account_user'): + res = [(1, '=', 1)] + elif self.user_has_groups('hr_expense.group_hr_expense_user') and self.env.user.employee_ids: + employee = self.env.user.employee_ids[0] + res = ['|', '|', ('department_id.manager_id.id', '=', employee.id), + ('parent_id.id', '=', employee.id), ('expense_manager_id.id', '=', employee.id)] + elif self.env.user.employee_ids: + employee = self.env.user.employee_ids[0] + res = [('id', '=', employee.id)] + + return res + name = fields.Char('Description', readonly=True, required=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}) date = fields.Date(readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, default=fields.Date.context_today, string="Date") - employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_employee_id) + employee_id = fields.Many2one('hr.employee', string="Employee", required=True, readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_employee_id, domain=lambda self: self._employee_filter) product_id = fields.Many2one('product.product', string='Product', readonly=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, domain=[('can_be_expensed', '=', True)], required=True) product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=_default_product_uom_id) unit_amount = fields.Float("Unit Price", readonly=True, required=True, states={'draft': [('readonly', False)], 'reported': [('readonly', False)], 'refused': [('readonly', False)]}, digits=dp.get_precision('Product Price')) @@ -451,7 +467,25 @@ class HrExpense(models.Model): class HrExpenseSheet(models.Model): + """ + Here are the rights associated with the expense flow + Action Group Restriction + ================================================================================= + Submit Employee Only his own + Officer If he is expense manager of the employee, manager of the employee + or the employee is in the department managed by the officer + Manager Always + Approve Officer Not his own and he is expense manager of the employee, manager of the employee + or the employee is in the department managed by the officer + Manager Always + Post Anybody State = approve and journal_id defined + Done Anybody State = approve and journal_id defined + Cancel Officer Not his own and he is expense manager of the employee, manager of the employee + or the employee is in the department managed by the officer + Manager Always + ================================================================================= + """ _name = "hr.expense.sheet" _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Expense Report" @@ -613,7 +647,16 @@ class HrExpenseSheet(models.Model): @api.multi def approve_expense_sheets(self): if not self.user_has_groups('hr_expense.group_hr_expense_user'): - raise UserError(_("Only HR Officers can approve expenses")) + raise UserError(_("Only Managers and HR Officers can approve expenses")) + elif not self.user_has_groups('hr_expense.group_hr_expense_manager'): + current_managers = self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id + + if self.employee_id.user_id == self.env.user: + raise UserError(_("You cannot approve your own expenses")) + + if not self.env.user in current_managers: + raise UserError(_("You can only approve your department expenses")) + responsible_id = self.user_id.id or self.env.user.id self.write({'state': 'approve', 'user_id': responsible_id}) self.activity_update() @@ -625,7 +668,16 @@ class HrExpenseSheet(models.Model): @api.multi def refuse_sheet(self, reason): if not self.user_has_groups('hr_expense.group_hr_expense_user'): - raise UserError(_("Only HR Officers can refuse expenses")) + raise UserError(_("Only Managers and HR Officers can approve expenses")) + elif not self.user_has_groups('hr_expense.group_hr_expense_manager'): + current_managers = self.employee_id.parent_id.user_id | self.employee_id.department_id.manager_id.user_id + + if self.employee_id.user_id == self.env.user: + raise UserError(_("You cannot refuse your own expenses")) + + if not self.env.user in current_managers: + raise UserError(_("You can only refuse your department expenses")) + self.write({'state': 'cancel'}) for sheet in self: sheet.message_post_with_view('hr_expense.hr_expense_template_refuse_reason', values={'reason': reason, 'is_sheet': True, 'name': self.name}) diff --git a/addons/hr_expense/security/ir.model.access.csv b/addons/hr_expense/security/ir.model.access.csv index b0e187d617f..f995ebe78d7 100644 --- a/addons/hr_expense/security/ir.model.access.csv +++ b/addons/hr_expense/security/ir.model.access.csv @@ -1,6 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_hr_expense_user,hr.expense.user,model_hr_expense,hr_expense.group_hr_expense_user,1,1,1,1 access_hr_expense_employee,hr.expense.employee,model_hr_expense,base.group_user,1,1,1,1 +access_hr_expense_sheet_manager,hr.expense.sheet.manager,model_hr_expense_sheet,hr_expense.group_hr_expense_manager,1,1,1,1 access_hr_expense_sheet_user,hr.expense.sheet.user,model_hr_expense_sheet,hr_expense.group_hr_expense_user,1,1,1,1 access_hr_expense_sheet_employee,hr.expense.sheet.employee,model_hr_expense_sheet,base.group_user,1,1,1,1 access_product_product_hr_expense_user,product.product.hr.expense.user,product.model_product_product,hr_expense.group_hr_expense_user,1,1,1,1 diff --git a/addons/hr_expense/security/ir_rule.xml b/addons/hr_expense/security/ir_rule.xml index eba7fe52e1a..5836c97b83a 100644 --- a/addons/hr_expense/security/ir_rule.xml +++ b/addons/hr_expense/security/ir_rule.xml @@ -2,21 +2,56 @@ - + Manager Expense [(1, '=', 1)] + (4, ref('hr_expense.group_hr_expense_manager'))]"/> - + + Officer Expense + + ['|', '|', '|', + ('employee_id.user_id.id', '=', user.id), + ('employee_id.department_id.manager_id.user_id.id', '=', user.id), + ('employee_id.parent_id.user_id.id', '=', user.id), + ('employee_id.expense_manager_id.id', '=', user.id)] + + + Employee Expense [('employee_id.user_id', '=', user.id)] + + + Manager Expense Sheet + + [(1, '=', 1)] + + + + Officer Expense Sheet + + ['|', '|', '|', + ('employee_id.user_id.id', '=', user.id), + ('employee_id.department_id.manager_id.user_id.id', '=', user.id), + ('employee_id.parent_id.user_id.id', '=', user.id), + ('employee_id.expense_manager_id.id', '=', user.id)] + + + + Employee Expense Sheet + + [('employee_id.user_id', '=', user.id)] + + + Expense multi company rule diff --git a/addons/hr_expense/tests/common.py b/addons/hr_expense/tests/common.py index 71a4d33e757..0ff8e27fba0 100644 --- a/addons/hr_expense/tests/common.py +++ b/addons/hr_expense/tests/common.py @@ -21,6 +21,7 @@ class TestExpenseCommon(TestAccountNoChartCommon): # create employee cls.employee = cls.env['hr.employee'].create({ 'name': 'Johnny Employee', + 'user_id': cls.user_employee.id, 'address_home_id': cls.user_employee.partner_id.id, 'address_id': cls.user_employee.partner_id.id, }) @@ -56,3 +57,66 @@ class TestExpenseCommon(TestAccountNoChartCommon): 'employee_id': cls.employee.id, 'journal_id': cls.journal.id, }) + + Users = cls.env['res.users'].with_context(no_reset_password=True) + + # Find Employee group + group_employee_id = cls.env.ref('base.group_user').id + + cls.user_emp2 = Users.create({ + 'name': 'Superboy Employee', + 'login': 'superboy', + 'email': 'superboy@example.com', + 'groups_id': [(6, 0, [group_employee_id])] + }) + + cls.user_officer = Users.create({ + 'name': 'Batman Officer', + 'login': 'batman', + 'email': 'batman.hero@example.com', + 'groups_id': [(6, 0, [group_employee_id, cls.env.ref('hr_expense.group_hr_expense_user').id])] + }) + + cls.emp_emp2 = cls.env['hr.employee'].create({ + 'name': 'Superboy', + 'user_id': cls.user_emp2.id, + }) + + cls.emp_officer = cls.env['hr.employee'].create({ + 'name': 'Batman', + 'user_id': cls.user_officer.id, + }) + + cls.emp_manager = cls.env['hr.employee'].create({ + 'name': 'Superman', + 'user_id': cls.user_manager.id, + }) + + cls.rd = cls.env['hr.department'].create({ + 'name': 'R&D', + 'manager_id': cls.emp_officer.id, + 'member_ids': [(6, 0, [cls.employee.id])], + }) + + cls.ps = cls.env['hr.department'].create({ + 'name': 'PS', + 'manager_id': cls.emp_manager.id, + 'member_ids': [(6, 0, [cls.emp_emp2.id])], + }) + + cls.uom_unit = cls.env.ref('uom.product_uom_unit').id + cls.uom_dozen = cls.env.ref('uom.product_uom_dozen').id + + cls.product_1 = cls.env['product.product'].create({ + 'name': 'Batmobile repair', + 'type': 'service', + 'uom_id': cls.uom_unit, + 'uom_po_id': cls.uom_unit, + }) + + cls.product_2 = cls.env['product.product'].create({ + 'name': 'Superboy costume washing', + 'type': 'service', + 'uom_id': cls.uom_unit, + 'uom_po_id': cls.uom_unit, + }) diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py index 976ba9188ae..8d526b02a36 100644 --- a/addons/hr_expense/tests/test_expenses.py +++ b/addons/hr_expense/tests/test_expenses.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo.exceptions import UserError, AccessError + from odoo.addons.hr_expense.tests.common import TestExpenseCommon @@ -72,3 +74,98 @@ class TestAccountEntry(TestExpenseCommon): self.assertEquals(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line") self.assertAlmostEquals(self.analytic_account.line_ids[0].amount, -636.36, "Amount on the only AAL is wrong") self.assertEquals(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense") + + +class TestExpenseRights(TestExpenseCommon): + + @classmethod + def setUpClass(cls): + super(TestExpenseRights, cls).setUpClass() + + def test_expense_create(self): + # Employee should be able to create an Expense + self.env['hr.expense'].sudo(self.user_employee.id).create({ + 'name': 'Batmobile repair', + 'employee_id': self.employee.id, + 'product_id': self.product_1.id, + 'unit_amount': 1, + 'quantity': 1, + }) + + # Employee should not be able to create an Expense for someone else + with self.assertRaises(AccessError): + self.env['hr.expense'].sudo(self.user_employee.id).create({ + 'name': 'Superboy costume washing', + 'employee_id': self.emp_emp2.id, + 'product_id': self.product_2.id, + 'unit_amount': 1, + 'quantity': 1, + }) + + def test_expense_approve(self): + sheet = self.env['hr.expense.sheet'].create({ + 'name': 'Furnitures', + 'employee_id': self.emp_officer.id, + }) + + sheet_2 = self.env['hr.expense.sheet'].create({ + 'name': 'Services', + 'employee_id': self.employee.id, + }) + + sheet_3 = self.env['hr.expense.sheet'].create({ + 'name': 'Services 2', + 'employee_id': self.emp_emp2.id, + }) + + # Employee should not be able to approve expense sheet + with self.assertRaises(UserError): + sheet.sudo(self.user_officer).approve_expense_sheets() + # Officer should not be able to approve own expense sheet + with self.assertRaises(UserError): + sheet.sudo(self.user_officer).approve_expense_sheets() + sheet.sudo(self.user_manager).approve_expense_sheets() + + # Officer should be able to approve expense from his department + sheet_2.sudo(self.user_officer).approve_expense_sheets() + + # Officer should not be able to approve expense sheet from another department + with self.assertRaises(AccessError): + sheet_3.sudo(self.user_officer).approve_expense_sheets() + sheet_3.sudo(self.user_manager).approve_expense_sheets() + + def test_expense_refuse(self): + sheet = self.env['hr.expense.sheet'].create({ + 'name': 'Furnitures', + 'employee_id': self.emp_officer.id, + }) + + sheet_2 = self.env['hr.expense.sheet'].create({ + 'name': 'Services', + 'employee_id': self.employee.id, + }) + + sheet_3 = self.env['hr.expense.sheet'].create({ + 'name': 'Services 2', + 'employee_id': self.emp_emp2.id, + }) + + sheet.sudo(self.user_manager).approve_expense_sheets() + sheet_2.sudo(self.user_manager).approve_expense_sheets() + sheet_3.sudo(self.user_manager).approve_expense_sheets() + + # Employee should not be able to refuse expense sheet + with self.assertRaises(UserError): + sheet.sudo(self.user_employee).refuse_sheet('') + # Officer should not be able to refuse own expense sheet + with self.assertRaises(UserError): + sheet.sudo(self.user_officer).refuse_sheet('') + sheet.sudo(self.user_manager).refuse_sheet('') + + # Officer should be able to refuse expense from his department + sheet_2.sudo(self.user_officer).refuse_sheet('') + + # Officer should not be able to refuse expense sheet from another department + with self.assertRaises(AccessError): + sheet_3.sudo(self.user_officer).refuse_sheet('') + sheet_3.sudo(self.user_manager).refuse_sheet('') diff --git a/addons/hr_expense/views/hr_expense_views.xml b/addons/hr_expense/views/hr_expense_views.xml index 76134466aef..6176b2057d5 100644 --- a/addons/hr_expense/views/hr_expense_views.xml +++ b/addons/hr_expense/views/hr_expense_views.xml @@ -245,8 +245,7 @@ hr.expense tree,kanban,form,graph,pivot - [('employee_id.user_id', '=', uid)] - {'search_default_to_report': 1} + {'search_default_to_report': 1, 'search_default_my_expenses': 1}

Create a new expense