mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 05:11:59 +02:00
Initial commit: Sale packages
This commit is contained in:
commit
14e3d26998
6469 changed files with 2479670 additions and 0 deletions
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import test_child_tasks
|
||||
from . import test_project_profitability
|
||||
from . import test_res_config_settings
|
||||
from . import test_sale_project
|
||||
from . import test_so_line_milestones
|
||||
141
odoo-bringout-oca-ocb-sale_project/sale_project/tests/common.py
Normal file
141
odoo-bringout-oca-ocb-sale_project/sale_project/tests/common.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
|
||||
|
||||
class TestSaleProjectCommon(TestSaleCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': True}) \
|
||||
.execute()
|
||||
|
||||
cls.uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
cls.account_sale = cls.company_data['default_account_revenue']
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan Test',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
})
|
||||
cls.analytic_account_sale = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project for selling timesheet - AA',
|
||||
'code': 'AA-2030',
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
'company_id': cls.company_data['company'].id,
|
||||
})
|
||||
Project = cls.env['project.project'].with_context(tracking_disable=True)
|
||||
cls.project_global = Project.create({
|
||||
'name': 'Project Global',
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
'allow_billable': True,
|
||||
})
|
||||
cls.project_template = Project.create({
|
||||
'name': 'Project TEMPLATE for services',
|
||||
})
|
||||
cls.project_template_state = cls.env['project.task.type'].create({
|
||||
'name': 'Only stage in project template',
|
||||
'sequence': 1,
|
||||
'project_ids': [(4, cls.project_template.id)]
|
||||
})
|
||||
|
||||
# -- manual (delivered, manual)
|
||||
cls.product_delivery_manual1 = cls.env['product.product'].create({
|
||||
'name': "Service delivered, create no task",
|
||||
'standard_price': 11,
|
||||
'list_price': 13,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI1',
|
||||
'service_type': 'manual',
|
||||
'service_tracking': 'no',
|
||||
'project_id': False,
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
cls.product_delivery_manual2 = cls.env['product.product'].create({
|
||||
'name': "Service delivered, create task in global project",
|
||||
'standard_price': 30,
|
||||
'list_price': 90,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI2',
|
||||
'service_type': 'manual',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project_global.id,
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
cls.product_delivery_manual3 = cls.env['product.product'].create({
|
||||
'name': "Service delivered, create task in new project",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI3',
|
||||
'service_type': 'manual',
|
||||
'service_tracking': 'task_in_project',
|
||||
'project_id': False, # will create a project
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
cls.product_delivery_manual4 = cls.env['product.product'].create({
|
||||
'name': "Service delivered, create project only",
|
||||
'standard_price': 15,
|
||||
'list_price': 30,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI4',
|
||||
'service_type': 'manual',
|
||||
'service_tracking': 'project_only',
|
||||
'project_id': False,
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
cls.product_delivery_manual5 = cls.env['product.product'].create({
|
||||
'name': "Service delivered, create project only with template",
|
||||
'standard_price': 17,
|
||||
'list_price': 34,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-DELI4',
|
||||
'service_type': 'manual',
|
||||
'service_tracking': 'project_only',
|
||||
'project_id': False,
|
||||
'project_template_id': cls.project_template.id,
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
})
|
||||
|
||||
# -- devliered_milestones (delivered, milestones)
|
||||
product_milestone_vals = {
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': cls.uom_hour.id,
|
||||
'uom_po_id': cls.uom_hour.id,
|
||||
'default_code': 'SERV-MILES',
|
||||
'service_type': 'milestones',
|
||||
'service_tracking': 'no',
|
||||
'property_account_income_id': cls.account_sale.id,
|
||||
}
|
||||
cls.product_milestone, cls.product_milestone2 = cls.env['product.product'].create([
|
||||
{**product_milestone_vals, 'name': 'Milestone Product', 'list_price': 20},
|
||||
{**product_milestone_vals, 'name': 'Milestone Product 2', 'list_price': 15},
|
||||
])
|
||||
|
||||
def set_project_milestone_feature(self, value):
|
||||
self.env['res.config.settings'] \
|
||||
.create({'group_project_milestone': value}) \
|
||||
.execute()
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details
|
||||
|
||||
from odoo.tests.common import TransactionCase, new_test_user
|
||||
|
||||
|
||||
class TestNestedTaskUpdate(TransactionCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.partner = cls.env['res.partner'].create({'name': "Mur en béton"})
|
||||
sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
'partner_id': cls.partner.id,
|
||||
'partner_invoice_id': cls.partner.id,
|
||||
'partner_shipping_id': cls.partner.id,
|
||||
})
|
||||
product = cls.env['product.product'].create({
|
||||
'name': "Prepaid Consulting",
|
||||
'type': 'service',
|
||||
})
|
||||
cls.order_line = cls.env['sale.order.line'].create({
|
||||
'name': "Order line",
|
||||
'product_id': product.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
cls.user = new_test_user(cls.env, login='mla')
|
||||
|
||||
#----------------------------------
|
||||
#
|
||||
# When creating tasks that have a parent_id, they pick some values from their parent
|
||||
#
|
||||
#----------------------------------
|
||||
|
||||
def test_creating_subtask_user_id_on_parent_dont_go_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'user_ids': False})
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_creating_subtask_partner_id_on_parent_goes_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
child._compute_partner_id() # the compute will be triggered since the user set the parent_id.
|
||||
self.assertEqual(child.partner_id, self.user.partner_id)
|
||||
|
||||
# Another case, it is the parent as a default value
|
||||
child = self.env['project.task'].with_context(default_parent_id=parent.id).create({'name': 'child'})
|
||||
self.assertEqual(child.partner_id, self.user.partner_id)
|
||||
|
||||
def test_creating_subtask_email_from_on_parent_goes_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
self.assertEqual(child.email_from, 'a@c.be')
|
||||
|
||||
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_if_same_partner_in_values(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id, 'parent_id': parent.id})
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
parent.write({'sale_line_id': False})
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
|
||||
def test_creating_subtask_sale_line_id_on_parent_goes_on_child_with_partner_if_not_in_values(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
self.assertEqual(child.partner_id, parent.partner_id)
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
|
||||
def test_creating_subtask_sale_line_id_on_parent_dont_go_on_child_if_other_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
self.assertNotEqual(child.partner_id, parent.partner_id)
|
||||
|
||||
def test_creating_subtask_sale_line_id_on_parent_go_on_child_if_same_commercial_partner(self):
|
||||
commercial_partner = self.env['res.partner'].create({'name': "Jémémy"})
|
||||
self.partner.parent_id = commercial_partner
|
||||
self.user.partner_id.parent_id = commercial_partner
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id, 'parent_id': parent.id})
|
||||
self.assertEqual(child.sale_line_id, self.order_line, "Sale order line on parent should be transfered to child")
|
||||
self.assertNotEqual(child.partner_id, parent.partner_id)
|
||||
|
||||
#----------------------------------------
|
||||
#
|
||||
# When writing on a parent task, some values adapt on their children
|
||||
#
|
||||
#----------------------------------------
|
||||
|
||||
def test_write_user_id_on_parent_dont_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False, 'parent_id': parent.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
parent.write({'user_ids': [(4, self.user.id)]})
|
||||
self.assertFalse(child.user_ids)
|
||||
parent.write({'user_ids': False})
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_write_partner_id_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': False})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False, 'parent_id': parent.id})
|
||||
self.assertFalse(child.partner_id)
|
||||
parent.write({'partner_id': self.user.partner_id.id})
|
||||
self.assertNotEqual(child.partner_id, parent.partner_id)
|
||||
parent.write({'partner_id': False})
|
||||
self.assertNotEqual(child.partner_id, self.user.partner_id)
|
||||
|
||||
def test_write_email_from_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent'})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
self.assertFalse(child.email_from)
|
||||
parent.write({'email_from': 'a@c.be'})
|
||||
self.assertEqual(child.email_from, parent.email_from)
|
||||
parent.write({'email_from': ''})
|
||||
self.assertEqual(child.email_from, 'a@c.be')
|
||||
|
||||
def test_write_sale_line_id_on_parent_write_on_child_if_same_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.partner.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
parent.write({'sale_line_id': self.order_line.id})
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
parent.write({'sale_line_id': False})
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
|
||||
def test_write_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id})
|
||||
child._compute_partner_id()
|
||||
self.assertFalse(child.sale_line_id)
|
||||
parent.write({'sale_line_id': self.order_line.id})
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
self.assertEqual(child.partner_id, self.partner)
|
||||
parent.write({'sale_line_id': False})
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
|
||||
def test_write_sale_line_id_on_parent_dont_write_on_child_if_other_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'parent_id': parent.id, 'partner_id': self.user.partner_id.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
parent.write({'sale_line_id': self.order_line.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
|
||||
#----------------------------------
|
||||
#
|
||||
# When linking two existent task, some values go on the child
|
||||
#
|
||||
#----------------------------------
|
||||
|
||||
def test_linking_user_id_on_parent_dont_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': [(4, self.user.id)]})
|
||||
child = self.env['project.task'].create({'name': 'child', 'user_ids': False})
|
||||
self.assertFalse(child.user_ids)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertFalse(child.user_ids)
|
||||
|
||||
def test_linking_partner_id_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.user.partner_id.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
|
||||
self.assertFalse(child.partner_id)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertEqual(child.partner_id, self.user.partner_id)
|
||||
|
||||
def test_linking_email_from_on_parent_write_on_child(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'email_from': 'a@c.be'})
|
||||
child = self.env['project.task'].create({'name': 'child', 'email_from': False})
|
||||
self.assertFalse(child.email_from)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertEqual(child.email_from, 'a@c.be')
|
||||
|
||||
def test_linking_sale_line_id_on_parent_write_on_child_if_same_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.partner.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
parent.write({'sale_line_id': False})
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
|
||||
def test_linking_sale_line_id_on_parent_write_on_child_with_partner_if_not_set(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': False})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
self.assertFalse(child.partner_id)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertEqual(child.partner_id, parent.partner_id)
|
||||
self.assertEqual(child.sale_line_id, parent.sale_line_id)
|
||||
|
||||
def test_linking_sale_line_id_on_parent_write_dont_child_if_other_partner(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id})
|
||||
child = self.env['project.task'].create({'name': 'child', 'partner_id': self.user.partner_id.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
self.assertNotEqual(child.partner_id, parent.partner_id)
|
||||
child.write({'parent_id': parent.id})
|
||||
self.assertFalse(child.sale_line_id)
|
||||
|
||||
def test_writing_on_parent_with_multiple_tasks(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'user_ids': False, 'partner_id': self.partner.id})
|
||||
children_values = [{'name': 'child%s' % i, 'user_ids': False, 'parent_id': parent.id} for i in range(5)]
|
||||
children = self.env['project.task'].create(children_values)
|
||||
children._compute_partner_id()
|
||||
# test writing sale_line_id
|
||||
for child in children:
|
||||
self.assertFalse(child.sale_line_id)
|
||||
parent.write({'sale_line_id': self.order_line.id})
|
||||
for child in children:
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
|
||||
def test_linking_on_parent_with_multiple_tasks(self):
|
||||
parent = self.env['project.task'].create({'name': 'parent', 'partner_id': self.partner.id, 'sale_line_id': self.order_line.id, 'user_ids': [(4, self.user.id)]})
|
||||
children_values = [{'name': 'child%s' % i, 'user_ids': False} for i in range(5)]
|
||||
children = self.env['project.task'].create(children_values)
|
||||
# test writing user_ids and sale_line_id
|
||||
|
||||
for child in children:
|
||||
self.assertFalse(child.user_ids)
|
||||
self.assertFalse(child.sale_line_id)
|
||||
|
||||
children.write({'parent_id': parent.id})
|
||||
|
||||
for child in children:
|
||||
self.assertEqual(child.sale_line_id, self.order_line)
|
||||
self.assertFalse(child.user_ids)
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.addons.project.tests.test_project_profitability import TestProjectProfitabilityCommon as Common
|
||||
|
||||
|
||||
class TestProjectProfitabilityCommon(Common):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
uom_unit_id = cls.env.ref('uom.product_uom_unit').id
|
||||
|
||||
# Create material product
|
||||
cls.material_product = cls.env['product.product'].create({
|
||||
'name': 'Material',
|
||||
'type': 'consu',
|
||||
'standard_price': 5,
|
||||
'list_price': 10,
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': uom_unit_id,
|
||||
'uom_po_id': uom_unit_id,
|
||||
})
|
||||
|
||||
# Create service products
|
||||
uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
cls.product_delivery_service = cls.env['product.product'].create({
|
||||
'name': "Service Delivery, create task in global project",
|
||||
'standard_price': 30,
|
||||
'list_price': 90,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'service_type': 'manual',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED2',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project.id,
|
||||
})
|
||||
cls.sale_order = cls.env['sale.order'].with_context(tracking_disable=True).create({
|
||||
'partner_id': cls.partner.id,
|
||||
'partner_invoice_id': cls.partner.id,
|
||||
'partner_shipping_id': cls.partner.id,
|
||||
})
|
||||
SaleOrderLine = cls.env['sale.order.line'].with_context(tracking_disable=True, default_order_id=cls.sale_order.id)
|
||||
cls.delivery_service_order_line = SaleOrderLine.create({
|
||||
'product_id': cls.product_delivery_service.id,
|
||||
'product_uom_qty': 10,
|
||||
})
|
||||
cls.sale_order.action_confirm()
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleProjectProfitability(TestProjectProfitabilityCommon, TestSaleCommon):
|
||||
def test_project_profitability(self):
|
||||
self.assertFalse(self.project.allow_billable, 'The project should be non billable.')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
'No data for the project profitability should be found since the project is not billable, so no SOL is linked to the project.'
|
||||
)
|
||||
self.project.write({'allow_billable': True})
|
||||
self.assertTrue(self.project.allow_billable, 'The project should be billable.')
|
||||
self.project.sale_line_id = self.delivery_service_order_line
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
'No data for the project profitability should be found since no product is delivered in the SO linked.'
|
||||
)
|
||||
self.delivery_service_order_line.qty_delivered = 1
|
||||
service_policy_to_invoice_type = self.project._get_service_policy_to_invoice_type()
|
||||
invoice_type = service_policy_to_invoice_type[self.delivery_service_order_line.product_id.service_policy]
|
||||
self.assertIn(
|
||||
invoice_type,
|
||||
['billable_manual', 'service_revenues'],
|
||||
'invoice_type="billable_manual" if sale_timesheet is installed otherwise it is equal to "service_revenues"')
|
||||
sequence_per_invoice_type = self.project._get_profitability_sequence_per_invoice_type()
|
||||
self.assertIn('service_revenues', sequence_per_invoice_type)
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{
|
||||
# id should be equal to "billable_manual" if "sale_timesheet" module is installed otherwise "service_revenues"
|
||||
'id': invoice_type,
|
||||
'sequence': sequence_per_invoice_type[invoice_type],
|
||||
'to_invoice': self.delivery_service_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_invoice': self.delivery_service_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
},
|
||||
'costs': {
|
||||
'data': [],
|
||||
'total': {'billed': 0.0, 'to_bill': 0.0},
|
||||
},
|
||||
}
|
||||
)
|
||||
self.assertNotEqual(self.delivery_service_order_line.untaxed_amount_to_invoice, 0.0)
|
||||
self.assertEqual(self.delivery_service_order_line.untaxed_amount_invoiced, 0.0)
|
||||
|
||||
# create an invoice
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': self.sale_order.ids,
|
||||
'active_id': self.sale_order.id,
|
||||
}
|
||||
invoices = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})._create_invoices(self.sale_order)
|
||||
invoices.action_post()
|
||||
|
||||
self.assertEqual(self.delivery_service_order_line.qty_invoiced, 1)
|
||||
self.assertEqual(self.delivery_service_order_line.untaxed_amount_to_invoice, 0.0)
|
||||
self.assertNotEqual(self.delivery_service_order_line.untaxed_amount_invoiced, 0.0)
|
||||
invoice_type = service_policy_to_invoice_type[self.delivery_service_order_line.product_id.service_policy]
|
||||
self.assertIn(
|
||||
invoice_type,
|
||||
['billable_manual', 'service_revenues'],
|
||||
'invoice_type="billable_manual" if sale_timesheet is installed otherwise it is equal to "service_revenues"')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{
|
||||
'id': invoice_type,
|
||||
'sequence': sequence_per_invoice_type[invoice_type],
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': self.delivery_service_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
},
|
||||
'costs': {
|
||||
'data': [],
|
||||
'total': {'billed': 0.0, 'to_bill': 0.0},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Add 2 sales order items in the SO
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True, default_order_id=self.sale_order.id)
|
||||
manual_service_order_line = SaleOrderLine.create({
|
||||
'product_id': self.product_delivery_service.id,
|
||||
'product_uom_qty': 5,
|
||||
'qty_delivered': 5,
|
||||
})
|
||||
material_order_line = SaleOrderLine.create({
|
||||
'product_id': self.material_product.id,
|
||||
'product_uom_qty': 1,
|
||||
'qty_delivered': 1,
|
||||
})
|
||||
service_sols = self.delivery_service_order_line + manual_service_order_line
|
||||
invoice_type = service_policy_to_invoice_type[manual_service_order_line.product_id.service_policy]
|
||||
self.assertIn(
|
||||
invoice_type,
|
||||
['billable_manual', 'service_revenues'],
|
||||
'invoice_type="billable_manual" if sale_timesheet is installed otherwise it is equal to "service_revenues"')
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{
|
||||
'id': invoice_type,
|
||||
'sequence': sequence_per_invoice_type[invoice_type],
|
||||
'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')),
|
||||
'invoiced': sum(service_sols.mapped('untaxed_amount_invoiced')),
|
||||
},
|
||||
{
|
||||
'id': 'other_revenues',
|
||||
'sequence': sequence_per_invoice_type['other_revenues'],
|
||||
'to_invoice': material_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': material_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')) + material_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': sum(service_sols.mapped('untaxed_amount_invoiced')) + material_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
},
|
||||
'costs': { # no cost because we have no purchase orders.
|
||||
'data': [],
|
||||
'total': {'billed': 0.0, 'to_bill': 0.0},
|
||||
},
|
||||
},
|
||||
)
|
||||
self.assertNotEqual(manual_service_order_line.untaxed_amount_to_invoice, 0.0)
|
||||
self.assertEqual(manual_service_order_line.untaxed_amount_invoiced, 0.0)
|
||||
self.assertNotEqual(material_order_line.untaxed_amount_to_invoice, 0.0)
|
||||
self.assertEqual(material_order_line.untaxed_amount_invoiced, 0.0)
|
||||
|
||||
credit_notes = invoices._reverse_moves()
|
||||
credit_notes.action_post()
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
{
|
||||
'revenues': {
|
||||
'data': [
|
||||
{
|
||||
'id': invoice_type,
|
||||
'sequence': sequence_per_invoice_type[invoice_type],
|
||||
'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')),
|
||||
'invoiced': manual_service_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
{
|
||||
'id': 'other_revenues',
|
||||
'sequence': sequence_per_invoice_type['other_revenues'],
|
||||
'to_invoice': material_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': material_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
],
|
||||
'total': {
|
||||
'to_invoice': sum(service_sols.mapped('untaxed_amount_to_invoice')) + material_order_line.untaxed_amount_to_invoice,
|
||||
'invoiced': manual_service_order_line.untaxed_amount_invoiced + material_order_line.untaxed_amount_invoiced,
|
||||
},
|
||||
},
|
||||
'costs': { # no cost because we have no purchase orders.
|
||||
'data': [],
|
||||
'total': {'billed': 0.0, 'to_bill': 0.0},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
self.sale_order._action_cancel()
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False),
|
||||
self.project_profitability_items_empty,
|
||||
)
|
||||
|
||||
def test_invoices_without_sale_order_are_accounted_in_profitability(self):
|
||||
"""
|
||||
An invoice that has an AAL on one of its line should be taken into account
|
||||
for the profitability of the project.
|
||||
The contribution of the line should only be dependent
|
||||
on the project's analytic account % that was set on the line
|
||||
"""
|
||||
self.project.allow_billable = True
|
||||
# a custom analytic contribution (number between 1 -> 100 included)
|
||||
analytic_distribution = 42
|
||||
analytic_contribution = analytic_distribution / 100.
|
||||
# create a invoice_1 with the AAL
|
||||
invoice_1 = self.env['account.move'].create({
|
||||
"name": "Invoice_1",
|
||||
"move_type": "out_invoice",
|
||||
"state": "draft",
|
||||
"partner_id": self.partner.id,
|
||||
"invoice_date": datetime.today(),
|
||||
"invoice_line_ids": [Command.create({
|
||||
"analytic_distribution": {self.analytic_account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
})],
|
||||
})
|
||||
# the bill_1 is in draft, therefor it should have the cost "to_invoice" same as the -product_price (untaxed)
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': self.product_a.standard_price * analytic_contribution,
|
||||
'invoiced': 0.0,
|
||||
}],
|
||||
'total': {'to_invoice': self.product_a.standard_price * analytic_contribution, 'invoiced': 0.0},
|
||||
},
|
||||
)
|
||||
# post invoice_1
|
||||
invoice_1.action_post()
|
||||
# we posted the invoice_1, therefore the revenue "invoiced" should be -product_price, to_invoice should be back to 0
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': self.product_a.standard_price * analytic_contribution,
|
||||
}],
|
||||
'total': {'to_invoice': 0.0, 'invoiced': self.product_a.standard_price * analytic_contribution},
|
||||
},
|
||||
)
|
||||
# create another invoice, with 2 lines, 2 diff products, the second line has 2 as quantity
|
||||
invoice_2 = self.env['account.move'].create({
|
||||
"name": "I have 2 lines",
|
||||
"move_type": "out_invoice",
|
||||
"state": "draft",
|
||||
"partner_id": self.partner.id,
|
||||
"invoice_date": datetime.today(),
|
||||
"invoice_line_ids": [Command.create({
|
||||
"analytic_distribution": {self.analytic_account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": self.product_a.standard_price,
|
||||
}), Command.create({
|
||||
"analytic_distribution": {self.analytic_account.id: analytic_distribution},
|
||||
"product_id": self.product_b.id,
|
||||
"quantity": 2,
|
||||
"product_uom_id": self.product_b.uom_id.id,
|
||||
"price_unit": self.product_b.standard_price,
|
||||
})],
|
||||
})
|
||||
# invoice_2 is not posted, therefor its cost should be "to_invoice" = - sum of all product_price * qty for each line
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution,
|
||||
'invoiced': self.product_a.standard_price * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_invoice': (self.product_a.standard_price + 2 * self.product_b.standard_price) * analytic_contribution,
|
||||
'invoiced': self.product_a.standard_price * analytic_contribution,
|
||||
},
|
||||
},
|
||||
)
|
||||
# post invoice_2
|
||||
invoice_2.action_post()
|
||||
# invoice_2 is posted, therefor its revenue should be counting in "invoiced", with the revenues from invoice_1
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution,
|
||||
},
|
||||
},
|
||||
)
|
||||
# invoice with negative subtotal on move line
|
||||
NEG_AMOUNT = -42
|
||||
invoice_3 = self.env['account.move'].create({
|
||||
"name": "I am negative",
|
||||
"move_type": "out_invoice",
|
||||
"state": "draft",
|
||||
"partner_id": self.partner.id,
|
||||
"invoice_date": datetime.today(),
|
||||
"invoice_line_ids": [Command.create({
|
||||
"analytic_distribution": {self.analytic_account.id: analytic_distribution},
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": NEG_AMOUNT,
|
||||
}), Command.create({
|
||||
"product_id": self.product_a.id,
|
||||
"quantity": 1,
|
||||
"product_uom_id": self.product_a.uom_id.id,
|
||||
"price_unit": -NEG_AMOUNT, # so the invoice is not negative and we can post it
|
||||
})],
|
||||
})
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': NEG_AMOUNT * analytic_contribution,
|
||||
'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_invoice': NEG_AMOUNT * analytic_contribution,
|
||||
'invoiced': 2 * (self.product_a.standard_price + self.product_b.standard_price) * analytic_contribution,
|
||||
},
|
||||
},
|
||||
)
|
||||
invoice_3.action_post()
|
||||
self.assertDictEqual(
|
||||
self.project._get_profitability_items(False)['revenues'],
|
||||
{
|
||||
'data': [{
|
||||
'id': 'other_invoice_revenues',
|
||||
'sequence': self.project._get_profitability_sequence_per_invoice_type()['other_invoice_revenues'],
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': (2 * (self.product_a.standard_price + self.product_b.standard_price) + NEG_AMOUNT) * analytic_contribution,
|
||||
}],
|
||||
'total': {
|
||||
'to_invoice': 0.0,
|
||||
'invoiced': (2 * (self.product_a.standard_price + self.product_b.standard_price) + NEG_AMOUNT) * analytic_contribution,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests import tagged
|
||||
|
||||
from .common import TestSaleProjectCommon
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestResConfigSettings(TestSaleProjectCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_b.id,
|
||||
'partner_invoice_id': cls.partner_b.id,
|
||||
'partner_shipping_id': cls.partner_b.id,
|
||||
'order_line': [
|
||||
Command.create({
|
||||
'product_id': cls.product_milestone.id,
|
||||
'product_uom_qty': 3,
|
||||
}),
|
||||
Command.create({
|
||||
'product_id': cls.product_delivery_manual1.id,
|
||||
'product_uom_qty': 2,
|
||||
})
|
||||
]
|
||||
})
|
||||
cls.product_milestone_sale_line = cls.sale_order.order_line.filtered(lambda sol: sol.product_id == cls.product_milestone)
|
||||
cls.product_delivery_manual1_sale_line = cls.sale_order.order_line.filtered(lambda sol: sol.product_id == cls.product_delivery_manual1)
|
||||
cls.sale_order.action_confirm()
|
||||
|
||||
cls.milestone = cls.env['project.milestone'].create({
|
||||
'name': 'Test Milestone',
|
||||
'sale_line_id': cls.product_milestone_sale_line.id,
|
||||
'project_id': cls.project_global.id,
|
||||
})
|
||||
|
||||
def test_disable_and_enable_project_milestone_feature(self):
|
||||
self.assertTrue(self.env.user.has_group('project.group_project_milestone'), 'The Project Milestones feature should be enabled.')
|
||||
|
||||
self.set_project_milestone_feature(False)
|
||||
self.assertFalse(self.env.user.has_group('project.group_project_milestone'), 'The Project Milestones feature should be disabled.')
|
||||
product_milestones = self.product_milestone + self.product_milestone2
|
||||
self.assertEqual(
|
||||
product_milestones.mapped('service_policy'),
|
||||
['delivered_manual'] * 2,
|
||||
'Both milestone products should become a manual product when the project milestones feature is disabled')
|
||||
self.assertEqual(
|
||||
product_milestones.mapped('service_type'),
|
||||
['manual'] * 2,
|
||||
'Both milestone products should become a manual product when the project milestones feature is disabled')
|
||||
self.assertEqual(
|
||||
self.product_milestone_sale_line.qty_delivered_method,
|
||||
'manual',
|
||||
'The quantity delivered method of SOL containing milestone product should be changed to manual when the project milestones feature is disabled')
|
||||
|
||||
# Since the quantity delivered manual is manual then the user can manually change the quantity delivered
|
||||
self.product_milestone_sale_line.qty_delivered = 2
|
||||
|
||||
# Enable the project milestones feature
|
||||
self.set_project_milestone_feature(True)
|
||||
|
||||
self.assertEqual(
|
||||
self.product_milestone.service_policy,
|
||||
'delivered_milestones',
|
||||
'The product has been updated and considered as milestones product since a SOL containing this product is linked to a milestone.')
|
||||
self.assertEqual(
|
||||
self.product_milestone.service_type,
|
||||
'milestones',
|
||||
'The product has been updated and considered as milestones product since a SOL containing this product is linked to a milestone.')
|
||||
self.assertEqual(
|
||||
self.product_milestone2.service_policy,
|
||||
'delivered_manual',
|
||||
'The product should not be updated since we cannot assume this product was a milestone when the feature'
|
||||
' was enabled because no SOL with this product is linked to a milestone.')
|
||||
self.assertEqual(
|
||||
self.product_milestone2.service_type,
|
||||
'manual',
|
||||
'The product should not be updated since we cannot assume this product was a milestone when the feature'
|
||||
' was enabled because no SOL with this product is linked to a milestone.')
|
||||
self.assertEqual(
|
||||
self.product_milestone_sale_line.qty_delivered_method,
|
||||
'manual',
|
||||
'The quantity delivered method of SOL containing milestone product should keep the same quantity delivered method even if the project milestones feature is renabled.')
|
||||
self.assertEqual(self.product_milestone_sale_line.qty_delivered, 2, 'The quantity delivered should be the one set by the user.')
|
||||
|
|
@ -0,0 +1,334 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import Command
|
||||
from odoo.tests.common import users
|
||||
from odoo.addons.base.tests.common import TransactionCaseWithUserDemo
|
||||
|
||||
|
||||
class TestSaleProject(TransactionCaseWithUserDemo):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.analytic_plan = cls.env['account.analytic.plan'].create({
|
||||
'name': 'Plan Test',
|
||||
'company_id': False,
|
||||
})
|
||||
cls.analytic_account_sale = cls.env['account.analytic.account'].create({
|
||||
'name': 'Project for selling timesheet - AA',
|
||||
'plan_id': cls.analytic_plan.id,
|
||||
'code': 'AA-2030'
|
||||
})
|
||||
|
||||
# Create projects
|
||||
cls.project_global = cls.env['project.project'].create({
|
||||
'name': 'Global Project',
|
||||
'analytic_account_id': cls.analytic_account_sale.id,
|
||||
'allow_billable': True,
|
||||
})
|
||||
cls.project_template = cls.env['project.project'].create({
|
||||
'name': 'Project TEMPLATE for services',
|
||||
})
|
||||
cls.project_template_state = cls.env['project.task.type'].create({
|
||||
'name': 'Only stage in project template',
|
||||
'sequence': 1,
|
||||
'project_ids': [(4, cls.project_template.id)]
|
||||
})
|
||||
|
||||
# Create service products
|
||||
uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
|
||||
cls.product_order_service1 = cls.env['product.product'].create({
|
||||
'name': "Service Ordered, create no task",
|
||||
'standard_price': 11,
|
||||
'list_price': 13,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED1',
|
||||
'service_tracking': 'no',
|
||||
'project_id': False,
|
||||
})
|
||||
cls.product_order_service2 = cls.env['product.product'].create({
|
||||
'name': "Service Ordered, create task in global project",
|
||||
'standard_price': 30,
|
||||
'list_price': 90,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED2',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id': cls.project_global.id,
|
||||
})
|
||||
cls.product_order_service3 = cls.env['product.product'].create({
|
||||
'name': "Service Ordered, create task in new project",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED3',
|
||||
'service_tracking': 'task_in_project',
|
||||
'project_id': False, # will create a project
|
||||
})
|
||||
cls.product_order_service4 = cls.env['product.product'].create({
|
||||
'name': "Service Ordered, create project only",
|
||||
'standard_price': 15,
|
||||
'list_price': 30,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'SERV-ORDERED4',
|
||||
'service_tracking': 'project_only',
|
||||
'project_id': False,
|
||||
})
|
||||
|
||||
# Create partner
|
||||
cls.partner = cls.env['res.partner'].create({'name': "Mur en béton"})
|
||||
|
||||
def test_sale_order_with_project_task(self):
|
||||
SaleOrder = self.env['sale.order'].with_context(tracking_disable=True)
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||||
|
||||
sale_order = SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
'partner_invoice_id': self.partner.id,
|
||||
'partner_shipping_id': self.partner.id,
|
||||
})
|
||||
so_line_order_no_task = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service1.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
so_line_order_task_in_global = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service2.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
so_line_order_new_task_new_project = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service3.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
so_line_order_only_project = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service4.id,
|
||||
'product_uom_qty': 10,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
|
||||
# service_tracking 'no'
|
||||
self.assertFalse(so_line_order_no_task.project_id, "The project should not be linked to no task product")
|
||||
self.assertFalse(so_line_order_no_task.task_id, "The task should not be linked to no task product")
|
||||
# service_tracking 'task_global_project'
|
||||
self.assertFalse(so_line_order_task_in_global.project_id, "Only task should be created, project should not be linked")
|
||||
self.assertEqual(self.project_global.tasks.sale_line_id, so_line_order_task_in_global, "Global project's task should be linked to so line")
|
||||
# service_tracking 'task_in_project'
|
||||
self.assertTrue(so_line_order_new_task_new_project.project_id, "Sales order line should be linked to newly created project")
|
||||
self.assertTrue(so_line_order_new_task_new_project.task_id, "Sales order line should be linked to newly created task")
|
||||
# service_tracking 'project_only'
|
||||
self.assertFalse(so_line_order_only_project.task_id, "Task should not be created")
|
||||
self.assertTrue(so_line_order_only_project.project_id, "Sales order line should be linked to newly created project")
|
||||
|
||||
self.assertEqual(self.project_global._get_sale_order_items(), self.project_global.sale_line_id | self.project_global.tasks.sale_line_id, 'The _get_sale_order_items should returns all the SOLs linked to the project and its active tasks.')
|
||||
|
||||
sale_order_2 = SaleOrder.create({
|
||||
'partner_id': self.partner.id,
|
||||
'partner_invoice_id': self.partner.id,
|
||||
'partner_shipping_id': self.partner.id,
|
||||
})
|
||||
sale_line_1_order_2 = SaleOrderLine.create({
|
||||
'product_id': self.product_order_service1.id,
|
||||
'product_uom_qty': 10,
|
||||
'product_uom': self.product_order_service1.uom_id.id,
|
||||
'price_unit': self.product_order_service1.list_price,
|
||||
'order_id': sale_order_2.id,
|
||||
})
|
||||
section_sale_line_order_2 = SaleOrderLine.create({
|
||||
'display_type': 'line_section',
|
||||
'name': 'Test Section',
|
||||
'order_id': sale_order_2.id,
|
||||
})
|
||||
note_sale_line_order_2 = SaleOrderLine.create({
|
||||
'display_type': 'line_note',
|
||||
'name': 'Test Note',
|
||||
'order_id': sale_order_2.id,
|
||||
})
|
||||
sale_order_2.action_confirm()
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Task',
|
||||
'sale_line_id': sale_line_1_order_2.id,
|
||||
'project_id': self.project_global.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id, sale_line_1_order_2)
|
||||
self.assertIn(task.sale_line_id, self.project_global._get_sale_order_items())
|
||||
self.assertEqual(self.project_global._get_sale_orders(), sale_order | sale_order_2)
|
||||
|
||||
sale_order_lines = sale_order.order_line + sale_line_1_order_2 # exclude the Section and Note Sales Order Items
|
||||
sale_items_data = self.project_global._get_sale_items(with_action=False)
|
||||
self.assertEqual(sale_items_data['total'], len(sale_order_lines - so_line_order_new_task_new_project - so_line_order_only_project),
|
||||
"Should be all the sale items linked to the global project.")
|
||||
expected_sale_line_dict = {
|
||||
sol_read['id']: sol_read
|
||||
for sol_read in sale_order_lines.read(['display_name', 'product_uom_qty', 'qty_delivered', 'qty_invoiced', 'product_uom'])
|
||||
}
|
||||
actual_sol_ids = []
|
||||
for line in sale_items_data['data']:
|
||||
sol_id = line['id']
|
||||
actual_sol_ids.append(sol_id)
|
||||
self.assertIn(sol_id, expected_sale_line_dict)
|
||||
self.assertDictEqual(line, expected_sale_line_dict[sol_id])
|
||||
self.assertNotIn(section_sale_line_order_2.id, actual_sol_ids, 'The section Sales Order Item should not be takken into account in the Sales section of project.')
|
||||
self.assertNotIn(note_sale_line_order_2.id, actual_sol_ids, 'The note Sales Order Item should not be takken into account in the Sales section of project.')
|
||||
|
||||
def test_sol_product_type_update(self):
|
||||
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,
|
||||
})
|
||||
self.product_order_service3.type = 'consu'
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'order_id': sale_order.id,
|
||||
'name': self.product_order_service3.name,
|
||||
'product_id': self.product_order_service3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': self.product_order_service3.uom_id.id,
|
||||
'price_unit': self.product_order_service3.list_price
|
||||
})
|
||||
self.assertFalse(sale_order_line.is_service, "As the product is consumable, the SOL should not be a service")
|
||||
|
||||
self.product_order_service3.type = 'service'
|
||||
self.assertTrue(sale_order_line.is_service, "As the product is a service, the SOL should be a service")
|
||||
|
||||
@users('demo')
|
||||
def test_cancel_so_linked_to_project(self):
|
||||
""" Test that cancelling a SO linked to a project will not raise an error """
|
||||
# Ensure user don't have edit right access to the project
|
||||
group_sale_manager = self.env.ref('sales_team.group_sale_manager')
|
||||
group_project_user = self.env.ref('project.group_project_user')
|
||||
self.env.user.write({'groups_id': [(6, 0, [group_sale_manager.id, group_project_user.id])]})
|
||||
|
||||
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,
|
||||
'project_id': self.project_global.id,
|
||||
})
|
||||
sale_order_line = self.env['sale.order.line'].create({
|
||||
'name': self.product_order_service2.name,
|
||||
'product_id': self.product_order_service2.id,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
self.assertFalse(self.project_global.tasks.sale_line_id, "The project tasks should not be linked to the SOL")
|
||||
|
||||
sale_order.action_confirm()
|
||||
self.assertEqual(self.project_global.tasks.sale_line_id.id, sale_order_line.id, "The project tasks should be linked to the SOL from the SO")
|
||||
|
||||
self.project_global.sale_line_id = sale_order_line
|
||||
sale_order.with_context({'disable_cancel_warning': True}).action_cancel()
|
||||
self.assertFalse(self.project_global.sale_line_id, "The project should not be linked to the SOL anymore")
|
||||
|
||||
def test_create_task_from_template_line(self):
|
||||
"""
|
||||
When we add an SOL from a template that is a service that has a service_policy that will generate a task,
|
||||
even if default_task_id is present in the context, a new task should be created when confirming the SO.
|
||||
"""
|
||||
default_task = self.env['project.task'].with_context(tracking_disable=True).create({
|
||||
'name': 'Task',
|
||||
'project_id': self.project_global.id
|
||||
})
|
||||
sale_order = self.env['sale.order'].with_context(tracking_disable=True, default_task_id=default_task.id).create({
|
||||
'partner_id': self.partner.id,
|
||||
})
|
||||
quotation_template = self.env['sale.order.template'].create({
|
||||
'name': 'Test quotation',
|
||||
})
|
||||
quotation_template.write({
|
||||
'sale_order_template_line_ids': [
|
||||
Command.set(
|
||||
self.env['sale.order.template.line'].create([{
|
||||
'name': self.product_order_service2.display_name,
|
||||
'sale_order_template_id': quotation_template.id,
|
||||
'product_id': self.product_order_service2.id,
|
||||
'product_uom_id': self.product_order_service2.uom_id.id,
|
||||
}, {
|
||||
'name': self.product_order_service3.display_name,
|
||||
'sale_order_template_id': quotation_template.id,
|
||||
'product_id': self.product_order_service3.id,
|
||||
'product_uom_id': self.product_order_service3.uom_id.id,
|
||||
}]).ids
|
||||
)
|
||||
]
|
||||
})
|
||||
sale_order.with_context(default_task_id=default_task.id).write({
|
||||
'sale_order_template_id': quotation_template.id,
|
||||
})
|
||||
sale_order.with_context(default_task_id=default_task.id)._onchange_sale_order_template_id()
|
||||
self.assertFalse(sale_order.order_line.mapped('task_id'),
|
||||
"SOL should have no related tasks, because they are from services that generates a task")
|
||||
sale_order.action_confirm()
|
||||
self.assertEqual(sale_order.tasks_count, 2, "SO should have 2 related tasks")
|
||||
self.assertNotIn(default_task, sale_order.tasks_ids, "SO should link to the default task from the context")
|
||||
|
||||
|
||||
def test_include_archived_projects_in_stat_btn_related_view(self):
|
||||
"""Checks if the project stat-button action includes both archived and active projects."""
|
||||
# Setup
|
||||
project_A = self.env['project.project'].create({'name': 'Project_A'})
|
||||
project_B = self.env['project.project'].create({'name': 'Project_B'})
|
||||
|
||||
product_A = self.env['product.product'].create({
|
||||
'name': 'product A',
|
||||
'list_price': 1.0,
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id':project_A.id,
|
||||
})
|
||||
product_B = self.env['product.product'].create({
|
||||
'name': 'product B',
|
||||
'list_price': 2.0,
|
||||
'type': 'service',
|
||||
'service_tracking': 'task_global_project',
|
||||
'project_id':project_B.id,
|
||||
})
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
SaleOrderLine = self.env['sale.order.line'].with_context(tracking_disable=True)
|
||||
SaleOrderLine.create({
|
||||
'name': product_A.name,
|
||||
'product_id': product_A.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': product_A.list_price,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
SaleOrderLine.create({
|
||||
'name': product_B.name,
|
||||
'product_id': product_B.id,
|
||||
'product_uom_qty': 10,
|
||||
'price_unit': product_B.list_price,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
|
||||
# Check if button action includes both projects BEFORE archivization
|
||||
action = sale_order.action_view_project_ids()
|
||||
self.assertEqual(len(action['domain'][0][2]), 2, "Domain should contain 2 projects.")
|
||||
|
||||
# Check if button action includes both projects AFTER archivization
|
||||
project_B.write({'active': False})
|
||||
action = sale_order.action_view_project_ids()
|
||||
self.assertEqual(len(action['domain'][0][2]), 2, "Domain should contain 2 projects. (one archived, one not)")
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.tests.common import TestSaleCommon
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tests.common import tagged
|
||||
from psycopg2 import Error as Psycopg2Error
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSoLineMilestones(TestSaleCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
cls.env['res.config.settings'].create({'group_project_milestone': True}).execute()
|
||||
uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
|
||||
cls.product_delivery_milestones1 = cls.env['product.product'].create({
|
||||
'name': "Milestones 1, create project only",
|
||||
'standard_price': 15,
|
||||
'list_price': 30,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'MILE-DELI4',
|
||||
'service_type': 'milestones',
|
||||
'service_tracking': 'project_only',
|
||||
})
|
||||
cls.product_delivery_milestones2 = cls.env['product.product'].create({
|
||||
'name': "Milestones 2, create project only",
|
||||
'standard_price':20,
|
||||
'list_price': 35,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'MILE-DELI4',
|
||||
'service_type': 'milestones',
|
||||
'service_tracking': 'project_only',
|
||||
})
|
||||
cls.product_delivery_milestones3 = cls.env['product.product'].create({
|
||||
'name': "Milestones 3, create project & task",
|
||||
'standard_price': 20,
|
||||
'list_price': 35,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'default_code': 'MILE-DELI4',
|
||||
'service_type': 'milestones',
|
||||
'service_tracking': 'task_in_project',
|
||||
})
|
||||
|
||||
cls.sale_order = cls.env['sale.order'].create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
})
|
||||
cls.sol1 = cls.env['sale.order.line'].create({
|
||||
'product_id': cls.product_delivery_milestones1.id,
|
||||
'product_uom_qty': 20,
|
||||
'order_id': cls.sale_order.id,
|
||||
})
|
||||
cls.sol2 = cls.env['sale.order.line'].create({
|
||||
'product_id': cls.product_delivery_milestones2.id,
|
||||
'product_uom_qty': 30,
|
||||
'order_id': cls.sale_order.id,
|
||||
})
|
||||
cls.sale_order.action_confirm()
|
||||
|
||||
cls.project = cls.sol1.project_id
|
||||
|
||||
cls.milestone1 = cls.env['project.milestone'].create({
|
||||
'name': 'Milestone 1',
|
||||
'project_id': cls.project.id,
|
||||
'is_reached': False,
|
||||
'sale_line_id': cls.sol1.id,
|
||||
'quantity_percentage': 0.5,
|
||||
})
|
||||
|
||||
def test_reached_milestones_delivered_quantity(self):
|
||||
self.milestone2 = self.env['project.milestone'].create({
|
||||
'name': 'Milestone 2',
|
||||
'project_id': self.project.id,
|
||||
'is_reached': False,
|
||||
'sale_line_id': self.sol2.id,
|
||||
'quantity_percentage': 0.2,
|
||||
})
|
||||
self.milestone3 = self.env['project.milestone'].create({
|
||||
'name': 'Milestone 3',
|
||||
'project_id': self.project.id,
|
||||
'is_reached': False,
|
||||
'sale_line_id': self.sol2.id,
|
||||
'quantity_percentage': 0.4,
|
||||
})
|
||||
|
||||
self.assertEqual(self.sol1.qty_delivered, 0.0, "Delivered quantity should start at 0")
|
||||
self.assertEqual(self.sol2.qty_delivered, 0.0, "Delivered quantity should start at 0")
|
||||
|
||||
self.milestone1.is_reached = True
|
||||
self.assertEqual(self.sol1.qty_delivered, 10.0, "Delivered quantity should update after a milestone is reached")
|
||||
|
||||
self.milestone2.is_reached = True
|
||||
self.assertEqual(self.sol2.qty_delivered, 6.0, "Delivered quantity should update after a milestone is reached")
|
||||
|
||||
self.milestone3.is_reached = True
|
||||
self.assertEqual(self.sol2.qty_delivered, 18.0, "Delivered quantity should update after a milestone is reached")
|
||||
|
||||
def test_update_reached_milestone_quantity(self):
|
||||
self.milestone1.is_reached = True
|
||||
self.assertEqual(self.sol1.qty_delivered, 10.0, "Delivered quantity should start at 10")
|
||||
|
||||
self.milestone1.quantity_percentage = 0.75
|
||||
self.assertEqual(self.sol1.qty_delivered, 15.0, "Delivered quantity should update after a milestone's quantity is updated")
|
||||
|
||||
def test_remove_reached_milestone(self):
|
||||
self.milestone1.is_reached = True
|
||||
self.assertEqual(self.sol1.qty_delivered, 10.0, "Delivered quantity should start at 10")
|
||||
|
||||
self.milestone1.unlink()
|
||||
self.assertEqual(self.sol1.qty_delivered, 0.0, "Delivered quantity should update when a milestone is removed")
|
||||
|
||||
def test_compute_sale_line_in_task(self):
|
||||
task = self.env['project.task'].create({
|
||||
'name': 'Test Task',
|
||||
'project_id': self.project.id,
|
||||
})
|
||||
self.assertEqual(task.sale_line_id, self.sol1, 'The task should have the one of the project linked')
|
||||
self.project.sale_line_id = False
|
||||
task.sale_line_id = False
|
||||
self.assertFalse(task.sale_line_id)
|
||||
task.write({'milestone_id': self.milestone1.id})
|
||||
self.assertEqual(task.sale_line_id, self.milestone1.sale_line_id, 'The task should have the SOL from the milestone.')
|
||||
self.project.sale_line_id = self.sol2
|
||||
self.assertEqual(task.sale_line_id, self.sol1, 'The task should keep the SOL linked to the milestone.')
|
||||
|
||||
def test_create_milestone_on_project_set_on_sales_order(self):
|
||||
"""
|
||||
Regression Test:
|
||||
If we confirm an SO with a service with a delivery based on milestones,
|
||||
that creates both a project & task, and we set a project on the SO,
|
||||
the project for the milestone should be the one set on the SO,
|
||||
and no ValidationError or NotNullViolation should be raised.
|
||||
"""
|
||||
project = self.env['project.project'].create({
|
||||
'name': 'Test Project For Milestones',
|
||||
'partner_id': self.partner_a.id
|
||||
})
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'project_id': project.id, # the user set a project on the SO
|
||||
})
|
||||
self.env['sale.order.line'].create({
|
||||
'product_id': self.product_delivery_milestones3.id,
|
||||
'product_uom_qty': 20,
|
||||
'order_id': sale_order.id,
|
||||
})
|
||||
try:
|
||||
sale_order.action_confirm()
|
||||
except ValidationError:
|
||||
self.fail("The sale order should be confirmed, "
|
||||
"and no ValidationError should be raised, "
|
||||
"for a missing project on the milestone.")
|
||||
except Psycopg2Error as e:
|
||||
# Check if the error is a NOT NULL violation
|
||||
NotNullViolationPgCode = '23502'
|
||||
if e.pgcode == NotNullViolationPgCode:
|
||||
self.fail("The sale order should be confirmed, "
|
||||
"and no NotNullViolation should be raised, "
|
||||
"for a missing project on the milestone.")
|
||||
else:
|
||||
# Re-raise any other unexpected database error
|
||||
raise
|
||||
Loading…
Add table
Add a link
Reference in a new issue