From ba922611481b1f46a1fdf9c689cd6abbb4dbb0a6 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Wed, 14 Aug 2019 07:44:11 +0000 Subject: [PATCH 1/5] [FIX] sale_timesheet_purchase: correct action xmlid Since the refactoring of the accounting model, some action changed but their xmlid were not adapted everywhere. Task-1843241 --- addons/sale_timesheet_purchase/models/project_overview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale_timesheet_purchase/models/project_overview.py b/addons/sale_timesheet_purchase/models/project_overview.py index 6af3cbef830..c827a89bbce 100644 --- a/addons/sale_timesheet_purchase/models/project_overview.py +++ b/addons/sale_timesheet_purchase/models/project_overview.py @@ -30,7 +30,7 @@ class Project(models.Model): 'count': len(account_invoices), 'icon': 'fa pencil-square-o', 'action': _to_action_data( - action=self.env.ref('purchase.action_invoice_pending'), + action=self.env.ref('account.action_move_in_invoice_type'), domain=[('id', 'in', account_invoices.ids)], context={'create': False, 'edit': False, 'delete': False} )}) From 00ac6c60f6bc2d83ca614db043b1e655fd892b5e Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Fri, 24 May 2019 14:28:09 +0000 Subject: [PATCH 2/5] [FIX] project: portal user can not read AA For historical reason (problably the inheritS between project and AA, removed in 12.0), portal user can read Analytic Account. This makes no sense, as it is a purely internal resource for companies. Removing the access rights don't change anything for portal user and make those data secure. Task-1911581 --- addons/project/security/ir.model.access.csv | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/project/security/ir.model.access.csv b/addons/project/security/ir.model.access.csv index f7bf080c5cf..ccc5238f501 100644 --- a/addons/project/security/ir.model.access.csv +++ b/addons/project/security/ir.model.access.csv @@ -22,5 +22,4 @@ access_mail_alias,mail.alias,mail.model_mail_alias,project.group_project_manager access_mail_activity_type_project_manager,mail.activity.type.project.manager,mail.model_mail_activity_type,project.group_project_manager,1,1,1,1 access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0 access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1 -access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0 -access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 \ No newline at end of file +access_account_analytic_line_project,account.analytic.line project,analytic.model_account_analytic_line,project.group_project_manager,1,1,1,1 From 7c5ce75aeb2804cf13411017dc5c710fdf433506 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Tue, 20 Aug 2019 08:31:42 +0000 Subject: [PATCH 3/5] [FIX] analytic: missing depends This commit adds a missing `depends` on the `debit` and `credit` computed field on analytic account model. Indeed, without that, the recompute is not done when accessing thoses fields after adding lines in the same transaction (like in tests). Task-1911581 --- addons/analytic/models/analytic_account.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/analytic/models/analytic_account.py b/addons/analytic/models/analytic_account.py index 2ec45c72f73..9d08316ee01 100644 --- a/addons/analytic/models/analytic_account.py +++ b/addons/analytic/models/analytic_account.py @@ -78,6 +78,7 @@ class AccountAnalyticAccount(models.Model): line['credit'] = sum(accounts.mapped('credit')) return res + @api.depends('line_ids.amount') def _compute_debit_credit_balance(self): Curr = self.env['res.currency'] analytic_line_obj = self.env['account.analytic.line'] From 1f77ff3cd6ef27c0945aaf6d6f677e44e342ffd8 Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Mon, 19 Aug 2019 14:49:46 +0000 Subject: [PATCH 4/5] [FIX] sale: name get uniformize API This commit makes the override of the `name_search` more consistent the basic implementation of the ORM (using lazy name_get and supports the `name_get_uid` parameters) Task-1911581 --- addons/sale/models/sale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index cb293cd5d97..428be0c3363 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -445,7 +445,7 @@ class SaleOrder(models.Model): ['|', ('name', operator, name), ('partner_id.name', operator, name)] ]) order_ids = self._search(domain, limit=limit, access_rights_uid=name_get_uid) - return self.browse(order_ids).name_get() + return models.lazy_name_get(self.browse(order_ids).with_user(name_get_uid)) return super(SaleOrder, self)._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid) def _prepare_invoice(self): From f871908e86dc868a7d2fb1d2dff6cae0fb79977c Mon Sep 17 00:00:00 2001 From: jem-odoo Date: Thu, 13 Jun 2019 12:46:47 +0000 Subject: [PATCH 5/5] [IMP] sale_expense: cost tracking and reinvoice flow for expense Since 12.0, we wanted the employee to select directly the sales order on which the expense should be reinvoiced. The goal was to avoid employee to select the analytic account (too technical and usually unknown for employee). Problem is that we had to give read access to SO to simple employee, breaking the access rules "See your own document". As we want to allow all internal employees to determine if the expense should be reinvoice and on which sales order it should be, we need to allow them to select that sales order. This commit allows user to search on SO (simply get their name_get), no information more to avoid breaking "See your own document" for salesman. Falty ir.rule and access rights are now removed. We now clearly split the cost tracking and reinvoicing flow: - the analytic account field (visible only with group 'analytic' as considered as advance feature) allows user to track their expenses in some accounts. - the sales order is displayed for users or approvers to reinvoice the expense. As this operation implies the tracking, if the AA is not give, the SO will create its own AA in which the expense will be tracked. This SO field is only relevant when the product has a reinvoice policy, so we hide it if there is no reinvoice policy defined. Reinvoicing an expense has now a different flow from reinvoicing a "Vendor Bill", which still do magic to determine the SO from the AA set on its lines. Technical: to allow user to select SO from a m2o dropdown, the `name_search` is done in `sudo` for internal user. This is a borderline 'feature' of the system. A few thing show that the system does completely supports this ("Search More" gives access error, ...). We still keep this solution, as we don't want a sub group giving access to SO to all employee (aka an option allowing people to break the sales rights). Task-1843241 --- addons/sale/models/account_move.py | 15 ++++++++--- addons/sale_expense/__manifest__.py | 2 -- addons/sale_expense/models/account_move.py | 12 +++++++-- addons/sale_expense/models/hr_expense.py | 25 ++++++++++++++----- addons/sale_expense/models/sale_order.py | 6 +++++ addons/sale_expense/tests/test_reinvoice.py | 13 ++++++---- .../sale_expense/tests/test_sale_expense.py | 6 +++-- .../sale_expense/views/hr_expense_views.xml | 20 ++++++++++++--- 8 files changed, 75 insertions(+), 24 deletions(-) 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 @@ - + +