diff --git a/hr_expense_usability/README.rst b/hr_expense_usability/README.rst index a146e84..9a90de5 100644 --- a/hr_expense_usability/README.rst +++ b/hr_expense_usability/README.rst @@ -1,9 +1,13 @@ +==================== HR Expense Usability ==================== -This module adds a few usability enhancements to the official Expense modules: +This module adds a many usability enhancements and new features to the official Expense modules: -* Re-organise access rights: Officer can see the expense notes of his subordinates ; Manager can see all expense notes +* support for Private car expenses (frais kilométriques selon barème fiscal), +* remove support for *Payment by Company* +* TODO: multi-currency fixes +* TODO: full re-implementation of the account.move Credits ======= diff --git a/hr_expense_usability/__init__.py b/hr_expense_usability/__init__.py index 40a96af..8899c78 100644 --- a/hr_expense_usability/__init__.py +++ b/hr_expense_usability/__init__.py @@ -1 +1,3 @@ # -*- coding: utf-8 -*- + +from . import hr_expense diff --git a/hr_expense_usability/__manifest__.py b/hr_expense_usability/__manifest__.py index e3cf181..568ebea 100644 --- a/hr_expense_usability/__manifest__.py +++ b/hr_expense_usability/__manifest__.py @@ -1,38 +1,28 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# HR Expense Usability module for Odoo -# Copyright (C) 2015 Akretion (http://www.akretion.com) -# @author Alexis de Lattre -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - +# © 2015-2017 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'HR Expense Usability', - 'version': '0.1', + 'version': '10.0.1.0.0', 'category': 'Human Resources', 'license': 'AGPL-3', 'summary': 'Better usability for the management of expenses', 'description': '', 'author': 'Akretion', 'website': 'http://www.akretion.com', - 'depends': ['hr_expense'], + 'depends': [ + 'hr_expense', + 'hr_expense_sequence', + ], 'data': [ + 'private_car_data.xml', + 'hr_employee_view.xml', + 'hr_expense_view.xml', + 'product_view.xml', 'security/expense_security.xml', ], - 'installable': False, + 'demo': ['private_car_demo.xml'], + 'installable': True, } diff --git a/hr_expense_usability/hr_employee_view.xml b/hr_expense_usability/hr_employee_view.xml new file mode 100644 index 0000000..e50d3b3 --- /dev/null +++ b/hr_expense_usability/hr_employee_view.xml @@ -0,0 +1,26 @@ + + + + + + + private.car.employee.form + hr.employee + + + + + + + + + + + + + diff --git a/hr_expense_usability/hr_expense.py b/hr_expense_usability/hr_expense.py new file mode 100644 index 0000000..4abc5f8 --- /dev/null +++ b/hr_expense_usability/hr_expense.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- +# © 2014-2017 Akretion (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import float_compare + + +# I had to choose between several ideas when I developped this module : +# 1) constraint on product_id in expense line +# Idea : we put a constraint on the field product_id of the expense line +# and, if it's a private_car_expense_ok=True product but it's not the private +# car expense product of the employee, we block +# Drawback : not convenient for the employee because he has to select the +# right private car expense product by himself + +# 2) single product, dedicated object for prices +# Idea : we create only one "private car expense" product, and we +# create a new object to store the price depending on the CV, etc... +# Drawback : need to create a new object + +# 3) single generic "My private car" product selectable by the user ; +# several specific private car products NOT selectable by the user +# Idea : When the user selects the generic "My private car" product, +# it is automatically replaced by the specific one via the on_change +# Drawback : none ? :) +# => that's what is implemented in this module + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + private_car_expense_ok = fields.Boolean( + string='Private Car Expense', track_visibility='onchange') + + @api.onchange('private_car_expense_ok') + def onchange_private_car_expense_ok(self): + if self.private_car_expense_ok: + km_uom = self.env.ref('product.product_uom_km') + self.type = 'service' + self.list_price = 0.0 + self.can_be_expensed = False + self.sale_ok = False + self.purchase_ok = False + self.uom_id = km_uom.id + self.po_uom_id = km_uom.id + self.taxes_id = False + self.supplier_taxes_id = False + + @api.constrains( + 'private_car_expense_ok', 'can_be_expensed', 'uom_id', + 'standard_price') + def _check_private_car_expense(self): + for product in self: + if product.private_car_expense_ok: + if product.can_be_expensed: + raise ValidationError(_( + "The product '%s' cannot have both the properties " + "'Can be Expensed' and 'Private Car Expense'.") + % product.display_name) + km_uom = self.env.ref('product.product_uom_km') + if product.uom_id != km_uom: + raise ValidationError(_( + "The product '%s' is a Private Car Expense, so " + "it's unit of measure must be kilometers (KM).") + % product.display_name) + + +class HrEmployee(models.Model): + _inherit = 'hr.employee' + + def compute_private_car_total_km_this_year(self): + print "compute_private_car_total_km_this_year self=", self + res = {} + private_car_products = self.env['product.product'].search( + [('private_car_expense_ok', '=', True)]) + today = fields.Date.context_today(self) + today_dt = fields.Date.from_string(today) + self._cr.execute( + """ + SELECT el.employee_id, sum(el.quantity) + FROM hr_expense el + WHERE el.state NOT IN ('draft', 'cancel') + AND el.employee_id IN %s + AND el.product_id IN %s + AND EXTRACT(year FROM el.date) = %s + GROUP BY el.employee_id + """, + (tuple(self.ids), tuple(private_car_products.ids), today_dt.year)) + for line in self._cr.dictfetchall(): + res[line['employee_id']] = line['sum'] + for empl in self: + empl.private_car_total_km_this_year = res.get(empl.id) or 0.0 + + private_car_plate = fields.Char( + 'Private Car Plate', size=32, copy=False, track_visibility='onchange', + help="This field will be copied on the expenses of this employee.") + private_car_product_id = fields.Many2one( + 'product.product', string='Private Car Product', copy=False, + domain=[('private_car_expense_ok', '=', True)], + ondelete='restrict', track_visibility='onchange', + help="This field will be copied on the expenses of this employee.") + private_car_total_km_this_year = fields.Float( + compute='compute_private_car_total_km_this_year', + string="Total KM with Private Car This Year", readonly=True, + help="Number of kilometers (KM) with private car for this " + "employee in expenses in Approved, Waiting Payment or Paid " + "state in the current civil year. This is usefull to check or " + "estimate if the Private Car Product selected for this " + "employee is compatible with the number of kilometers " + "reimbursed to this employee during the civil year.") + + +class HrExpense(models.Model): + _inherit = 'hr.expense' + + private_car_plate = fields.Char( + string='Private Car Plate', size=32, readonly=True, + track_visibility='onchange', + states={'draft': [('readonly', False)]}) + private_car_expense = fields.Boolean( + related='product_id.private_car_expense_ok', readonly=True, store=True) + + # as private_car_plate id readonly, we have to inherit create() to set it + + @api.onchange('product_id') + def _onchange_product_id(self): + if ( + self.product_id and self.product_id == self.env.ref( + 'hr_expense_usability.generic_private_car_expense')): + if not self.employee_id.private_car_product_id: + raise UserError(_( + "Missing Private Car Product on the configuration of " + "the employee '%s'.") % self.employee_id.display_name) + if not self.employee_id.private_car_plate: + raise UserError(_( + "Missing Private Car Plate on the configuration of " + "the employee '%s'.") % self.employee_id.display_name) + self.product_id = self.employee_id.private_car_product_id + self.private_car_plate = self.employee_id.private_car_plate + return super(HrExpense, self)._onchange_product_id() + + @api.onchange('unit_amount') + def _onchange_unit_amount(self): + res = {} + if self.product_id.private_car_expense_ok: + original_unit_amount = self.product_id.price_compute( + 'standard_price')[self.product_id.id] + prec = self.env['decimal.precision'].precision_get('Product Price') + if float_compare( + original_unit_amount, self.unit_amount, + precision_digits=prec): + if self.env.user.has_group('account.group_account_manager'): + res['warning'] = { + 'title': _('Warning - Private Car Expense'), + 'message': _( + "You should not change the unit price " + "for private car expenses. You should change " + "the Private Car Product or update the Cost " + "Price of the selected Private Car Product " + "and re-create the Expense.\n\nBut, as " + "you are in the group 'Account Manager', we " + "suppose that you know what you are doing, " + "so the original unit amount (%s) is not " + "restored.") % original_unit_amount, + } + else: + res['warning'] = { + 'title': _('Warning - Private Car Expense'), + 'message': _( + "You should not change the unit price " + "for private car expenses. The original unit " + "amount has been restored.\n\nOnly users in " + "the 'Account Manager' group are allowed to " + "change the unit amount for private car " + "expenses manually.")} + res['value'] = {'unit_amount': original_unit_amount} + return res + + @api.constrains('product_id') + def _check_private_car(self): + generic_private_car_product = self.env.ref( + 'hr_expense_usability.generic_private_car_expense') + for exp in self: + if exp.product_id == generic_private_car_product: + raise ValidationError(_( + "You are trying to save the expense '%s' " + "with the generic product '%s': it is not possible, " + "this product should have been automatically replaced " + "by the specific private car product configured for " + "the employee '%s'.") % ( + exp.name, + generic_private_car_product.name, + exp.employee_id.display_name)) + if ( + exp.product_id.private_car_expense_ok and + not exp.private_car_plate): + raise ValidationError(_( + "Missing 'Private Car Plate' on the " + "expense '%s' of employee '%s'.") + % (exp.name, exp.employee_id.display_name)) diff --git a/hr_expense_usability/hr_expense_view.xml b/hr_expense_usability/hr_expense_view.xml new file mode 100644 index 0000000..2aa43db --- /dev/null +++ b/hr_expense_usability/hr_expense_view.xml @@ -0,0 +1,41 @@ + + + + + + + + usability.hr.expense.form + hr.expense + + + + + + + + + 1 + + + + + + usability.hr.expense.sheet.form + hr.expense.sheet + + + + 1 + + + + + + diff --git a/hr_expense_usability/private_car_data.xml b/hr_expense_usability/private_car_data.xml new file mode 100644 index 0000000..1d3deb9 --- /dev/null +++ b/hr_expense_usability/private_car_data.xml @@ -0,0 +1,281 @@ + + + + + + + My Private Car Expense (in km) + PrivateCarExp + + + + + service + 0 + + + + + + + + 1-3CV 0-5000km Private Car Expenses + 1-3CV/0-5000km + + + + + + service + 0 + 0.41 + + + + + + + + 4CV 0-5000km Private Car Expenses + 4CV/0-5000km + + + + + + service + 0 + 0.493 + + + + + + + + 5CV 0-5000km Private Car Expenses + 5CV/0-5000km + + + + + + service + 0 + 0.543 + + + + + + + + 6CV 0-5000km Private Car Expenses + 6CV/0-5000km + + + + + + service + 0 + 0.568 + + + + + + + + 7CV+ 0-5000km Private Car Expenses + 7CV+/0-5000km + + + + + + service + 0 + 0.595 + + + + + + + + 1-3CV 5-20000km Private Car Expenses + 1-3CV/5-20000km + + + + + + service + 0 + 0.245 + + + + + + + + 4CV 5-20000km Private Car Expenses + 4CV/5-20000km + + + + + + service + 0 + 0.277 + + + + + + + + 5CV 5-20000km Private Car Expenses + 5CV/5-20000km + + + + + + service + 0 + 0.305 + + + + + + + + 6CV 5-20000km Private Car Expenses + 6CV/5-20000km + + + + + + service + 0 + 0.32 + + + + + + + + 7CV+ 5-20000km Private Car Expenses + 7CV+/5-20000km + + + + + + service + 0 + 0.337 + + + + + + + + 1-3CV 20000km+ Private Car Expenses + 1-3CV/20000km+ + + + + + + service + 0 + 0.286 + + + + + + + + 4CV 20000km+ Private Car Expenses + 4CV/20000km+ + + + + + + service + 0 + 0.332 + + + + + + + + 5CV 20000km+ Private Car Expenses + 5CV/20000km+ + + + + + + service + 0 + 0.364 + + + + + + + + 6CV 20000km+ Private Car Expenses + 6CV/20000km+ + + + + + + service + 0 + 0.382 + + + + + + + + 7CV+ 20000km+ Private Car Expenses + 7CV+/20000km+ + + + + + + service + 0 + 0.401 + + + + + + + + diff --git a/hr_expense_usability/private_car_demo.xml b/hr_expense_usability/private_car_demo.xml new file mode 100644 index 0000000..596eb7d --- /dev/null +++ b/hr_expense_usability/private_car_demo.xml @@ -0,0 +1,30 @@ + + + + + + + OD 4212 OO + + + + + OE 1234 EO + + + + + BE 6543 AL + + + + + BE 1235 QD + + + + diff --git a/hr_expense_usability/product_view.xml b/hr_expense_usability/product_view.xml new file mode 100644 index 0000000..1642eaa --- /dev/null +++ b/hr_expense_usability/product_view.xml @@ -0,0 +1,47 @@ + + + + + + private.car.expense.product.template.form + product.template + + +
+
+ +
+
+
+
+ + + private.car.expense.product.template.search + product.template + + + + + + + + + + + Private Car Expenses + product.product + tree,form,kanban + {'default_private_car_expense_ok': 1, 'search_default_private_car_expense_ok': 1} + + + + +
diff --git a/hr_expense_usability/security/expense_security.xml b/hr_expense_usability/security/expense_security.xml index da78ed2..51d161a 100644 --- a/hr_expense_usability/security/expense_security.xml +++ b/hr_expense_usability/security/expense_security.xml @@ -1,24 +1,19 @@ - - + - - - - - + + Employee Expense Sheets + + [('employee_id.user_id', '=', user.id)] + - - HR Officer can see expenses of his subordinates - - ['|', ('employee_id.user_id','=',user.id), ('employee_id','child_of',user.employee_ids.ids)] - + + Manager Expense Sheets + + [(1, '=', 1)] + - - + +