[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:
committed by
Laurent Stukkens (LTU)
parent
232e75421e
commit
2731e7cb08
@@ -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', {}),
|
||||
{},
|
||||
)
|
||||
Reference in New Issue
Block a user