[10.0] extract hr_expense_private_car (#36)

* remove private car stuff from hr_expense_usability

* hr_private_car_expenses
This commit is contained in:
Stéphane Bidoul (ACSONE)
2017-06-23 16:55:01 +02:00
committed by Alexis de Lattre
parent 19d2614feb
commit a9d8137d1f
24 changed files with 281 additions and 201 deletions

View File

@@ -4,7 +4,6 @@ HR Expense Usability
This module adds a many usability enhancements and new features to the official Expense modules:
* support for Private car expenses (frais kilométriques selon barème fiscal),
* remove support for *Payment by Company*
* multi-currency fixes (cf https://github.com/odoo/odoo/issues/17341)
* full re-implementation of the generation of the account.move (by default, it generates 1 move per expense line ; with this module, it generates 1 move per expense report and move lines can be grouped)

View File

@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import hr_expense
from .post_install import create_private_car_km_prices

View File

@@ -17,15 +17,9 @@
'hr_expense_usability_dp',
],
'data': [
'hr_expense_data.xml',
'hr_employee_view.xml',
'hr_expense_view.xml',
'product_view.xml',
'private_car_km_price_view.xml',
'security/expense_security.xml',
'security/ir.model.access.csv',
],
'post_init_hook': 'create_private_car_km_prices',
'demo': ['private_car_demo.xml'],
'installable': True,
}

View File

