[ADD] project_sale_expense: add reinvoice expense in project profitability

Before this commit, the reinvoice expenses are not takken into account
for the project profitability.

This commit adds a new module called `project_sale_expense` to make the
bridge between the sale_expense and project_hr_expense and so in this
module, the revenues impacted by the reinvoice expenses will be computed
and takken into account for the project profitability.

task-2710808
This commit is contained in:
Xavier BOL (xbo)
2022-04-01 15:33:00 +02:00
committed by Laurent Stukkens (LTU)
parent 232e75421e
commit 2731e7cb08
5 changed files with 217 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Project - Sale - Expense',
'version': '1.0',
'description': 'Adds a full traceability of reinvoice expenses on the profitability report.',
'license': 'LGPL-3',
'category': 'Hidden',
'depends': ['sale_project', 'sale_expense', 'project_hr_expense'],
'data': [],
'demo': [],
'auto_install': True,
'application': False,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project_project
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
class Project(models.Model):
_inherit = 'project.project'
def _get_expenses_profitability_items(self):
if not self.analytic_account_id:
return {}
expenses_read_group = self.env['hr.expense'].sudo()._read_group(
[
('analytic_account_id', 'in', self.analytic_account_id.ids),
('is_refused', '=', False),
('state', 'in', ['approved', 'done']),
],
['sale_order_id', 'product_id', 'untaxed_amount'],
['sale_order_id', 'product_id'],
lazy=False,
)
if not expenses_read_group:
return {}
expenses_per_so_id = {}
amount_billed = 0.0
for res in expenses_read_group:
so_id = res['sale_order_id'] and res['sale_order_id'][0]
product_id = res['product_id'] and res['product_id'][0]
expenses_per_so_id.setdefault(so_id, {})[product_id] = res['ids']
amount_billed += res['untaxed_amount']
sol_read_group = self.env['sale.order.line'].sudo()._read_group(
[
('order_id', 'in', list(expenses_per_so_id.keys())),
('is_expense', '=', True),
('state', 'in', ['sale', 'done']),
],
['order_id', 'product_id', 'untaxed_amount_to_invoice', 'untaxed_amount_invoiced'],
['order_id', 'product_id'],
lazy=False)
total_amount_expense_invoiced = total_amount_expense_to_invoice = 0.0
reinvoice_expense_ids = []
for res in sol_read_group:
expense_data_per_product_id = expenses_per_so_id[res['order_id'][0]]
product_id = res['product_id'][0]
if product_id in expense_data_per_product_id:
total_amount_expense_to_invoice += res['untaxed_amount_to_invoice']
total_amount_expense_invoiced += res['untaxed_amount_invoiced']
reinvoice_expense_ids += expense_data_per_product_id[product_id]
expense_name = _('Expenses')
section_id = 'expenses'
expense_data = {
'costs': {
'id': section_id,
'name': expense_name,
'billed': -amount_billed,
'to_bill': 0.0,
},
}
if reinvoice_expense_ids:
expense_data['revenues'] = {
'id': section_id,
'name': expense_name,
'invoiced': total_amount_expense_invoiced,
'to_invoice': total_amount_expense_to_invoice,
}
return expense_data
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tests import tagged
from odoo.addons.project_hr_expense.tests.test_project_profitability import TestProjectHrExpenseProfitabilityCommon
from odoo.addons.sale.tests.common import TestSaleCommon
from odoo.addons.sale_project.tests.test_project_profitability import TestProjectProfitabilityCommon
@tagged('-at_install', 'post_install')
class TestProjectSaleExpenseProfitability(TestProjectProfitabilityCommon, TestProjectHrExpenseProfitabilityCommon, TestSaleCommon):
def test_project_profitability(self):
expense = self.env['hr.expense'].create({
'name': 'expense',
'product_id': self.company_data['product_order_sales_price'].id,
'unit_amount': self.company_data['product_order_sales_price'].list_price,
'employee_id': self.expense_employee.id,
'analytic_account_id': self.project.analytic_account_id.id,
'sale_order_id': self.sale_order.id,
})
# See method definition in `project_hr_expense.tests.test_project_profitability`
expense_sheet = self.check_project_profitability_before_creating_and_approving_expense_sheet(
expense,
self.project,
self.project_profitability_items_empty)
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{},
)
self.assertDictEqual(
expense_profitability['costs'],
{'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0},
)
expense_sheet.action_sheet_move_create()
self.assertRecordValues(self.sale_order.order_line, [
# Original SO line:
{
'product_id': self.product_delivery_service.id,
'qty_delivered': 0.0,
'product_uom_qty': 10,
'is_expense': False,
},
{
'product_id': self.company_data['product_order_sales_price'].id,
'qty_delivered': 1.0,
'product_uom_qty': 1.0,
'is_expense': True,
},
])
expense_sol = self.sale_order.order_line.filtered(lambda sol: sol.product_id == self.company_data['product_order_sales_price'])
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{'id': 'expenses', 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice},
)
self.assertDictEqual(
expense_profitability['costs'],
{'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0},
)
self.assertDictEqual(
self.project._get_profitability_items(),
{
'revenues': {
'data': [expense_profitability['revenues']],
'total': {k: v for k, v in expense_profitability['revenues'].items() if k in ['to_invoice', 'invoiced']},
},
'costs': {
'data': [expense_profitability['costs']],
'total': {k: v for k, v in expense_profitability['costs'].items() if k in ['to_bill', 'billed']},
},
}
)
invoice = self.env['sale.advance.payment.inv'] \
.with_context({
'active_model': 'sale.order',
'active_id': self.sale_order.id,
}).create({
'advance_payment_method': 'delivered',
})._create_invoices(self.sale_order)
invoice.action_post()
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{'id': 'expenses', 'invoiced': expense_sol.untaxed_amount_invoiced, 'to_invoice': 0.0},
)
credit_note = invoice._reverse_moves()
credit_note.action_post()
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{'id': 'expenses', 'invoiced': 0.0, 'to_invoice': expense_sol.untaxed_amount_to_invoice},
)
self.sale_order.action_cancel()
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{},
)
self.assertDictEqual(
expense_profitability['costs'],
{'id': 'expenses', 'billed': -280.0, 'to_bill': 0.0},
)
expense_sheet.refuse_sheet('Test Cancel Expense')
expense_profitability = self.project._get_expenses_profitability_items()
self.assertDictEqual(
expense_profitability.get('revenues', {}),
{},
)
self.assertDictEqual(
expense_profitability.get('costs', {}),
{},
)