forked from Mapan/odoo17e
379 lines
17 KiB
Python
379 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo.tests import tagged
|
|
from odoo import fields, Command
|
|
|
|
from odoo.addons.project.tests.test_project_profitability import TestProjectProfitabilityCommon
|
|
from odoo.addons.sale_subscription.tests.common_sale_subscription import TestSubscriptionCommon
|
|
|
|
|
|
@tagged('-at_install', 'post_install')
|
|
class TestSaleSubscriptionProjectProfitability(TestSubscriptionCommon, TestProjectProfitabilityCommon):
|
|
@classmethod
|
|
def setUpClass(cls, chart_template_ref=None):
|
|
super().setUpClass(chart_template_ref=chart_template_ref)
|
|
context_no_mail = {'no_reset_password': True, 'mail_create_nosubscribe': True, 'mail_create_nolog': True}
|
|
|
|
cls.project.write({
|
|
'partner_id': cls.user_portal.partner_id.id,
|
|
'company_id': False,
|
|
'analytic_account_id': cls.account_1.id,
|
|
})
|
|
|
|
cls.product_no_tax = cls.sub_product_tmpl.product_variant_id
|
|
cls.subscription_tmpl_foreign_company = cls.env['sale.order.template'].with_context(context_no_mail).create({
|
|
'name': 'Subscription template without discount',
|
|
'note': "This is the template description",
|
|
'plan_id': cls.plan_month.id,
|
|
'company_id': False,
|
|
'sale_order_template_line_ids': [Command.create({
|
|
'name': "Product 1",
|
|
'product_id': cls.product_no_tax.id,
|
|
'product_uom_qty': 1,
|
|
'product_uom_id': cls.product_no_tax.uom_id.id,
|
|
}), Command.create({
|
|
'name': "Product 2",
|
|
'product_id': cls.product_no_tax.id,
|
|
'product_uom_qty': 2,
|
|
'product_uom_id': cls.product_no_tax.uom_id.id,
|
|
})]
|
|
})
|
|
|
|
cls.subscription_foreign, cls.subscription_main_with_foreign_template = cls.env['sale.order'].with_context(context_no_mail).create([{
|
|
'name': 'TestSubscription',
|
|
'is_subscription': True,
|
|
'plan_id': cls.plan_month.id,
|
|
'note': "original subscription description",
|
|
'partner_id': cls.user_portal.partner_id.id,
|
|
'pricelist_id': cls.company_data_2['default_pricelist'].id,
|
|
'company_id': cls.company_data_2['company'].id,
|
|
'sale_order_template_id': cls.subscription_tmpl_foreign_company.id,
|
|
}, {
|
|
'name': 'TestSubscription',
|
|
'is_subscription': True,
|
|
'plan_id': cls.plan_month.id,
|
|
'note': "original subscription description",
|
|
'partner_id': cls.user_portal.partner_id.id,
|
|
'pricelist_id': cls.company_data['default_pricelist'].id,
|
|
'company_id': cls.company_data['company'].id,
|
|
'sale_order_template_id': cls.subscription_tmpl_foreign_company.id,
|
|
}])
|
|
cls.subscription_foreign._onchange_sale_order_template_id()
|
|
cls.subscription_main_with_foreign_template._onchange_sale_order_template_id()
|
|
|
|
def test_project_profitability(self):
|
|
self.account_1.company_id = False
|
|
self.project.company_id = False
|
|
|
|
foreign_company = self.company_data_2['company']
|
|
foreign_company.currency_id = self.foreign_currency
|
|
|
|
# Create and confirm a subscription with the foreign company
|
|
subscription_foreign = self.subscription_foreign.copy({'analytic_account_id': self.account_1.id}) # we work on a copy to test the whole flow
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
self.project_profitability_items_empty,
|
|
'No data should be found since the subscription is still in draft.'
|
|
)
|
|
subscription_foreign.currency_id = self.foreign_currency
|
|
subscription_foreign.order_line.price_unit = 100
|
|
subscription_foreign.action_confirm()
|
|
|
|
self.assertEqual(subscription_foreign.subscription_state, '3_progress')
|
|
self.assertEqual(len(subscription_foreign.order_line), 2)
|
|
sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type()
|
|
self.assertIn('subscriptions', sequence_per_invoice_type)
|
|
subscription_sequence = sequence_per_invoice_type['subscriptions']
|
|
new_amount_expected = subscription_foreign.recurring_monthly * subscription_foreign.sale_order_template_id.duration_value * 0.2
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
{
|
|
'revenues': {
|
|
'data': [{'id': 'subscriptions', 'sequence': subscription_sequence, 'to_invoice': new_amount_expected, 'invoiced': 0.0}],
|
|
'total': {'to_invoice': new_amount_expected, 'invoiced': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'to_bill': 0.0, 'billed': 0.0},
|
|
}
|
|
}
|
|
)
|
|
|
|
# Create and confirm a subscription with the main company and the same template as the foreign subscription
|
|
# This ensures that even if subscriptions share a template, the currency is correctly computed
|
|
subscription_main_with_foreign_template = self.subscription_main_with_foreign_template.copy({'analytic_account_id': self.account_1.id})
|
|
subscription_main_with_foreign_template.order_line.price_unit = 100
|
|
subscription_main_with_foreign_template.action_confirm()
|
|
new_amount_expected += subscription_main_with_foreign_template.recurring_monthly * subscription_main_with_foreign_template.sale_order_template_id.duration_value
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
{
|
|
'revenues': {
|
|
'data': [{'id': 'subscriptions', 'sequence': subscription_sequence, 'to_invoice': new_amount_expected, 'invoiced': 0.0}],
|
|
'total': {'to_invoice': new_amount_expected, 'invoiced': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'to_bill': 0.0, 'billed': 0.0},
|
|
}
|
|
}
|
|
)
|
|
# Confirm the main company subscription
|
|
# This ensures that subscriptions with different template are correctly computed
|
|
subscription = self.subscription.copy({'analytic_account_id': self.account_1.id}) # we work on a copy to test the whole flow
|
|
subscription.action_confirm()
|
|
new_amount_expected += subscription.recurring_monthly * subscription.sale_order_template_id.duration_value
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
{
|
|
'revenues': {
|
|
'data': [{'id': 'subscriptions', 'sequence': subscription_sequence, 'to_invoice': new_amount_expected, 'invoiced': 0.0}],
|
|
'total': {'to_invoice': new_amount_expected, 'invoiced': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'to_bill': 0.0, 'billed': 0.0},
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_project_profitability_with_subscription_without_template(self):
|
|
self.account_1.company_id = False
|
|
self.project.company_id = False
|
|
|
|
foreign_company = self.company_data_2['company']
|
|
foreign_company.currency_id = self.foreign_currency
|
|
|
|
# Create and confirm a subscription with the foreign company
|
|
subscription_foreign = self.subscription_foreign.copy({'sale_order_template_id': False, 'analytic_account_id': self.account_1.id})
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
self.project_profitability_items_empty,
|
|
'No data should be found since the subscription is still in draft.'
|
|
)
|
|
subscription_foreign.currency_id = self.foreign_currency
|
|
subscription_foreign.order_line.price_unit = 100
|
|
subscription_foreign.action_confirm()
|
|
self.assertEqual(subscription_foreign.subscription_state, '3_progress')
|
|
self.assertEqual(len(subscription_foreign.order_line), 2)
|
|
self.assertFalse(subscription_foreign.sale_order_template_id, 'No template should be set in this subscription.')
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
{
|
|
'revenues': {
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': self.project._get_profitability_sequence_per_invoice_type()['subscriptions'],
|
|
'to_invoice': subscription_foreign.recurring_monthly * 0.2,
|
|
'invoiced': 0.0,
|
|
}],
|
|
'total': {'to_invoice': subscription_foreign.recurring_monthly * 0.2, 'invoiced': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'to_bill': 0.0, 'billed': 0.0},
|
|
}
|
|
}
|
|
)
|
|
|
|
# Confirm the main company subscription
|
|
# This ensures that subscriptions with different template are correctly computed
|
|
subscription = self.subscription.copy({'sale_order_template_id': False, 'analytic_account_id': self.account_1.id})
|
|
subscription.action_confirm()
|
|
self.assertEqual(subscription.subscription_state, '3_progress')
|
|
self.assertEqual(len(subscription.order_line), 2)
|
|
self.assertFalse(subscription.sale_order_template_id, 'No template should be set in this subscription.')
|
|
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False),
|
|
{
|
|
'revenues': {
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': self.project._get_profitability_sequence_per_invoice_type()['subscriptions'],
|
|
'to_invoice': subscription_foreign.recurring_monthly * 0.2 + subscription.recurring_monthly,
|
|
'invoiced': 0.0,
|
|
}],
|
|
'total': {'to_invoice': subscription_foreign.recurring_monthly * 0.2 + subscription.recurring_monthly, 'invoiced': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'to_bill': 0.0, 'billed': 0.0},
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_recurrent_fixed_service_only_in_subscription_section(self):
|
|
"""
|
|
A recurrent service with prepaid/fixed invoicing should only be included in
|
|
the subscription section, not the "Fixed Hourly" cost. (because it is recurrent)
|
|
"""
|
|
self.project.company_id = False
|
|
|
|
foreign_company = self.company_data_2['company']
|
|
foreign_company.currency_id = self.foreign_currency
|
|
self.project.allow_billable = True
|
|
product_service_fixed_recurrent = self.product_no_tax
|
|
product_service_fixed_recurrent.write({
|
|
'name': "Recurrent Service with Prepaid/Fixed Invoicing Policy",
|
|
'service_policy': 'ordered_prepaid',
|
|
'service_tracking': 'task_global_project',
|
|
'project_id': self.project.id,
|
|
})
|
|
sale_order_foreign = self.env['sale.order'].with_context(tracking_disable=True).create({
|
|
'partner_id': self.partner_a.id,
|
|
'partner_invoice_id': self.partner_a.id,
|
|
'partner_shipping_id': self.partner_a.id,
|
|
'is_subscription': True,
|
|
'plan_id': self.plan_month.id,
|
|
'analytic_account_id': self.project.analytic_account_id.id,
|
|
'company_id': foreign_company.id,
|
|
})
|
|
self.env['sale.order.line'].with_context(tracking_disable=True).create({
|
|
'product_id': product_service_fixed_recurrent.id,
|
|
'product_uom_qty': 10,
|
|
'order_id': sale_order_foreign.id,
|
|
})
|
|
sale_order_foreign.currency_id = self.foreign_currency
|
|
sale_order_foreign.action_confirm()
|
|
# there should be only a subscription section, not the fixed/prepaid services section
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False)['revenues'],
|
|
{
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': self.project._get_profitability_sequence_per_invoice_type()['subscriptions'],
|
|
'to_invoice': sale_order_foreign.recurring_monthly * 0.2,
|
|
'invoiced': 0.0,
|
|
}],
|
|
'total': {'to_invoice': sale_order_foreign.recurring_monthly * 0.2, 'invoiced': 0.0},
|
|
},
|
|
)
|
|
|
|
sale_order = self.env['sale.order'].with_context(tracking_disable=True).create({
|
|
'partner_id': self.partner.id,
|
|
'partner_invoice_id': self.partner.id,
|
|
'partner_shipping_id': self.partner.id,
|
|
'is_subscription': True,
|
|
'plan_id': self.plan_month.id,
|
|
'analytic_account_id': self.project.analytic_account_id.id,
|
|
})
|
|
self.env['sale.order.line'].with_context(tracking_disable=True).create({
|
|
'product_id': product_service_fixed_recurrent.id,
|
|
'product_uom_qty': 10,
|
|
'order_id': sale_order.id,
|
|
})
|
|
sale_order.action_confirm()
|
|
# there should be only a subscription section, not the fixed/prepaid services section
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(False)['revenues'],
|
|
{
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': self.project._get_profitability_sequence_per_invoice_type()['subscriptions'],
|
|
'to_invoice': sale_order.recurring_monthly + sale_order_foreign.recurring_monthly * 0.2,
|
|
'invoiced': 0.0,
|
|
}],
|
|
'total': {'to_invoice': sale_order.recurring_monthly + sale_order_foreign.recurring_monthly * 0.2, 'invoiced': 0.0},
|
|
},
|
|
)
|
|
|
|
def test_project_update(self):
|
|
"""Test that the project update panel works when the project
|
|
is linked to a closed subscription that was invoiced."""
|
|
self.env.user.groups_id += self.env.ref('analytic.group_analytic_accounting')
|
|
|
|
sale_order = self.env['sale.order'].create({
|
|
'is_subscription': True,
|
|
'note': "original subscription description",
|
|
'partner_id': self.partner.id,
|
|
'analytic_account_id': self.project.analytic_account_id.id,
|
|
'plan_id': self.plan_month.id,
|
|
'end_date': fields.Date.today() + relativedelta(months=1),
|
|
})
|
|
product = self.env['product.template'].create([{
|
|
'name': 'Test Product',
|
|
'recurring_invoice': True,
|
|
'type': 'service',
|
|
'project_id': self.project.id,
|
|
'service_tracking': 'task_global_project',
|
|
}])
|
|
self.env['sale.order.line'].create({
|
|
'order_id': sale_order.id,
|
|
'product_id': product.product_variant_id.id,
|
|
})
|
|
sale_order.action_confirm()
|
|
invoice = sale_order._create_invoices()
|
|
invoice.action_post()
|
|
self.env['account.analytic.line'].create([{
|
|
'name': 'Sale',
|
|
'move_line_id': invoice.line_ids[0].id,
|
|
'account_id': self.project.analytic_account_id.id,
|
|
'currency_id': self.company_data['currency'].id,
|
|
'amount': 1,
|
|
}])
|
|
sale_order.set_close()
|
|
|
|
self.assertDictEqual(
|
|
self.project._get_profitability_items(with_action=False),
|
|
{
|
|
'revenues': {
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': 8,
|
|
'invoiced': 2.0,
|
|
'to_invoice': 0.0
|
|
}],
|
|
'total': {'invoiced': 2.0, 'to_invoice': 0.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'billed': 0.0, 'to_bill': 0.0}
|
|
}
|
|
}
|
|
)
|
|
|
|
def test_project_profitability_when_different_plan(self):
|
|
plan = self.env['account.analytic.plan'].create({'name': 'Custom Plan'})
|
|
project = self.env['project.project'].create({'name': 'Project'})
|
|
project._create_analytic_account()
|
|
project.analytic_account_id.update({'plan_id': plan.id})
|
|
|
|
sale_subscription = self.env['sale.order'].create({
|
|
'is_subscription': True,
|
|
'partner_id': self.partner.id,
|
|
'analytic_account_id': project.analytic_account_id.id,
|
|
'plan_id': self.plan_month.id,
|
|
'end_date': fields.Date.today() + relativedelta(months=1),
|
|
})
|
|
|
|
self.env['sale.order.line'].create({
|
|
'order_id': sale_subscription.id,
|
|
'product_id': self.product.product_variant_id.id,
|
|
})
|
|
sale_subscription.action_confirm()
|
|
invoice = sale_subscription._create_invoices()
|
|
invoice.action_post()
|
|
|
|
self.assertDictEqual(
|
|
project._get_profitability_items(with_action=False),
|
|
{
|
|
'revenues': {
|
|
'data': [{
|
|
'id': 'subscriptions',
|
|
'sequence': 8,
|
|
'invoiced': 1.0,
|
|
'to_invoice': 1.0
|
|
}],
|
|
'total': {'invoiced': 1.0, 'to_invoice': 1.0},
|
|
},
|
|
'costs': {
|
|
'data': [],
|
|
'total': {'billed': 0.0, 'to_bill': 0.0}
|
|
}
|
|
}
|
|
)
|