diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py
index 8d983164445..c2e51c9d7c3 100644
--- a/addons/sale_timesheet/controllers/main.py
+++ b/addons/sale_timesheet/controllers/main.py
@@ -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)
# --------------------------------------------------
diff --git a/addons/sale_timesheet/report/project_profitability_report_analysis.py b/addons/sale_timesheet/report/project_profitability_report_analysis.py
index 50eb685d103..484aefa0b92 100644
--- a/addons/sale_timesheet/report/project_profitability_report_analysis.py
+++ b/addons/sale_timesheet/report/project_profitability_report_analysis.py
@@ -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)
diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py
index 1c072a7a43e..5232437fea0 100644
--- a/addons/sale_timesheet/tests/__init__.py
+++ b/addons/sale_timesheet/tests/__init__.py
@@ -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
diff --git a/addons/sale_timesheet/tests/test_reporting.py b/addons/sale_timesheet/tests/test_reporting.py
new file mode 100644
index 00000000000..644e13e13f9
--- /dev/null
+++ b/addons/sale_timesheet/tests/test_reporting.py
@@ -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")
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index 2192583395e..171cc1dcdd3 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -39,24 +39,25 @@
+
| + | Hours |
|
Timesheets | -+ | Rates |
|
Timesheets | -+ | Profitability |
@@ -101,8 +102,18 @@
|
Non billable | -- | + |
+ |
+ Other costs | +
+ |
+ Total | +
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -111,10 +122,32 @@ | Total | - |
- |
- Total | +
+ |
+ Re-invoiced costs | ++ | + | |||||||||
| + | + | + | + |
+ |
+ Total | ++ | + |