[FIX] sale_timesheet: fix multiple issues in project profitability
This commit fixes multiple issues in project profitability report. Steps to reproduce opw-2596224: - Go to sale - Make a RFQ for a service product with service_policy set at delivered_timesheet (Timesheets on tasks), set quantity as 10 - Confirm order and create two invoices for 15%, confirm the two invoices - Create a credit note for one of the invoice and confirm it - Go back to the Sale Order and click on Project Overview => Inconsistencies (Downpayment reported twice) - Add an expense => Inconsistencies in expense amount untaxed invoiced - Add timesheets - Create an invoice from the SOL => Inconsistencies in Other costs, counting the downpayment. This issue is fixed by : - Excluding downpayments which are linked to a reversed invoice line - Use expense amount to invoice and expense amount invoiced separetely in the project overview. - Do not report not invoiced SOLs in the expense amount invoiced. - Exclude negative amounts in analytic account which are linked to credit notes. - Include all services and not timesheetes SOLs linked to project, project task or project's analytic account in the invoice/invoiced subquery. - Remove the unused downpayment column in the subqueries - Remove unused subquery (relative to downpayment) PR : #78230 opw-2596224 opw-2631163 X-original-commit: 69c5a55887d67b8852c7c45a52def38f3c742e7d Part-of: odoo/odoo#78646
This commit is contained in:
@@ -135,7 +135,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
SUM(timesheet_cost) AS timesheet_cost,
|
||||
SUM(expense_cost) AS expense_cost,
|
||||
SUM(other_revenues) AS other_revenues,
|
||||
SUM(downpayment_invoiced) AS downpayment_invoiced,
|
||||
SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
|
||||
SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
|
||||
SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
|
||||
@@ -151,7 +150,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
TS.amount AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
@@ -162,7 +160,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the other revenues
|
||||
-- Get the other revenues (products that are not services)
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
@@ -171,7 +169,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
AAL.amount AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
@@ -180,16 +177,12 @@ class ProfitabilityAnalysis(models.Model):
|
||||
FROM project_project P
|
||||
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
JOIN product_product PP ON PP.id = AAL.product_id
|
||||
JOIN product_template PT ON PT.id = PP.product_tmpl_id
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id
|
||||
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
|
||||
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
|
||||
AND P.allow_timesheets = 't'
|
||||
AND PT.service_type = 'manual' -- default value or Milestone service for services products
|
||||
AND PT.service_tracking = 'no' -- default value or not a tracking service for services products
|
||||
AND (SOL.id IS NULL
|
||||
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE))
|
||||
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -202,64 +195,32 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
AAL.amount AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
AAL.date AS line_date
|
||||
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'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the invoiced downpayments
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
MY_SOLS.id AS sale_line_id,
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
MY_S.date_order AS line_date
|
||||
FROM project_project P
|
||||
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
|
||||
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
|
||||
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
|
||||
WHERE MY_SOLS.is_downpayment = 't'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the expense costs from sale order line
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
OLIS.id AS sale_line_id,
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
OLIS.price_reduce AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
ANLI.date AS line_date
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
|
||||
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
|
||||
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
|
||||
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
|
||||
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
LEFT JOIN account_move_line RINVL ON AAL.move_id = RINVL.id
|
||||
AND RINVL.parent_state = 'posted'
|
||||
AND RINVL.exclude_from_invoice_tab = 'f'
|
||||
-- Check if the AAL is not related to a reversed credit note
|
||||
LEFT JOIN account_move RINV ON RINV.id = RINVL.move_id
|
||||
LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id
|
||||
AND INVL.parent_state = 'posted'
|
||||
AND INVL.exclude_from_invoice_tab = 'f'
|
||||
AND INVL.product_id = RINVL.product_id
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = INVL.id
|
||||
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
|
||||
AND SOL.product_id = AAL.product_id
|
||||
-- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = RINVL.id
|
||||
LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
|
||||
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
|
||||
AND SOL.id IS NULL AND SOLDOWN.id IS NULL -- Not linked to a credit note and not a downpayment
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -273,17 +234,16 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE 0.0
|
||||
END AS expense_amount_untaxed_to_invoice,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
|
||||
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced'
|
||||
THEN
|
||||
CASE
|
||||
WHEN T.expense_policy = 'sales_price'
|
||||
THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced
|
||||
THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE -AMOUNT_UNTAXED.expense_cost
|
||||
END
|
||||
ELSE 0.0
|
||||
@@ -293,31 +253,80 @@ class ProfitabilityAnalysis(models.Model):
|
||||
ELSE 0.0
|
||||
END AS amount_untaxed_to_invoice,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, AMOUNT_UNTAXED.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE 0.0
|
||||
END AS amount_untaxed_invoiced,
|
||||
S.date_order AS line_date
|
||||
FROM project_project P
|
||||
JOIN res_company C ON C.id = P.company_id
|
||||
LEFT JOIN (
|
||||
-- Gets SOL linked to timesheets
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
AAL.so_line AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 AS expense_cost
|
||||
FROM account_analytic_line AAL, project_project P
|
||||
WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
|
||||
GROUP BY P.id, AAL.so_line
|
||||
|
||||
UNION
|
||||
-- Service SOL linked to a project task AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN project_task T ON T.sale_line_id = SOL.id
|
||||
JOIN project_project P ON T.project_id = P.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
UNION
|
||||
-- Service SOL linked to project AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN project_project P ON P.sale_line_id = SOL.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id
|
||||
LEFT JOIN project_task T ON T.sale_line_id = SOL.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
UNION
|
||||
-- Service SOL linked to analytic account AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN sale_order SO ON SO.id = SOL.order_id
|
||||
JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id
|
||||
JOIN project_project P ON P.analytic_account_id = AA.id
|
||||
LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id
|
||||
LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND TSOL.id IS NULL -- not linked to a task
|
||||
AND PSOL.id IS NULL -- not linked to a project
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
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 expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 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
|
||||
@@ -328,8 +337,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
AAL.so_line AS sale_line_id,
|
||||
SUM(AAL.amount) AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
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
|
||||
@@ -339,37 +347,31 @@ class ProfitabilityAnalysis(models.Model):
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
MY_SOLS.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
|
||||
SOLDOWN.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM project_project P
|
||||
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
|
||||
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
|
||||
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
|
||||
WHERE MY_SOLS.is_downpayment = 't'
|
||||
GROUP BY P.id, MY_SOLS.id
|
||||
UNION
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
OLIS.id AS sale_line_id,
|
||||
OLIS.price_reduce AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
|
||||
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
|
||||
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
|
||||
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
|
||||
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, OLIS.id
|
||||
LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id
|
||||
LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id
|
||||
LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't'
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id
|
||||
LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id
|
||||
AND INVL.parent_state = 'posted'
|
||||
AND INVL.exclude_from_invoice_tab = 'f'
|
||||
LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id
|
||||
LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id
|
||||
AND RINVL.parent_state = 'posted'
|
||||
AND RINVL.exclude_from_invoice_tab = 'f'
|
||||
AND RINVL.product_id = SOLDOWN.product_id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0
|
||||
WHERE ANLI.id IS NULL -- there are no credit note for this downpayment
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOLDOWN.id
|
||||
UNION
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
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'
|
||||
@@ -378,8 +380,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# # Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import common_reporting
|
||||
from . import test_sale_timesheet
|
||||
from . import test_sale_service
|
||||
from . import test_project_billing
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
class TestCommonReporting(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
# 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.company_data['default_account_revenue'].id,
|
||||
})
|
||||
|
||||
# create Analytic Accounts
|
||||
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 1',
|
||||
'code': 'AA1',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 2',
|
||||
'code': 'AA2',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.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_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.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_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.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,
|
||||
})
|
||||
@@ -1,115 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero, float_compare
|
||||
from odoo.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from odoo.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestReporting(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
# 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.company_data['default_account_revenue'].id,
|
||||
})
|
||||
|
||||
# create Analytic Accounts
|
||||
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 1',
|
||||
'code': 'AA1',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 2',
|
||||
'code': 'AA2',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.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_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.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_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.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,
|
||||
})
|
||||
class TestReporting(TestCommonReporting):
|
||||
|
||||
def test_profitability_report(self):
|
||||
|
||||
@@ -397,3 +295,207 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
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")
|
||||
|
||||
def test_reversed_downpayment(self):
|
||||
# this test suppose everything is in the same currency as the current one
|
||||
currency = self.env.company.currency_id
|
||||
rounding = currency.rounding
|
||||
|
||||
self.sale_order_2.action_confirm()
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': self.sale_order_2.ids,
|
||||
'active_id': self.sale_order_2.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
'open_invoices': True,
|
||||
}
|
||||
# Let's do an invoice for a deposit of 100
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 10,
|
||||
})
|
||||
action_invoice = downpayment.create_invoices()
|
||||
invoice_id = action_invoice['res_id']
|
||||
invoice_downpayment = self.env['account.move'].browse(invoice_id)
|
||||
invoice_downpayment.action_post()
|
||||
posted_invoice_res_ids = [invoice_id]
|
||||
downpayment2 = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 25,
|
||||
})
|
||||
action_invoice = downpayment2.create_invoices()
|
||||
invoice_downpayment2 = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
|
||||
invoice_downpayment2.action_post()
|
||||
posted_invoice_res_ids += invoice_downpayment2.ids
|
||||
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
|
||||
timesheets_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice
|
||||
total_product_price = milestone_to_invoice + timesheets_to_invoice
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_ids': invoice_downpayment2.ids,
|
||||
'active_id': invoice_downpayment2.id,
|
||||
'active_model': 'account.move'
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'reason test create',
|
||||
'journal_id': invoice_downpayment2.journal_id.id,
|
||||
})
|
||||
action_moves = credit_note_wizard.reverse_moves()
|
||||
credit_id = action_moves['res_id']
|
||||
invoice_credit = self.env['account.move'].browse(credit_id)
|
||||
invoice_credit.action_post()
|
||||
posted_invoice_res_ids += invoice_credit.ids
|
||||
project_so_2 = self.so_line_order_project.project_id
|
||||
task_so_2 = self.so_line_order_project.task_id
|
||||
task_in_global_2 = self.so_line_order_task.task_id
|
||||
|
||||
self.env['project.profitability.report'].flush()
|
||||
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', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount is the amount of downpayments not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice is the milestone product minus the downpayment not reversed")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
|
||||
"The timesheet unit amount of the project from SO2 is 0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
|
||||
"The timesheet cost of the project from SO2 is 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 project from SO2 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")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# logged some timesheets: on project only, then on tasks with different employees
|
||||
timesheet2 = self._log_timesheet_user(project_so_2, 2)
|
||||
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
|
||||
timesheet6 = self._log_timesheet_manager(project_so_2, 2, task_so_2)
|
||||
self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
|
||||
|
||||
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
|
||||
so_line_expense = self.env['sale.order.line'].create({
|
||||
'name': self.product_expense.name,
|
||||
'product_id': self.product_expense.id,
|
||||
'product_uom_qty': 0.0,
|
||||
'product_uom': self.product_expense.uom_id.id,
|
||||
'price_unit': self.product_expense.list_price, # reinvoice at sales price
|
||||
'order_id': self.sale_order_2.id,
|
||||
'is_expense': True,
|
||||
})
|
||||
|
||||
expense1 = self.env['account.analytic.line'].create({
|
||||
'name': 'expense on project_so_2',
|
||||
'account_id': project_so_2.analytic_account_id.id,
|
||||
'so_line': so_line_expense.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,
|
||||
})
|
||||
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
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', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
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.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO1 should be expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# invoice the Sales Order SO2
|
||||
context = {
|
||||
"active_model": 'sale.order',
|
||||
"active_ids": self.sale_order_2.ids,
|
||||
"active_id": self.sale_order_2.id,
|
||||
'open_invoices': True,
|
||||
'mail_notrack': True,
|
||||
}
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})
|
||||
action_invoice = payment.with_context(context).create_invoices()
|
||||
invoice_payment = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
|
||||
invoice_payment.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
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', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include timesheet linked to task")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The amount to invoice of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
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 to invoice amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_invoiced'], -1 * expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost reinvoiced of the project from SO2 should be the expense amount")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_ids': invoice_payment.ids,
|
||||
'active_id': invoice_payment.id,
|
||||
'active_model': 'account.move'
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'reason test create',
|
||||
'journal_id': invoice_payment.journal_id.id,
|
||||
})
|
||||
action_moves = credit_note_wizard.reverse_moves()
|
||||
credit_id = action_moves['res_id']
|
||||
invoice_credit = self.env['account.move'].browse(credit_id)
|
||||
invoice_credit.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
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', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
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.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO1 should be expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
Reference in New Issue
Block a user