diff --git a/addons/sale/models/account_move.py b/addons/sale/models/account_move.py
index 4d0174618c2..ce4a1d411ef 100644
--- a/addons/sale/models/account_move.py
+++ b/addons/sale/models/account_move.py
@@ -26,14 +26,13 @@ class AccountMoveLine(models.Model):
"""
values_list = super(AccountMoveLine, self)._prepare_analytic_line()
- uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
-
# filter the move lines that can be reinvoiced: a cost (negative amount) analytic line without SO line but with a product can be reinvoiced
move_to_reinvoice = self.env['account.move.line']
for index, move_line in enumerate(self):
values = values_list[index]
- if 'so_line' not in values and float_compare(move_line.credit or 0.0, move_line.debit or 0.0, precision_digits=uom_precision_digits) != 1 and move_line.product_id.expense_policy not in [False, 'no']:
- move_to_reinvoice |= move_line
+ if 'so_line' not in values:
+ if move_line._sale_can_be_reinvoice():
+ move_to_reinvoice |= move_line
# insert the sale line in the create values of the analytic entries
if move_to_reinvoice:
@@ -46,6 +45,14 @@ class AccountMoveLine(models.Model):
return values_list
+ def _sale_can_be_reinvoice(self):
+ """ determine if the generated analytic line should be reinvoiced or not.
+ For Vendor Bill flow, if the product has a 'erinvoice policy' and is a cost, then we will find the SO on which reinvoice the AAL
+ """
+ self.ensure_one()
+ uom_precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ return float_compare(self.credit or 0.0, self.debit or 0.0, precision_digits=uom_precision_digits) != 1 and self.product_id.expense_policy not in [False, 'no']
+
def _sale_create_reinvoice_sale_line(self):
sale_order_map = self._sale_determine_order()
diff --git a/addons/sale_expense/__manifest__.py b/addons/sale_expense/__manifest__.py
index a9150bee904..7126a91a46c 100644
--- a/addons/sale_expense/__manifest__.py
+++ b/addons/sale_expense/__manifest__.py
@@ -16,8 +16,6 @@ This module allow to reinvoice employee expense, by setting the SO directly on t
'depends': ['sale_management', 'hr_expense'],
'data': [
'data/digest_data.xml',
- 'security/ir.model.access.csv',
- 'security/sale_expense_security.xml',
'views/product_view.xml',
'views/hr_expense_views.xml',
'views/sale_order_views.xml',
diff --git a/addons/sale_expense/models/account_move.py b/addons/sale_expense/models/account_move.py
index 0d41cbf1593..5420a3f3dc7 100644
--- a/addons/sale_expense/models/account_move.py
+++ b/addons/sale_expense/models/account_move.py
@@ -7,6 +7,15 @@ from odoo import api, models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
+ def _sale_can_be_reinvoice(self):
+ """ determine if the generated analytic line should be reinvoiced or not.
+ For Expense flow, if the product has a 'reinvoice policy' and a Sales Order is set on the expense, then we will reinvoice the AAL
+ """
+ self.ensure_one()
+ if self.expense_id: # expense flow is different from vendor bill reinvoice flow
+ return self.expense_id.product_id.expense_policy in ['sales_price', 'cost'] and self.expense_id.sale_order_id
+ return super(AccountMoveLine, self)._sale_can_be_reinvoice()
+
def _sale_determine_order(self):
""" For move lines created from expense, we override the normal behavior.
Note: if no SO but an AA is given on the expense, we will determine anyway the SO from the AA, using the same
@@ -16,8 +25,7 @@ class AccountMoveLine(models.Model):
mapping_from_expense = {}
for move_line in self.filtered(lambda move_line: move_line.expense_id):
- if move_line.expense_id.sale_order_id:
- mapping_from_expense[move_line.id] = move_line.expense_id.sale_order_id or None
+ mapping_from_expense[move_line.id] = move_line.expense_id.sale_order_id or None
mapping_from_invoice.update(mapping_from_expense)
return mapping_from_invoice
diff --git a/addons/sale_expense/models/hr_expense.py b/addons/sale_expense/models/hr_expense.py
index 057381815ab..8fcab91001e 100644
--- a/addons/sale_expense/models/hr_expense.py
+++ b/addons/sale_expense/models/hr_expense.py
@@ -10,18 +10,31 @@ class Expense(models.Model):
sale_order_id = fields.Many2one(
'sale.order', string='Reinvoice Customer', readonly=True,
states={'draft': [('readonly', False)], 'reported': [('readonly', False)]},
- domain="[('state', '=', 'sale'), ('company_id', '=', company_id)]")
+ domain="[('state', '=', 'sale'), ('company_id', '=', company_id)]",
+ help="If the product has an expense policy, it will be reinvoiced on this sales order")
+ can_be_reinvoiced = fields.Boolean("Can be reinvoiced", compute='_compute_can_be_reinvoiced')
+
+ @api.depends('product_id')
+ def _compute_can_be_reinvoiced(self):
+ for expense in self:
+ expense.can_be_reinvoiced = expense.product_id.expense_policy in ['sales_price', 'cost']
+
+ @api.onchange('product_id')
+ def _onchange_product_id(self):
+ super(Expense, self)._onchange_product_id()
+ if not self.can_be_reinvoiced:
+ self.sale_order_id = False
@api.onchange('sale_order_id')
def _onchange_sale_order(self):
if self.sale_order_id:
- self.analytic_account_id = self.sale_order_id.analytic_account_id
+ self.analytic_account_id = self.sale_order_id.sudo().analytic_account_id # `sudo` required for normal employee without sale access rights
def action_move_create(self):
- """ When posting expense, if a SO is set, this means you want to reinvoice. To do so, we
- have to set an Analytic Account on the expense. We choose the one from the SO, and
- if it does not exist, we generate it. Create AA even for product with no expense policy
- to keep track of the analytic.
+ """ When posting expense, if the AA is given, we will track cost in that
+ If a SO is set, this means we want to reinvoice the expense. But to do so, we
+ need the analytic entries to be generated, so a AA is required to reinvoice. So,
+ we ensure the AA if a SO is given.
"""
for expense in self.filtered(lambda expense: expense.sale_order_id and not expense.analytic_account_id):
if not expense.sale_order_id.analytic_account_id:
diff --git a/addons/sale_expense/models/sale_order.py b/addons/sale_expense/models/sale_order.py
index 1f83148c178..a9f54e1fcd8 100644
--- a/addons/sale_expense/models/sale_order.py
+++ b/addons/sale_expense/models/sale_order.py
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
+from odoo import SUPERUSER_ID
class SaleOrder(models.Model):
@@ -10,6 +11,11 @@ class SaleOrder(models.Model):
expense_ids = fields.One2many('hr.expense', 'sale_order_id', string='Expenses', domain=[('state', '=', 'done')], readonly=True, copy=False)
expense_count = fields.Integer("# of Expenses", compute='_compute_expense_count', compute_sudo=True)
+ @api.model
+ def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None):
+ name_get_uid = SUPERUSER_ID if self.env.user.has_group('base.group_user') else self.env.user.id
+ return super(SaleOrder, self)._name_search(name=name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
+
@api.depends('expense_ids')
def _compute_expense_count(self):
expense_data = self.env['hr.expense'].read_group([('sale_order_id', 'in', self.ids), ('state', '=', 'done')], ['sale_order_id'], ['sale_order_id'])
diff --git a/addons/sale_expense/tests/test_reinvoice.py b/addons/sale_expense/tests/test_reinvoice.py
index 4fee8c6831b..0a7fa507c53 100644
--- a/addons/sale_expense/tests/test_reinvoice.py
+++ b/addons/sale_expense/tests/test_reinvoice.py
@@ -197,6 +197,9 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
def test_no_expense(self):
""" Test invoicing expenses with no policy. Check nothing happen. """
+ analytic_account = self.env['account.analytic.account'].create({
+ 'name': "AA to track expense",
+ })
# confirm SO
sale_order_line = self.env['sale.order.line'].create({
'name': self.product_no_expense.name,
@@ -220,18 +223,18 @@ class TestReInvoice(TestExpenseCommon, TestCommonSaleNoChart):
'quantity': 3,
'sheet_id': self.expense_sheet.id,
'sale_order_id': self.sale_order.id,
- 'analytic_account_id': self.sale_order.analytic_account_id.id,
+ 'analytic_account_id': analytic_account.id,
})
- expense1._onchange_product_id()
+ expense1._onchange_product_id() # will reset the SO field to NULL
# approve and generate entries
self.expense_sheet.approve_expense_sheets()
self.expense_sheet.action_sheet_move_create()
- self.assertTrue(self.sale_order.analytic_account_id, "Posting expense with an expense product (even with no expense pilocy) should trigger the analytic account creation")
- self.assertEquals(self.sale_order.analytic_account_id, expense1.analytic_account_id, "SO analytic account should be the same for the expense")
- self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting")
+ self.assertFalse(expense1.sale_order_id, "None reinvoicable expense can not be linked to SO")
+ self.assertFalse(self.sale_order.analytic_account_id, "Posting expense with an non expense product should not trigger analytic Account creation from SO, since non reinvoicable expense can not be linked to SO")
+ self.assertEquals(len(self.sale_order.order_line), 1, "No SO line should have been created (or removed) on expense report posting")
self.assertEquals(sale_order_line.price_unit, self.product_no_expense.list_price, "The unit price of SO line should be the same")
self.assertEquals(sale_order_line.product_uom_qty, 2, "The ordered quantity of SO line should be two")
self.assertEquals(sale_order_line.qty_delivered, 0, "The delivered quantity of SO line should have been incremented")
diff --git a/addons/sale_expense/tests/test_sale_expense.py b/addons/sale_expense/tests/test_sale_expense.py
index 0af29c9dacb..920d1ee51fd 100644
--- a/addons/sale_expense/tests/test_sale_expense.py
+++ b/addons/sale_expense/tests/test_sale_expense.py
@@ -45,7 +45,8 @@ class TestSaleExpense(TestSale):
'analytic_account_id': so.analytic_account_id.id,
'unit_amount': 621.54,
'employee_id': employee.id,
- 'sheet_id': sheet.id
+ 'sheet_id': sheet.id,
+ 'sale_order_id': so.id,
})
# Approve
sheet.approve_expense_sheets()
@@ -74,7 +75,8 @@ class TestSaleExpense(TestSale):
'unit_amount': 0.15,
'quantity': 100,
'employee_id': employee.id,
- 'sheet_id': sheet.id
+ 'sheet_id': sheet.id,
+ 'sale_order_id': so.id,
})
# Approve
sheet.approve_expense_sheets()
diff --git a/addons/sale_expense/views/hr_expense_views.xml b/addons/sale_expense/views/hr_expense_views.xml
index ab1a8fc0eb2..b5d90ca34d9 100644
--- a/addons/sale_expense/views/hr_expense_views.xml
+++ b/addons/sale_expense/views/hr_expense_views.xml
@@ -4,12 +4,25 @@
hr.expense.form.inherit.sale.expense
hr.expense
+ 30
-
-
+
+
+
+
+ hr.expense.form.inherit.sale.expense
+ hr.expense
+
+
+
+ {'invisible':[['can_be_reinvoiced','=',False]],'readonly':[['state','in',['done']]]}
+
+
+
+
hr.expense.sheet.form.inherit.sale.expense
@@ -17,7 +30,8 @@
-
+
+