@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2014-2017 Akretion (http://www.akretion.com)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="view_employee_form" model="ir.ui.view">
<field name="name">private.car.employee.form</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<page name="hr_settings" position="inside">
<group name="private_car_expenses" string="Private Car Expenses">
<field name="private_car_plate"/>
<field name="private_car_km_price_id"/>
<field name="private_car_total_km_this_year"/>
</group>
</page>
</field>
</record>
</odoo>

View File

@@ -9,27 +9,6 @@ from odoo.tools import float_compare, float_is_zero
import odoo.addons.decimal_precision as dp
# 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
# => that's what is implemented in this module
# 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 : decimal precision 'Product Price' on standard_price of product
# (but we need 3)
class ProductTemplate(models.Model):
_inherit = 'product.template'
@@ -84,64 +63,9 @@ class ProductProduct(models.Model):
self.taxes_id = False
class PrivateCarKmPrice(models.Model):
_name = 'private.car.km.price'
_description = 'Private Car Kilometer Price'
_order = 'name'
name = fields.Char(required=True)
unit_amount = fields.Float(
string='Price per KM', digits=dp.get_precision('Expense Unit Price'),
help='Price per kilometer in company currency.')
company_id = fields.Many2one(
'res.company', string='Company',
default=lambda self: self.env['res.company']._company_default_get(
'private.car.km.price'))
active = fields.Boolean(default=True)
class HrEmployee(models.Model):
_inherit = 'hr.employee'
def compute_private_car_total_km_this_year(self):
res = {}
private_car_product_id = self.env.ref(
'hr_expense_usability.generic_private_car_expense').id
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=%s
AND EXTRACT(year FROM el.date) = %s
GROUP BY el.employee_id
""",
(tuple(self.ids), private_car_product_id, 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_km_price_id = fields.Many2one(
'private.car.km.price', string='Private Car Price', copy=False,
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.")
def _get_accounting_partner_from_employee(self):
# By default, odoo uses self.employee_id.address_home_id
# which users usually don't configure
@@ -172,15 +96,6 @@ class HrExpense(models.Model):
# I want a specific precision for unit_amount of expense
# main reason is KM cost which is 3 by default
unit_amount = fields.Float(digits=dp.get_precision('Expense Unit Price'))
private_car_plate = fields.Char(
string='Private Car Plate', size=32, track_visibility='onchange',
readonly=True, states={'draft': [('readonly', False)]})
private_car_km_price_id = fields.Many2one(
'private.car.km.price', string='Private Car Price', copy=False,
ondelete='restrict', track_visibility='onchange',
help="This field will be copied on the expenses of this employee.")
# only for field visibility
private_car_expense = fields.Boolean()
tax_amount = fields.Monetary(
string='Tax Amount', currency_field='currency_id',
readonly=True, states={'draft': [('readonly', False)]})
@@ -241,106 +156,11 @@ class HrExpense(models.Model):
self.untaxed_amount_usability = total
self.tax_amount = False
@api.onchange('product_id')
def _onchange_product_id(self):
private_car_product = self.env.ref(
'hr_expense_usability.generic_private_car_expense')
if (
self.product_id and
self.product_id == private_car_product and
self.employee_id):
if not self.employee_id.private_car_km_price_id:
raise UserError(_(
"Missing Private Car Km Price 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.private_car_expense = True
self.currency_id = self.company_id.currency_id
self.private_car_plate = self.employee_id.private_car_plate
self.private_car_km_price_id =\
self.employee_id.private_car_km_price_id
else:
self.private_car_expense = False
self.private_car_plate = False
self.private_car_km_price_id = False
return super(HrExpense, self)._onchange_product_id()
@api.onchange('private_car_km_price_id')
def _onchange_private_car_km_price_id(self):
if self.private_car_km_price_id and self.employee_id:
self.unit_amount =\
self.employee_id.private_car_km_price_id.unit_amount
@api.onchange('unit_amount')
def _onchange_unit_amount(self):
res = {}
if self.private_car_expense:
original_unit_amount = self.private_car_km_price_id.unit_amount
prec = self.env['decimal.precision'].precision_get(
'Expense Unit 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', 'private_car_plate', 'payment_mode', 'tax_ids',
'product_id', 'payment_mode', 'tax_ids',
'untaxed_amount_usability', 'tax_amount', 'quantity', 'unit_amount')
def _check_expense(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:
if 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))
if not exp.private_car_km_price_id:
raise ValidationError(_(
"Missing 'Private Car Km Price' on the "
"expense '%s'.") % exp.name)
if exp.currency_id != exp.company_id.currency_id:
raise ValidationError(_(
"The expense '%s' is a private car expense, "
"so the currency of this expense (%s) should "
"be the currency of the company (%s).") % (
exp.name,
exp.currency_id.name,
exp.company_id.currency_id.name))
if exp.tax_ids:
raise ValidationError(_(
"The expense '%s' is a private car expense "
"so it shouldn't have taxes.")
% exp.name)
if exp.tax_ids:
if len(exp.tax_ids) > 1:
raise ValidationError(_(

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2014-2017 Akretion (http://www.akretion.com)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="generic_private_car_expense" model="product.product">
<field name="name">My Private Car Expense (in km)</field>
<field name="default_code">PrivateCarExp</field>
<field name="categ_id" ref="hr_expense.cat_expense"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="can_be_expensed" eval="True"/>
<field name="type">service</field>
<field name="list_price">0</field>
<field name="uom_id" ref="product.product_uom_km"/>
<field name="uom_po_id" ref="product.product_uom_km"/>
<field name="taxes_id" eval="False"/>
<field name="supplier_taxes_id" eval="False"/>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -13,11 +13,6 @@
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_form_view"/>
<field name="arch" type="xml">
<field name="reference" position="after">
<field name="private_car_expense" invisible="1"/>
<field name="private_car_plate" attrs="{'invisible': [('private_car_expense', '!=', True)]}"/>
<field name="private_car_km_price_id" attrs="{'invisible': [('private_car_expense', '!=', True)]}" groups="hr_expense.group_hr_expense_manager,account.group_account_manager"/>
</field>
<label for="payment_mode" position="attributes">
<attribute name="invisible">1</attribute>
</label>

View File

@@ -1,42 +0,0 @@
# -*- coding: utf-8 -*-
# © 2017 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, SUPERUSER_ID
KM_PRICES = {
'FR': [
{'name': u"[FR] 1-3 CV, < 5 000 km", 'unit_amount': 0.41},
{'name': u"[FR] 4 CV, < 5 000 km", 'unit_amount': 0.493},
{'name': u"[FR] 5 CV, < 5 000 km", 'unit_amount': 0.543},
{'name': u"[FR] 6 CV, < 5 000 km", 'unit_amount': 0.568},
{'name': u"[FR] 7+ CV, < 5 000 km", 'unit_amount': 0.595},
{'name': u"[FR] 1-3 CV, 5-20 000 km", 'unit_amount': 0.245},
{'name': u"[FR] 4 CV, 5-20 000 km", 'unit_amount': 0.277},
{'name': u"[FR] 5 CV, 5-20 000 km", 'unit_amount': 0.305},
{'name': u"[FR] 6 CV, 5-20 000 km", 'unit_amount': 0.32},
{'name': u"[FR] 7+ CV, 5-20 000 km", 'unit_amount': 0.337},
{'name': u"[FR] 1-3 CV, > 20 000 km", 'unit_amount': 0.245},
{'name': u"[FR] 4 CV, > 20 000 km", 'unit_amount': 0.277},
{'name': u"[FR] 5 CV, > 20 000 km", 'unit_amount': 0.305},
{'name': u"[FR] 6 CV, > 20 000 km", 'unit_amount': 0.32},
{'name': u"[FR] 7+ CV, > 20 000 km", 'unit_amount': 0.337},
]
}
def create_private_car_km_prices(cr, registry):
with api.Environment.manage():
env = api.Environment(cr, SUPERUSER_ID, {})
companies = env['res.company'].search([])
for company in companies:
company_country_code = company.country_id.code and\
company.country_id.code.upper() or False
if company_country_code in KM_PRICES:
for record in KM_PRICES[company_country_code]:
env['private.car.km.price'].create({
'name': record['name'],
'unit_amount': record['unit_amount'],
'company_id': company.id,
})
return

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2014-2017 Akretion (http://www.akretion.com)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="demo_private_car_km_price" model="private.car.km.price">
<field name="name">France: 1-2CV 0-12000km</field>
<field name="unit_amount">0.423</field>
</record>
<record id="hr.employee_root" model="hr.employee">
<field name="private_car_plate">OD 4212 OO</field>
<field name="private_car_km_price_id" ref="demo_private_car_km_price"/>
</record>
<record id="hr.employee_mit" model="hr.employee">
<field name="private_car_plate">OE 1234 EO</field>
<field name="private_car_km_price_id" ref="demo_private_car_km_price"/>
</record>
<record id="hr.employee_al" model="hr.employee">
<field name="private_car_plate">BE 6543 AL</field>
<field name="private_car_km_price_id" ref="demo_private_car_km_price"/>
</record>
<record id="hr.employee_qdp" model="hr.employee">
<field name="private_car_plate">BE 1235 QD</field>
<field name="private_car_km_price_id" ref="demo_private_car_km_price"/>
</record>
</odoo>

View File

@@ -1,55 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2017 Akretion (http://www.akretion.com)
@author: Alexis de Lattre <alexis.delattre@akretion.com>
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
-->
<odoo>
<record id="private_car_km_price_form" model="ir.ui.view">
<field name="name">private_car_km_price.form</field>
<field name="model">private.car.km.price</field>
<field name="arch" type="xml">
<form string="Private Car Km Price">
<div class="oe_button_box" name="button_box">
<button name="toggle_active" type="object"
class="oe_stat_button" icon="fa-archive">
<field name="active" widget="boolean_button"
options='{"terminology": "archive"}'/>
</button>
</div>
<group name="main">
<field name="name"/>
<field name="unit_amount"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</form>
</field>
</record>
<record id="private_car_km_price_tree" model="ir.ui.view">
<field name="name">private_car_km_price.tree</field>
<field name="model">private.car.km.price</field>
<field name="arch" type="xml">
<tree string="Private Car Km Prices">
<field name="name"/>
<field name="unit_amount"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<record id="private_car_km_price_action" model="ir.actions.act_window">
<field name="name">Private Car Km Prices</field>
<field name="res_model">private.car.km.price</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="private_car_km_price_menu"
parent="hr_expense.menu_hr_expense_configuration"
action="private_car_km_price_action"
groups="account.group_account_manager,hr_expense.group_hr_expense_manager"
sequence="50"/>
</odoo>

View File

@@ -7,7 +7,7 @@
<odoo>
<record id="product_template_search_view" model="ir.ui.view">
<field name="name">private.car.expense.product.template.search</field>
<field name="name">hr.expense.usablility.product.template.search</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">

View File

@@ -15,10 +15,4 @@
<field name="groups" eval="[(4, ref('account.group_account_user')), (4, ref('hr_expense.group_hr_expense_manager')), (4, ref('hr_expense.group_hr_expense_user'))]"/>
</record>
<record id="private_car_km_price_multicompany" model="ir.rule">
<field name="name">Private Car Kilometer Prices Multi-company</field>
<field name="model_id" ref="model_private_car_km_price"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])]</field>
</record>
</odoo>

View File

@@ -1,4 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_private_car_km_price_read,Read access on private.car.km.price to employees,model_private_car_km_price,base.group_user,1,0,0,0
access_private_car_km_price_full,Full access on private.car.km.price to HR Expense Manager,model_private_car_km_price,hr_expense.group_hr_expense_manager,1,1,1,1
access_private_car_km_price_full,Full access on private.car.km.price to Accounting Manager,model_private_car_km_price,account.group_account_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_private_car_km_price_read Read access on private.car.km.price to employees model_private_car_km_price base.group_user 1 0 0 0
3 access_private_car_km_price_full Full access on private.car.km.price to HR Expense Manager model_private_car_km_price hr_expense.group_hr_expense_manager 1 1 1 1
4 access_private_car_km_price_full Full access on private.car.km.price to Accounting Manager model_private_car_km_price account.group_account_manager 1 1 1 1