diff --git a/hr_expense_usability/__init__.py b/hr_expense_usability/__init__.py
index 8899c78..87a7dee 100644
--- a/hr_expense_usability/__init__.py
+++ b/hr_expense_usability/__init__.py
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import hr_expense
+from .post_install import create_private_car_expense_products
diff --git a/hr_expense_usability/__manifest__.py b/hr_expense_usability/__manifest__.py
index 568ebea..285a513 100644
--- a/hr_expense_usability/__manifest__.py
+++ b/hr_expense_usability/__manifest__.py
@@ -13,16 +13,16 @@
'author': 'Akretion',
'website': 'http://www.akretion.com',
'depends': [
- 'hr_expense',
'hr_expense_sequence',
],
'data': [
- 'private_car_data.xml',
'hr_employee_view.xml',
'hr_expense_view.xml',
'product_view.xml',
+ 'hr_expense_data.xml',
'security/expense_security.xml',
],
+ 'post_init_hook': 'create_private_car_expense_products',
'demo': ['private_car_demo.xml'],
'installable': True,
}
diff --git a/hr_expense_usability/hr_expense.py b/hr_expense_usability/hr_expense.py
index fcd7b0d..b600641 100644
--- a/hr_expense_usability/hr_expense.py
+++ b/hr_expense_usability/hr_expense.py
@@ -6,6 +6,7 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
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 :
@@ -20,14 +21,14 @@ from odoo.tools import float_compare, float_is_zero
# 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 : none ? :)
-# => that's what is implemented in this module
-
+# Drawback : decimal precision 'Product Price' on standard_price of product
+# (but we need 3)
class ProductTemplate(models.Model):
_inherit = 'product.template'
@@ -140,7 +141,6 @@ 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)])
@@ -180,6 +180,25 @@ class HrEmployee(models.Model):
"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
+ # (even demo data doesn't bother to set it...)
+ # So I decided to put a fallback on employee.user_id.partner_id
+ self.ensure_one()
+ if self.address_home_id:
+ partner = self.address_home_id.commercial_partner_id
+ elif self.user_id:
+ # We don't use "commercial partner" here...
+ partner = self.user_id.partner_id
+ else:
+ raise UserError(_(
+ "The employee '%s' doesn't have a Home Address and isn't "
+ "linked to an Odoo user. You have to set one of these two "
+ "fields on the employee form in order to get a partner from "
+ "the employee for the Journal Items.") % self.display_name)
+ return partner
+
class HrExpense(models.Model):
_inherit = 'hr.expense'
@@ -188,6 +207,9 @@ class HrExpense(models.Model):
date = fields.Date(track_visibility='onchange', required=True)
currency_id = fields.Many2one(track_visibility='onchange', required=True)
total_amount = fields.Float(track_visibility='onchange')
+ # 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)]})
@@ -276,7 +298,8 @@ class HrExpense(models.Model):
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')
+ prec = self.env['decimal.precision'].precision_get(
+ 'Expense Unit Price')
if float_compare(
original_unit_amount, self.unit_amount,
precision_digits=prec):
@@ -308,7 +331,8 @@ class HrExpense(models.Model):
return res
@api.constrains(
- 'product_id', 'private_car_plate', 'payment_mode', 'tax_ids')
+ 'product_id', 'private_car_plate', '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')
@@ -374,7 +398,38 @@ class HrExpense(models.Model):
"The amount tax of expense '%s' is %s, "
"but no tax is selected.")
% (exp.name, exp.tax_amount))
- # TODO: check all have the same sign
+ sign = {
+ 'untaxed_amount_usability': 0,
+ 'tax_amount': 0,
+ 'total_amount': 0,
+ }
+ for field_name in sign.iterkeys():
+ sign[field_name] = float_compare(
+ exp[field_name], 0, precision_rounding=prec)
+ if (
+ sign['total_amount'] < 0 and (
+ sign['untaxed_amount_usability'] > 0 or
+ sign['tax_amount'] > 0)):
+ raise ValidationError(_(
+ "On the expense '%s', the total amount (%s) is "
+ "negative, so the untaxed amount (%s) and the "
+ "tax amount (%s) should be negative or null.") % (
+ exp.name,
+ exp.total_amount,
+ exp.untaxed_amount_usability,
+ exp.tax_amount))
+ if (
+ sign['total_amount'] > 0 and (
+ sign['untaxed_amount_usability'] < 0 or
+ sign['tax_amount'] < 0)):
+ raise ValidationError(_(
+ "On the expense '%s', the total amount (%s) is "
+ "positive, so the untaxed amount (%s) and the "
+ "tax amount (%s) should be positive or null.") % (
+ exp.name,
+ exp.total_amount,
+ exp.untaxed_amount_usability,
+ exp.tax_amount))
def action_move_create(self):
'''disable account.move creation per hr.expense'''
@@ -419,12 +474,19 @@ class HrExpenseSheet(models.Model):
sheet.untaxed_amount_company_currency = untaxed
sheet.tax_amount_company_currency = total - untaxed
+ @api.one
+ @api.constrains('expense_line_ids')
+ def _check_amounts(self):
+ '''Remove the constraint 'You cannot have a positive and negative
+ amounts on the same expense report.' '''
+ return True
+
def _prepare_move(self):
self.ensure_one()
if not self.journal_id:
raise UserError(_(
"No journal selected for expense report %s.")
- % self.number)
+ % self.display_name)
date = self.accounting_date or fields.Date.context_today(self)
vals = {
'journal_id': self.journal_id.id,
@@ -437,35 +499,28 @@ class HrExpenseSheet(models.Model):
def _prepare_payable_move_line(self, total_company_currency):
self.ensure_one()
- debit = credit = 0.0
+ debit = credit = False
prec = self.company_id.currency_id.rounding
if float_compare(
total_company_currency, 0, precision_rounding=prec) > 0:
credit = total_company_currency
else:
debit = total_company_currency * -1
- if not self.employee_id.address_home_id:
- raise UserError(_(
- "The employee '%s' doesn't have a Home Address. "
- "The partner selected as 'Home Address' on the employee "
- "will be used as the partner for the accounting entry.")
- % (self.employee_id.display_name))
- partner = self.employee_id.address_home_id
+ partner = self.employee_id._get_accounting_partner_from_employee()
# by default date_maturity = move date
vals = {
'account_id': partner.property_account_payable_id.id,
'partner_id': partner.id,
- 'name': self.name[:60],
+ 'name': self.name[:64],
'credit': credit,
'debit': debit,
}
return vals
- # TODO: set tax properties for those who use them
def _prepare_expense_move_lines(self):
self.ensure_one()
mlines = []
- partner_id = self.employee_id.address_home_id.id
+ partner = self.employee_id._get_accounting_partner_from_employee()
prec = self.company_id.currency_id.rounding
for exp in self.expense_line_ids:
# Expense
@@ -482,7 +537,7 @@ class HrExpenseSheet(models.Model):
exp.product_id.categ_id.display_name))
mlines.append({
'type': 'expense',
- 'partner_id': partner_id,
+ 'partner_id': partner.id,
'account_id': account.id,
'analytic_account_id': exp.analytic_account_id.id or False,
'amount': exp.untaxed_amount_company_currency,
@@ -503,7 +558,7 @@ class HrExpenseSheet(models.Model):
analytic_account_id = False
mlines.append({
'type': 'tax',
- 'partner_id': partner_id,
+ 'partner_id': partner.id,
'account_id': tax_account_id,
'analytic_account_id': analytic_account_id,
'amount': exp.tax_amount_company_currency,
@@ -525,15 +580,14 @@ class HrExpenseSheet(models.Model):
key = (False, False, False, i)
if key in group_mlines:
group_mlines[key]['amount'] += mline['amount']
- group_mlines[key]['name'] = '%s %s' % (
- self.number, self.name[:60])
+ group_mlines[key]['name'] = self.name[:64]
else:
group_mlines[key] = mline
res_mlines = []
total_cc = 0.0
for gmlines in group_mlines.itervalues():
total_cc += gmlines['amount']
- credit = debit = 0.0
+ credit = debit = False
cmp_amount = float_compare(
gmlines['amount'], 0, precision_rounding=prec)
if cmp_amount > 0:
@@ -585,5 +639,5 @@ class HrExpenseSheet(models.Model):
return vals
# TODO: for multi-company with expenses envir., we would need a field
- # 'default_expense_journal' on company
- # TODO: test if state => paid(done) when reconciled via bank statement...
+ # 'default_expense_journal' on company (otherwise, it takes the
+ # first purchase journal, which is probably not the good one
diff --git a/hr_expense_usability/hr_expense_data.xml b/hr_expense_usability/hr_expense_data.xml
new file mode 100644
index 0000000..040b47f
--- /dev/null
+++ b/hr_expense_usability/hr_expense_data.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+ My Private Car Expense (in km)
+ PrivateCarExp
+
+
+
+
+ service
+ 0
+
+
+
+
+
+
+
+
+ Expense Unit Price
+ 3
+
+
+
diff --git a/hr_expense_usability/hr_expense_view.xml b/hr_expense_usability/hr_expense_view.xml
index 9f3274b..64e65c2 100644
--- a/hr_expense_usability/hr_expense_view.xml
+++ b/hr_expense_usability/hr_expense_view.xml
@@ -56,7 +56,37 @@
-
+
+
+ usability.hr.expense.pivot
+ hr.expense
+
+
+
+
+
+
+
+
+
+
+
+
+ usability.hr.expense.graph
+ hr.expense
+
+
+
+
+
+
+
+
+
+
+
+
+
usability.hr.expense.sheet.form
hr.expense.sheet
@@ -75,11 +105,15 @@
+
+
+
+
0
account.group_account_user
@@ -110,6 +144,28 @@
-
+
+
+ usability.hr.expense.sheet.pivot
+ hr.expense.sheet
+
+
+
+
+
+
+
+
+
+ usability.hr.expense.sheet.graph
+ hr.expense.sheet
+
+
+
+
+
+
+
+
diff --git a/hr_expense_usability/post_install.py b/hr_expense_usability/post_install.py
new file mode 100644
index 0000000..0078323
--- /dev/null
+++ b/hr_expense_usability/post_install.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# © 2016-2017 Akretion (Alexis de Lattre )
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from odoo import api, SUPERUSER_ID
+
+PRODUCTS = {
+ 'FR': [
+ {'default_code': '1-3CV_0-5000km',
+ 'name': u"Frais kilométriques: 1-3 CV, < 5 000 km",
+ 'cost': 0.41},
+ {'default_code': '4CV_0-5000km',
+ 'name': u"Frais kilométriques: 4 CV, < 5 000 km",
+ 'cost': 0.493},
+ {'default_code': '5CV_0-5000km',
+ 'name': u"Frais kilométriques: 5 CV, < 5 000 km",
+ 'cost': 0.543},
+ {'default_code': '6CV_0-5000km',
+ 'name': u"Frais kilométriques: 6 CV, < 5 000 km",
+ 'cost': 0.568},
+ {'default_code': '7+CV_0-5000km',
+ 'name': u"Frais kilométriques: 7+ CV, < 5 000 km",
+ 'cost': 0.595},
+ {'default_code': '1-3CV_5-20000km',
+ 'name': u"Frais kilométriques: 1-3 CV, 5-20 000 km",
+ 'cost': 0.245},
+ {'default_code': '4CV_5-20000km',
+ 'name': u"Frais kilométriques: 4 CV, 5-20 000 km",
+ 'cost': 0.277},
+ {'default_code': '5CV_5-20000km',
+ 'name': u"Frais kilométriques: 5 CV, 5-20 000 km",
+ 'cost': 0.305},
+ {'default_code': '6CV_5-20000km',
+ 'name': u"Frais kilométriques: 6 CV, 5-20 000 km",
+ 'cost': 0.32},
+ {'default_code': '7+CV_5-20000km',
+ 'name': u"Frais kilométriques: 7+ CV, 5-20 000 km",
+ 'cost': 0.337},
+ {'default_code': '1-3CV_+20000km',
+ 'name': u"Frais kilométriques: 1-3 CV, > 20 000 km",
+ 'cost': 0.245},
+ {'default_code': '4CV_+20000km',
+ 'name': u"Frais kilométriques: 4 CV, > 20 000 km",
+ 'cost': 0.277},
+ {'default_code': '5CV_+20000km',
+ 'name': u"Frais kilométriques: 5 CV, > 20 000 km",
+ 'cost': 0.305},
+ {'default_code': '6CV_+20000km',
+ 'name': u"Frais kilométriques: 6 CV, > 20 000 km",
+ 'cost': 0.32},
+ {'default_code': '7+CV_+20000km',
+ 'name': u"Frais kilométriques: 7+ CV, > 20 000 km",
+ 'cost': 0.337},
+ ]
+ }
+
+
+def create_private_car_expense_products(cr, registry):
+ with api.Environment.manage():
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ companies = env['res.company'].search([])
+ country_codes = []
+ for company in companies:
+ company_country_code = company.country_id.code and\
+ company.country_id.code.upper() or False
+ if company_country_code not in country_codes:
+ country_codes.append(company_country_code)
+ categ_id = env.ref('hr_expense.cat_expense').id
+ km_uom_id = env.ref('product.product_uom_km').id
+ for country_code in country_codes:
+ if country_code in PRODUCTS:
+ for product in PRODUCTS[country_code]:
+ env['product.product'].create({
+ 'name': product['name'],
+ 'default_code': product.get('default_code'),
+ 'categ_id': categ_id,
+ 'sale_ok': False,
+ 'purchase_ok': False,
+ 'can_be_expensed': False,
+ 'private_car_expense_ok': True,
+ 'type': 'service',
+ 'list_price': False,
+ 'standard_price': product['cost'],
+ 'uom_id': km_uom_id,
+ 'uom_po_id': km_uom_id,
+ 'taxes_id': False,
+ 'supplier_taxes_id': False,
+ })
+ return
diff --git a/hr_expense_usability/private_car_data.xml b/hr_expense_usability/private_car_data.xml
deleted file mode 100644
index 1d3deb9..0000000
--- a/hr_expense_usability/private_car_data.xml
+++ /dev/null
@@ -1,281 +0,0 @@
-
-
-
-
-
-
- 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
index 596eb7d..8c4b108 100644
--- a/hr_expense_usability/private_car_demo.xml
+++ b/hr_expense_usability/private_car_demo.xml
@@ -7,24 +7,41 @@
+
+ 1-2CV 0-12000km Private Car Expenses
+ 1-2CV/0-12000km
+
+
+
+
+
+ service
+ 0
+ 0.42
+
+
+
+
+
+
OD 4212 OO
-
+
OE 1234 EO
-
+
BE 6543 AL
-
+
BE 1235 QD
-
+