[MERGE] sale_timesheet: include expenses in overview

These commit mostly modify the profitability report to
extend it in order to include expense cost of a project.
If analytic entries are created on an analytic account
linked to a project, they will be count as expense
cost for that project.
So, if several project are linked to the same AA, the
same expense cost will appear in all those project
overviews, as they are linked to AA and not project.

This merge also provide test for the reporting view, as
it will probably change in future devs.

Task #49214
Closes #24593
This commit is contained in:
jem-odoo
2018-06-11 15:50:13 +02:00
5 changed files with 535 additions and 45 deletions
+5 -3
View File
@@ -70,12 +70,14 @@ class SaleTimesheetController(http.Controller):
dashboard_values['rates']['total'] += rate
# profitability, using profitability SQL report
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'total'], 0.0)
profitability_raw_data = request.env['project.profitability.report'].read_group([('project_id', 'in', projects.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost'], ['project_id'])
profit = dict.fromkeys(['invoiced', 'to_invoice', 'cost', 'expense_cost', 'expense_amount_untaxed_invoiced', 'total'], 0.0)
profitability_raw_data = request.env['project.profitability.report'].read_group([('project_id', 'in', projects.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_invoiced'], ['project_id'])
for data in profitability_raw_data:
profit['invoiced'] += data.get('amount_untaxed_invoiced', 0.0)
profit['to_invoice'] += data.get('amount_untaxed_to_invoice', 0.0)
profit['cost'] += data.get('timesheet_cost', 0.0)
profit['expense_cost'] += data.get('expense_cost', 0.0)
profit['expense_amount_untaxed_invoiced'] += data.get('expense_amount_untaxed_invoiced', 0.0)
profit['total'] = sum([profit[item] for item in profit.keys()])
dashboard_values['profit'] = profit
@@ -302,7 +304,7 @@ class SaleTimesheetController(http.Controller):
def _table_get_empty_so_lines(self, projects):
""" get the Sale Order Lines having no timesheet but having generated a task or a project """
so_lines = projects.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service)
so_lines = projects.sudo().mapped('sale_line_id.order_id.order_line').filtered(lambda sol: sol.is_service and not sol.is_expense)
return set(so_lines.ids), set(so_lines.mapped('order_id').ids)
# --------------------------------------------------
@@ -20,13 +20,17 @@ class ProfitabilityAnalysis(models.Model):
# cost
timesheet_unit_amount = fields.Float("Timesheet Unit Amount", digits=(16, 2), readonly=True, group_operator="sum")
timesheet_cost = fields.Float("Timesheet Cost", digits=(16, 2), readonly=True, group_operator="sum")
expense_cost = fields.Float("Other Cost", digits=(16, 2), readonly=True, group_operator="sum")
# sale revenue
order_confirmation_date = fields.Datetime('Sales Order Confirmation Date', readonly=True)
sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True)
sale_order_id = fields.Many2one('sale.order', string='Sale Order', readonly=True)
product_id = fields.Many2one('product.product', string='Product', readonly=True)
amount_untaxed_to_invoice = fields.Float("Untaxed Amout To Invoice", digits=(16, 2), readonly=True, group_operator="sum")
amount_untaxed_invoiced = fields.Float("Untaxed Amout Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
amount_untaxed_to_invoice = fields.Float("Untaxed Amount To Invoice", digits=(16, 2), readonly=True, group_operator="sum")
amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_invoiced = fields.Float("Untaxed Re-invoiced Amount", digits=(16, 2), readonly=True, group_operator="sum")
def init(self):
tools.drop_view_if_exists(self._cr, self._table)
@@ -35,52 +39,120 @@ class ProfitabilityAnalysis(models.Model):
WITH currency_rate as (%s)
SELECT
ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id,
P.id AS project_id,
P.user_id AS user_id,
TIMESHEET_AMOUNT.project_id AS project_id,
TIMESHEET_AMOUNT.sale_line_id AS sale_line_id,
AA.id AS analytic_account_id,
AA.partner_id AS partner_id,
SOL.id AS sale_line_id,
P.analytic_account_id AS analytic_account_id,
P.partner_id AS partner_id,
C.id AS company_id,
C.currency_id AS currency_id,
S.id AS sale_order_id,
S.date_order AS order_confirmation_date,
SOL.product_id AS product_id,
(SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_to_invoice AS amount_untaxed_to_invoice,
(SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_invoiced AS amount_untaxed_invoiced,
TIMESHEET_AMOUNT.timesheet_unit_amount AS timesheet_unit_amount,
TIMESHEET_AMOUNT.timesheet_cost AS timesheet_cost
SOL.qty_delivered_method AS sale_qty_delivered_method,
IP.value_float as standar_price,
CASE
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_to_invoice
ELSE 0.0
END AS expense_amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
THEN
CASE
WHEN T.expense_policy = 'sales_price'
THEN (SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_invoiced
ELSE IP.value_float * SOL.qty_invoiced
END
ELSE 0.0
END AS expense_amount_untaxed_invoiced,
CASE
WHEN SOL.qty_delivered_method = 'timesheet' THEN (SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_to_invoice
ELSE 0.0
END AS amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method = 'timesheet' THEN (SOL.price_reduce / COALESCE(CR.rate, 1.0)) * SOL.qty_invoiced
ELSE 0.0
END AS amount_untaxed_invoiced,
COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount,
COST_SUMMARY.timesheet_cost AS timesheet_cost,
COST_SUMMARY.expense_cost AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
JOIN res_company C ON C.id = AA.company_id
LEFT JOIN
(
SELECT T.sale_line_id AS sale_line_id, T.project_id AS project_id
FROM project_task T
WHERE T.sale_line_id IS NOT NULL
UNION
SELECT P.sale_line_id AS sale_line_id, P.id AS project_id
FROM project_project P
WHERE P.sale_line_id IS NOT NULL
) SOL_PER_PROJECT ON P.id = SOL_PER_PROJECT.project_id
RIGHT OUTER JOIN
(
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
SELECT
project_id,
analytic_account_id,
sale_line_id,
SUM(timesheet_unit_amount) AS timesheet_unit_amount,
SUM(timesheet_cost) AS timesheet_cost,
SUM(expense_cost) AS expense_cost
FROM (
SELECT
project_id AS project_id,
so_line AS sale_line_id,
array_agg(id),
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
TS.so_line AS sale_line_id,
SUM(TS.unit_amount) AS timesheet_unit_amount,
SUM(TS.amount) AS timesheet_cost
FROM account_analytic_line TS
WHERE project_id IS NOT NULL
GROUP BY project_id, so_line
) TIMESHEET_AMOUNT ON TIMESHEET_AMOUNT.project_id = SOL_PER_PROJECT.project_id AND TIMESHEET_AMOUNT.sale_line_id = SOL_PER_PROJECT.sale_line_id
LEFT JOIN sale_order_line SOL ON SOL_PER_PROJECT.sale_line_id = SOL.id
SUM(TS.amount) AS timesheet_cost,
0.0 AS expense_cost
FROM account_analytic_line TS, project_project P
WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, TS.so_line
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
AAL.so_line AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
SUM(AAL.amount) AS expense_cost
FROM project_project P
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
GROUP BY P.id, AA.id, AAL.so_line
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_project P ON SOL.project_id = P.id
WHERE P.active = 't' AND P.allow_timesheets = 't'
UNION
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
SOL.id AS sale_line_id,
0.0 AS timesheet_unit_amount,
0.0 AS timesheet_cost,
0.0 AS expense_cost
FROM sale_order_line SOL
INNER JOIN project_task T ON SOL.task_id = T.id
INNER JOIN project_project P ON P.id = T.project_id
WHERE P.active = 't' AND P.allow_timesheets = 't'
) SUB_COST_SUMMARY
GROUP BY project_id, analytic_account_id, sale_line_id
) COST_SUMMARY ON COST_SUMMARY.project_id = P.id
LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id
LEFT JOIN sale_order S ON SOL.order_id = S.id
LEFT JOIN product_product PP on (SOL.product_id=PP.id)
LEFT JOIN product_template T on (PP.product_tmpl_id=T.id)
LEFT JOIN ir_property IP ON (IP.name='standard_price' AND IP.res_id=CONCAT('product.product,',PP.id) AND IP.company_id=S.company_id)
LEFT JOIN currency_rate CR ON (CR.currency_id = SOL.currency_id
AND CR.currency_id != C.currency_id
AND CR.company_id = SOL.company_id
AND CR.date_start <= COALESCE(S.date_order, now())
AND (CR.date_end IS NULL OR cr.date_end > COALESCE(S.date_order, now())))
WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
)
""" % (self._table, self.env['res.currency']._select_companies_rates())
self._cr.execute(query)
+1
View File
@@ -6,3 +6,4 @@ from . import test_sale_timesheet
from . import test_sale_service
from . import test_timesheet_revenue
from . import test_reinvoice
from . import test_reporting
@@ -0,0 +1,382 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.tools import float_is_zero, float_compare
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheetNoChart
class TestReporting(TestCommonSaleTimesheetNoChart):
@classmethod
def setUpClass(cls):
super(TestReporting, cls).setUpClass()
cls.setUpEmployees()
cls.setUpServiceProducts()
cls.setUpAdditionalAccounts()
cls.setUpAccountJournal()
# expense product
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Expense service",
'standard_price': 10,
'list_price': 20,
'type': 'service',
'invoice_policy': 'delivery',
'expense_policy': 'sales_price',
'default_code': 'EXP',
'service_type': 'manual',
'taxes_id': False,
'property_account_income_id': cls.account_sale.id,
})
# create Analytic Accounts
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
'name': 'Test AA 1',
'code': 'AA1',
'company_id': cls.partner_customer_usd.company_id.id,
'partner_id': cls.partner_customer_usd.id
})
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
'name': 'Test AA 2',
'code': 'AA2',
'company_id': cls.partner_customer_usd.company_id.id,
'partner_id': cls.partner_customer_usd.id
})
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_customer_usd.id,
'partner_invoice_id': cls.partner_customer_usd.id,
'partner_shipping_id': cls.partner_customer_usd.id,
'analytic_account_id': cls.analytic_account_1.id,
})
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet3.name,
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
'price_unit': cls.product_delivery_timesheet3.list_price,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'name': cls.product_delivery_timesheet2.name,
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
'price_unit': cls.product_delivery_timesheet2.list_price,
'order_id': cls.sale_order_1.id,
})
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'partner_id': cls.partner_customer_usd.id,
'partner_invoice_id': cls.partner_customer_usd.id,
'partner_shipping_id': cls.partner_customer_usd.id,
'analytic_account_id': cls.analytic_account_2.id,
})
cls.so_line_order_project = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet3.name,
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'product_uom': cls.product_order_timesheet3.uom_id.id,
'price_unit': cls.product_order_timesheet3.list_price,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'name': cls.product_order_timesheet2.name,
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'product_uom': cls.product_order_timesheet2.uom_id.id,
'price_unit': cls.product_order_timesheet2.list_price,
'order_id': cls.sale_order_2.id,
})
def _log_timesheet_user(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_user.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def _log_timesheet_manager(self, project, unit_amount, task=False):
""" Utility method to log timesheet """
Timesheet = self.env['account.analytic.line']
return Timesheet.create({
'name': 'timesheet employee on project_so_1 only',
'account_id': project.analytic_account_id.id,
'project_id': project.id,
'employee_id': self.employee_manager.id,
'unit_amount': unit_amount,
'task_id': task.id if task else False,
})
def test_profitability_report(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.user.company_id.currency_id
rounding = currency.rounding
project_global_stat = self.env['project.profitability.report'].search([('project_id', '=', self.project_global.id)]).read()[0]
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
# confirm sales orders
self.sale_order_1.action_confirm()
self.sale_order_2.action_confirm()
project_so_1 = self.so_line_deliver_project.project_id
project_so_2 = self.so_line_order_project.project_id
task_so_1 = self.so_line_deliver_project.task_id
task_so_2 = self.so_line_order_project.task_id
task_in_global_1 = self.so_line_deliver_task.task_id
task_in_global_2 = self.so_line_order_task.task_id
# deliver project should not be impacted, as no timesheet are logged yet
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
# order project should be to invoice, but nothing has been delivered yet
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
# global project now contain 2 tasks: 'deliver' one which has no effect (no timesheet yet), and the 'order' one which should update the 'to invoice' amount
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' into account")
self.assertTrue(float_is_zero(project_global_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
# logged some timesheets: on project only, then on tasks with different employees
timesheet1 = self._log_timesheet_user(project_so_1, 2)
timesheet2 = self._log_timesheet_user(project_so_2, 2)
timesheet3 = self._log_timesheet_user(project_so_1, 3, task_so_1)
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
timesheet5 = self._log_timesheet_manager(project_so_1, 1, task_so_1)
timesheet6 = self._log_timesheet_manager(project_so_2, 1, task_so_2)
timesheet7 = self._log_timesheet_manager(self.project_global, 3, task_in_global_1)
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
# deliver project should now have cost and something to invoice
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet3.unit_amount + timesheet5.unit_amount
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_to_invoice'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The amount to invoice of the project from SO1 should only include timesheet linked to task")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit + timesheet1.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
# order project still have something to invoice but has costs now
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit + timesheet2.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], project_global_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
InvoiceWizard = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True)
# invoice the Sales Order SO1 (based on delivered qty)
context = {
"active_model": 'sale.order',
"active_ids": [self.sale_order_1.id],
"active_id": self.sale_order_1.id,
'open_invoices': True,
}
payment = InvoiceWizard.create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_id = action_invoice['res_id']
invoice_1 = self.env['account.invoice'].browse(invoice_id)
# deliver project should now have cost and something invoiced
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit + timesheet1.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
# order project has still nothing invoiced
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet4.unit_amount + timesheet6.unit_amount
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit + timesheet2.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
# global project should have more to invoice, and should now have costs
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty
project_global_invoiced = self.so_line_deliver_task.price_unit * timesheet7.unit_amount
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], project_global_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
# invoice the Sales Order SO2
context = {
"active_model": 'sale.order',
"active_ids": [self.sale_order_2.id],
"active_id": self.sale_order_2.id,
'open_invoices': True,
}
payment = InvoiceWizard.create({
'advance_payment_method': 'delivered',
})
action_invoice = payment.with_context(context).create_invoices()
invoice_id = action_invoice['res_id']
invoice_2 = self.env['account.invoice'].browse(invoice_id)
# deliver project should not be impacted by the invoice of the other SO
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit + timesheet1.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
# order project is now totally invoiced, as we are in ordered qty
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice should be the one 0.0, as all ordered quantity is invoiced")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit + timesheet2.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
# global project should be totally invoiced
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
# add expense AAL: 20% margin when reinvoicing
AnalyticLine = self.env['account.analytic.line']
expense1 = AnalyticLine.create({
'name': 'expense on project_so_1',
'account_id': project_so_1.analytic_account_id.id,
'employee_id': self.employee_user.id,
'unit_amount': 4,
'amount': 4 * self.product_expense.list_price * -1,
'product_id': self.product_expense.id,
'product_uom_id': self.product_expense.uom_id.id,
})
expense2 = AnalyticLine.create({
'name': 'expense on global project',
'account_id': self.project_global.analytic_account_id.id,
'employee_id': self.employee_user.id,
'unit_amount': 2,
'amount': 2 * self.product_expense.list_price * -1,
'product_id': self.product_expense.id,
'product_uom_id': self.product_expense.uom_id.id,
})
# deliver project should now have expense cost, and expense to reinvoice as there is a still open sales order linked to the AA1
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet3.unit_amount + timesheet5.unit_amount
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit + timesheet1.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
self.assertEqual(float_compare(project_so_1_stat['expense_amount_untaxed_to_invoice'], -1 * expense1.amount, precision_rounding=rounding), 0, "The expense cost to reinvoice of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_so_1_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0, "The expense cost of the project from SO1 should be 0.0")
# order project is not impacted by the expenses
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
project_so_2_timesheet_sold_unit = timesheet4.unit_amount + timesheet6.unit_amount
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice should be the one 0.0, as all ordered quantity is invoiced")
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit + timesheet2.unit_amount, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
# global project should have an expense, but not reinvoicable
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
self.assertEqual(float_compare(project_global_stat['expense_cost'], expense2.amount, precision_rounding=rounding), 0, "The expense cost of the global project should be 0.0")
@@ -39,24 +39,25 @@
</t>
</div>
<t t-set="display_cost" t-value="dashboard['profit']['expense_cost'] != 0.0"/>
<table class="table">
<tbody>
<tr>
<th rowspan="5">
<th rowspan="6">
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['date:month'],'pivot_column_groupby': ['timesheet_invoice_type'], 'pivot_measures': ['unit_amount']}">Hours</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['hours']['billable_time']" t-options="{'widget': 'float_time'}"/>
</td>
<td>Timesheets</td>
<th rowspan="5">
<th rowspan="6">
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['date:month', 'employee_id'], 'pivot_column_groupby': ['timesheet_invoice_type'],'pivot_measures': ['unit_amount']}">Rates</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['rates']['billable_time']"/> %
</td>
<td>Timesheets</td>
<th rowspan="5">
<th rowspan="6">
<a type="action" t-att-domain="json.dumps(timesheet_domain)" context="{'pivot_row_groupby': ['timesheet_invoice_id'], 'pivot_column_groupby': ['date:month'],'pivot_measures': ['amount', 'timesheet_revenue']}">Profitability</a>
</th>
<td class="o_timesheet_plan_dashboard_cell">
@@ -101,8 +102,18 @@
<t t-esc="dashboard['rates']['non_billable']"/> %
</td>
<td>Non billable</td>
<td class="o_timesheet_plan_dashboard_cell"></td>
<td></td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_cost']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Other costs</td>
</t>
<t t-else="">
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</t>
</tr>
<tr>
<td class="o_timesheet_plan_dashboard_total">
@@ -111,10 +122,32 @@
<td><b>Total</b></td>
<td></td>
<td></td>
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_cell">
<t t-esc="dashboard['profit']['expense_amount_untaxed_invoiced']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td>Re-invoiced costs</td>
</t>
<t t-else="">
<td></td>
<td></td>
</t>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<t t-if="display_cost">
<td class="o_timesheet_plan_dashboard_total">
<t t-esc="dashboard['profit']['total']" t-options='{"widget": "monetary", "display_currency": currency}'/>
</td>
<td><b>Total</b></td>
</t>
<t t-else="">
<td></td>
<td></td>
</t>
</tr>
</tbody>
</table>