[REM] sale_timesheet: remove project profitability report

This commit is contained in:
Xavier BOL (xbo)
2022-04-01 15:33:00 +02:00
committed by Laurent Stukkens (LTU)
parent 3f098a782f
commit db065941d1
7 changed files with 0 additions and 1493 deletions
-1
View File
@@ -3,7 +3,6 @@
from . import controllers
from . import models
from . import wizard
from . import report
from odoo import api, SUPERUSER_ID
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import project_profitability_report_analysis
@@ -1,425 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, tools
class ProfitabilityAnalysis(models.Model):
_name = "project.profitability.report"
_description = "Project Profitability Report"
_order = 'project_id, sale_line_id'
_auto = False
analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True)
project_id = fields.Many2one('project.project', string='Project', readonly=True)
task_id = fields.Many2one('project.task', string='Task', readonly=True)
currency_id = fields.Many2one('res.currency', string='Project Currency', readonly=True)
company_id = fields.Many2one('res.company', string='Project Company', readonly=True)
user_id = fields.Many2one('res.users', string='Project Manager', readonly=True)
partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
line_date = fields.Date("Date", readonly=True)
# cost
timesheet_unit_amount = fields.Float("Timesheet Duration", 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 Costs", 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("Amount to Invoice", digits=(16, 2), readonly=True, group_operator="sum")
amount_untaxed_invoiced = fields.Float("Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_to_invoice = fields.Float("Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum")
expense_amount_untaxed_invoiced = fields.Float("Amount Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum")
other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum",
help="All revenues that are not from timesheets and that are linked to the analytic account of the project.")
margin = fields.Float("Margin", digits=(16, 2), readonly=True, group_operator="sum")
_depends = {
'sale.order.line': [
'order_id',
'invoice_status',
'price_reduce',
'product_id',
'qty_invoiced',
'untaxed_amount_invoiced',
'untaxed_amount_to_invoice',
'currency_id',
'company_id',
'is_downpayment',
'project_id',
'task_id',
'qty_delivered_method',
],
'sale.order': [
'date_order',
'user_id',
'partner_id',
'currency_id',
'analytic_account_id',
'order_line',
'invoice_status',
'amount_untaxed',
'currency_rate',
'company_id',
'project_id',
],
}
def init(self):
tools.drop_view_if_exists(self._cr, self._table)
query = """
CREATE VIEW %s AS (
SELECT
sub.id as id,
sub.project_id as project_id,
sub.task_id as task_id,
sub.user_id as user_id,
sub.sale_line_id as sale_line_id,
sub.analytic_account_id as analytic_account_id,
sub.partner_id as partner_id,
sub.company_id as company_id,
sub.currency_id as currency_id,
sub.sale_order_id as sale_order_id,
sub.order_confirmation_date as order_confirmation_date,
sub.product_id as product_id,
sub.sale_qty_delivered_method as sale_qty_delivered_method,
sub.expense_amount_untaxed_to_invoice as expense_amount_untaxed_to_invoice,
sub.expense_amount_untaxed_invoiced as expense_amount_untaxed_invoiced,
sub.amount_untaxed_to_invoice as amount_untaxed_to_invoice,
sub.amount_untaxed_invoiced as amount_untaxed_invoiced,
sub.timesheet_unit_amount as timesheet_unit_amount,
sub.timesheet_cost as timesheet_cost,
sub.expense_cost as expense_cost,
sub.other_revenues as other_revenues,
sub.line_date as line_date,
(sub.expense_amount_untaxed_to_invoice + sub.expense_amount_untaxed_invoiced + sub.amount_untaxed_to_invoice +
sub.amount_untaxed_invoiced + sub.other_revenues + sub.timesheet_cost + sub.expense_cost)
as margin
FROM (
SELECT
ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id,
P.id AS project_id,
P.user_id AS user_id,
SOL.id AS sale_line_id,
SOL.task_id AS task_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.qty_delivered_method AS sale_qty_delivered_method,
COST_SUMMARY.expense_amount_untaxed_to_invoice AS expense_amount_untaxed_to_invoice,
COST_SUMMARY.expense_amount_untaxed_invoiced AS expense_amount_untaxed_invoiced,
COST_SUMMARY.amount_untaxed_to_invoice AS amount_untaxed_to_invoice,
COST_SUMMARY.amount_untaxed_invoiced 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,
COST_SUMMARY.other_revenues AS other_revenues,
COST_SUMMARY.line_date::date AS line_date
FROM project_project P
JOIN res_company C ON C.id = P.company_id
LEFT JOIN (
-- Each costs and revenues will be retrieved individually by sub-requests
-- This is required to able to get the date
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,
SUM(other_revenues) AS other_revenues,
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,
SUM(amount_untaxed_invoiced) AS amount_untaxed_invoiced,
line_date AS line_date
FROM (
-- Get the timesheet costs
SELECT
P.id AS project_id,
P.analytic_account_id AS analytic_account_id,
TS.so_line AS sale_line_id,
TS.unit_amount AS timesheet_unit_amount,
TS.amount AS timesheet_cost,
0.0 AS other_revenues,
0.0 AS expense_cost,
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,
TS.date AS line_date
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'
UNION ALL
-- Get the other revenues (products that are not services)
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,
AAL.amount AS other_revenues,
0.0 AS expense_cost,
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
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 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
LEFT JOIN account_move_line AML ON AAL.move_id = AML.id
AND AML.parent_state = 'posted'
AND AML.exclude_from_invoice_tab = 'f'
-- Check if it's not a Credit Note for a Vendor Bill
LEFT JOIN account_move RBILL ON RBILL.id = AML.move_id
LEFT JOIN account_move_line BILLL ON BILLL.move_id = RBILL.reversed_entry_id
AND BILLL.parent_state = 'posted'
AND BILLL.exclude_from_invoice_tab = 'f'
AND BILLL.product_id = AML.product_id
-- Check if it's not an Invoice reversed by a Credit Note
LEFT JOIN account_move RINV ON RINV.reversed_entry_id = AML.move_id
LEFT JOIN account_move_line RINVL ON RINVL.move_id = RINV.id
AND RINVL.parent_state = 'posted'
AND RINVL.exclude_from_invoice_tab = 'f'
AND RINVL.product_id = AML.product_id
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
AND P.allow_timesheets = 't'
AND BILLL.id IS NULL
AND RINVL.id IS NULL
AND (SOL.id IS NULL
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
UNION ALL
-- Get the expense costs from account analytic line
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,
0.0 AS other_revenues,
AAL.amount AS expense_cost,
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
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 AML ON AAL.move_id = AML.id
AND AML.parent_state = 'posted'
AND AML.exclude_from_invoice_tab = 'f'
-- Check if it's not a Credit Note for an Invoice
LEFT JOIN account_move RINV ON RINV.id = AML.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 = AML.product_id
-- Check if it's not a Bill reversed by a Credit Note
LEFT JOIN account_move RBILL ON RBILL.reversed_entry_id = AML.move_id
LEFT JOIN account_move_line RBILLL ON RBILLL.move_id = RBILL.id
AND RBILLL.parent_state = 'posted'
AND RBILLL.exclude_from_invoice_tab = 'f'
AND RBILLL.product_id = AML.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 = AML.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 INVL.id IS NULL
AND RBILLL.id IS NULL
AND SOLDOWN.id IS NULL
AND P.active = 't' AND P.allow_timesheets = 't'
UNION ALL
-- Get the following values: expense amount untaxed to invoice/invoiced, amount untaxed to invoice/invoiced
-- These values have to be computed from all the records retrieved just above but grouped by project and sale order line
SELECT
AMOUNT_UNTAXED.project_id AS project_id,
AMOUNT_UNTAXED.analytic_account_id AS analytic_account_id,
AMOUNT_UNTAXED.sale_line_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 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 = 'invoiced'
THEN
CASE
WHEN T.expense_policy = 'sales_price'
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
END AS expense_amount_untaxed_invoiced,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual', 'milestones') 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 amount_untaxed_to_invoice,
CASE
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual', 'milestones') 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
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
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,
AAL.so_line AS sale_line_id,
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,
SOLDOWN.id AS sale_line_id,
0.0 AS expense_cost
FROM project_project P
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
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 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'
) AMOUNT_UNTAXED ON AMOUNT_UNTAXED.project_id = P.id
LEFT JOIN sale_order_line SOL ON AMOUNT_UNTAXED.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)
WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
) SUB_COST_SUMMARY
GROUP BY project_id, analytic_account_id, sale_line_id, line_date
) 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
WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL
) AS sub
)
""" % self._table
self._cr.execute(query)
@@ -1,5 +1,4 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_project_profitability_report_analysis_manager,project.profitability.report.analysis,model_project_profitability_report,project.group_project_manager,1,1,1,1
access_project_sale_line_employee_map,access_project_sale_line_employee_map,model_project_sale_line_employee_map,base.group_user,1,0,0,0
access_project_sale_line_employee_map_manager,access_project_sale_line_employee_map_project_manager,model_project_sale_line_employee_map,project.group_project_manager,1,1,1,1
access_project_create_sale_order,access.project.create.sale.order,model_project_create_sale_order,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_project_profitability_report_analysis_manager project.profitability.report.analysis model_project_profitability_report project.group_project_manager 1 1 1 1
2 access_project_sale_line_employee_map access_project_sale_line_employee_map model_project_sale_line_employee_map base.group_user 1 0 0 0
3 access_project_sale_line_employee_map_manager access_project_sale_line_employee_map_project_manager model_project_sale_line_employee_map project.group_project_manager 1 1 1 1
4 access_project_create_sale_order access.project.create.sale.order model_project_create_sale_order sales_team.group_sale_salesman 1 1 1 0
-2
View File
@@ -2,14 +2,12 @@
# # 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
from . import test_project_billing
from . import test_project_profitability
from . import test_reinvoice
from . import test_reporting
from . import test_project_billing_multicompany
from . import test_upsell_warning
from . import test_edit_so_line_timesheet
@@ -1,126 +0,0 @@
# -*- 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)
# Note: According to the installed module, a default pricelist is set
# on the created company (see create override on product module)
# Force the pricelist on the company to avoid having a random one
# with advanced discount configuration, etc...
cls.env['ir.property']._set_default(
'property_product_pricelist', 'res.partner',
cls.company_data['default_pricelist'],
cls.company_data['company'],
)
# 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
})
cls.analytic_account_3 = cls.env['account.analytic.account'].create({
'name': 'Test AA 3',
'code': 'AA3',
'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')
# and a third one using fixed_price (which is 'delivered')
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({
'product_id': cls.product_delivery_timesheet3.id,
'product_uom_qty': 5,
'order_id': cls.sale_order_1.id,
})
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
'product_id': cls.product_delivery_timesheet2.id,
'product_uom_qty': 7,
'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({
'product_id': cls.product_order_timesheet3.id,
'product_uom_qty': 5,
'order_id': cls.sale_order_2.id,
})
cls.so_line_order_task = cls.env['sale.order.line'].create({
'product_id': cls.product_order_timesheet2.id,
'product_uom_qty': 7,
'order_id': cls.sale_order_2.id,
})
cls.sale_order_3 = 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_3.id,
})
cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({
'product_id': cls.product_delivery_manual3.id,
'product_uom_qty': 11,
'order_id': cls.sale_order_3.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,934 +0,0 @@
# -*- 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_reporting import TestCommonReporting
from odoo.tests import tagged
@tagged('-at_install', 'post_install')
class TestReporting(TestCommonReporting):
def test_profitability_report(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# confirm sales orders
self.sale_order_1.action_confirm()
self.sale_order_2.action_confirm()
self.sale_order_3.action_confirm()
self.env['project.profitability.report'].flush()
project_so_1 = self.so_line_deliver_project.project_id
project_so_2 = self.so_line_order_project.project_id
project_so_3 = self.so_line_deliver_manual_project.project_id
task_so_1 = self.so_line_deliver_project.task_id
task_so_2 = self.so_line_order_project.task_id
task_so_3 = self.so_line_deliver_manual_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', 'other_revenues'], ['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 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 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues 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', 'other_revenues'], ['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")
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")
# fixed price project should not be impacted, as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
# global project now contain 3 tasks: 'deliver' ones which have no effect (no timesheet or delivered 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues 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)
timesheet9 = self._log_timesheet_user(project_so_3, 4, task_so_3)
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project 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', '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.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, 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")
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")
# fixed price project should ow have cost and nothing to invoice as no delivered quantity are logged yet
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
self.so_line_deliver_manual_project.write({'qty_delivered': 7.0})
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project 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', '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.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, 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")
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")
# fixed price project should ow have cost and something to invoice as some delivered quantity are logged in
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues 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.move'].browse(invoice_id)
invoice_1.action_post()
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project 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', '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.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, 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")
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")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues 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.move'].browse(invoice_id)
invoice_2.action_post()
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project 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', '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'], 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, 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")
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")
# fixed price project has still nothing invoice
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# invoice the Sales Order SO3
context = {
"active_model": 'sale.order',
"active_ids": [self.sale_order_3.id],
"active_id": self.sale_order_3.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_3 = self.env['account.move'].browse(invoice_id)
invoice_3.action_post()
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
# order project should not be impacted by the invoice of the other SO
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'], 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, 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")
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")
# fixed price project is now partially invoiced, as we are in delivered qty
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.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_3_timesheet_cost = timesheet9.amount
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_invoiced'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_delivered, precision_rounding=rounding), 0, "The invoiced amount of the project from SO3 should be the delivered quantity * the unit price")
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
# 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({
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'order_id': self.sale_order_1.id,
'is_expense': True,
})
# 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,
'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,
})
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,
})
self.env['project.profitability.report'].flush()
# 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', 'other_revenues'], ['project_id'])[0]
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + 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, 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")
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project 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', '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'], 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, 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")
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")
# global project should have an expense, but not reinvoiceable
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', 'other_revenues'], ['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")
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues 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({
'product_id': self.product_expense.id,
'product_uom_qty': 0.0,
'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")
def test_milestone_no_tracking(self):
# this test suppose everything is in the same currency as the current one
currency = self.env.company.currency_id
rounding = currency.rounding
so_line_deliver_no_task = self.env['sale.order.line'].create({
'product_id': self.product_delivery_manual1.id,
'product_uom_qty': 50,
'order_id': self.sale_order_2.id,
})
so_line_deliver_no_task.write({'qty_delivered': 1.0})
self.sale_order_2.action_confirm()
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
milestone_no_task = so_line_deliver_no_task.price_unit * so_line_deliver_no_task.qty_to_invoice
self.env['project.profitability.report'].flush()
project_so_2 = self.so_line_order_project.project_id
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.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'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice")
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 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")
task_using_milestone_not_tracked = self.env['project.task'].create({
'name': 'Task with milestone not tracked',
'project_id': project_so_2.id,
'partner_id': project_so_2.partner_id.id,
'sale_line_id': so_line_deliver_no_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.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'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
"The amount to invoice of the project from SO2 should include the milestone to invoice linked to the project or project's task")
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 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")
timesheet = self._log_timesheet_user(project_so_2, 3, task_using_milestone_not_tracked)
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 = timesheet.amount
project_so_2_timesheet_sold_unit = timesheet.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 only include downpayment not reversed")
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, 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.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")
# 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'].browse(action_invoice['res_id'])
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 + milestone_no_task, precision_rounding=rounding), 0,
"The invoiced amount of the project from SO2 should include the milestone invoiced")
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 include the milestone to invoice")
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 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")
def test_profitability_credit_note_bill(self):
"""Test whether the profitability is zeroed by credit note on a vendor bill."""
ProjectProfitabilityReport = self.env['project.profitability.report']
analytic_account = self.project_global.analytic_account_id
product = self.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Product",
'standard_price': 100.0,
'list_price': 100.0,
'taxes_id': False,
})
test_bill = self.env['account.move'].create({
'move_type': 'in_invoice',
'currency_id': self.env.user.company_id.currency_id,
'partner_id': self.partner_a,
'invoice_date': '2021-01-01',
'invoice_line_ids': [(0, 0, {
'quantity': 1,
'product_id': product.id,
'price_unit': 100.0,
'analytic_account_id': analytic_account.id,
})]
})
test_bill.action_post()
ProjectProfitabilityReport.flush()
project_stat = ProjectProfitabilityReport.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', 'other_revenues'], ['project_id'])[0]
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_cost'], test_bill.amount_total_signed, msg="The expense cost of the project should be equal to the the invoice line price, before credit note.")
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues of the project should be zero, before credit note")
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_model': 'account.move',
'active_ids': test_bill.ids,
'active_id': test_bill.id,
}).create({
'refund_method': 'cancel',
'reason': 'no reason',
'journal_id': self.company_data['default_journal_purchase'].id
})
credit_note_wizard.reverse_moves()
ProjectProfitabilityReport.flush()
project_stat = ProjectProfitabilityReport.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', 'other_revenues'], ['project_id'])[0]
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense cost of the project should be zero, as it is balanced by credit note.")
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues (credit note) of the project should be zero (not taken into account), after credit note.")
def test_profitability_credit_note_invoice(self):
"""Test whether the profitability doesn't change with customer invoice or its credit note."""
ProjectProfitabilityReport = self.env['project.profitability.report']
analytic_account = self.project_global.analytic_account_id
product = self.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
'name': "Product",
'standard_price': 100.0,
'list_price': 100.0,
'taxes_id': False,
})
test_invoice = self.env['account.move'].create({
'move_type': 'out_invoice',
'currency_id': self.env.user.company_id.currency_id,
'partner_id': self.partner_a,
'invoice_date': '2021-01-01',
'invoice_line_ids': [(0, 0, {
'quantity': 1,
'product_id': product.id,
'price_unit': 100.0,
'analytic_account_id': analytic_account.id,
})]
})
test_invoice.action_post()
ProjectProfitabilityReport.flush()
project_stat = ProjectProfitabilityReport.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', 'other_revenues'], ['project_id'])[0]
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense cost of the project should be zero, before credit note.")
self.assertAlmostEqual(project_stat['other_revenues'], test_invoice.amount_total_signed, msg="The other revenues of the project should be equal to the the invoice line price, after credit note.")
credit_note_wizard = self.env['account.move.reversal'].with_context({
'active_model': 'account.move',
'active_ids': test_invoice.ids,
'active_id': test_invoice.id,
}).create({
'refund_method': 'cancel',
'reason': 'no reason',
'journal_id': self.company_data['default_journal_sale'].id
})
credit_note_wizard.reverse_moves()
ProjectProfitabilityReport.flush()
project_stat = ProjectProfitabilityReport.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', 'other_revenues'], ['project_id'])[0]
self.assertAlmostEqual(project_stat['amount_untaxed_invoiced'], 0, msg="The invoiced amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['amount_untaxed_to_invoice'], 0, msg="The amount to invoice of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['timesheet_unit_amount'], 0, msg="The timesheet unit amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['timesheet_cost'], 0, msg="The timesheet cost of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_to_invoice'], 0, msg="The expense cost to reinvoice of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_amount_untaxed_invoiced'], 0, msg="The expense invoiced amount of the project should be zero, after credit note.")
self.assertAlmostEqual(project_stat['expense_cost'], 0, msg="The expense costs (credit note) of the project should be zero (not taken into account), after credit note.")
self.assertAlmostEqual(project_stat['other_revenues'], 0, msg="The other revenues of the project should be zero, as it is balanced by credit note.")