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 @@ - + +