1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
|
# -*- 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.errors import NotNullViolation
@tagged('post_install', '-at_install')
class TestSoLineMilestones(TestSaleCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
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_default_values_milestone(self):
""" This test checks that newly created milestones have the correct default values:
1) the first SOL of the SO linked to the project should be used as the default one.
2) the quantity percentage should be 100% (1.0 in backend).
"""
project = self.env['project.project'].create({
'name': 'Test project',
'sale_line_id': self.sol2.id, # sol1 was created first so we use sol2 to demonstrate that sol1 is used
})
milestone = self.env['project.milestone'].with_context({'default_project_id': project.id}).create({
'name': 'Test milestone',
'project_id': project.id,
'is_reached': False,
})
# since SOL1 was created before SOL2, it should be selected
self.assertEqual(milestone.sale_line_id, self.sol1, "The milestone's sale order line should be the first one in the project's SO") #1
self.assertEqual(milestone.quantity_percentage, 1.0, "The milestone's quantity percentage should be 1.0") #2
def test_compute_qty_milestone(self):
""" This test will check that the compute methods for the milestone quantity fields work properly. """
ratio = self.milestone1.quantity_percentage / self.milestone1.product_uom_qty
self.milestone1.quantity_percentage = 1.0
self.assertEqual(self.milestone1.quantity_percentage / self.milestone1.product_uom_qty, ratio, "The ratio should be the same as before")
self.milestone1.product_uom_qty = 25
self.assertEqual(self.milestone1.quantity_percentage / self.milestone1.product_uom_qty, ratio, "The ratio should be the same as before")
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.
"""
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,
})
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, NotNullViolation):
self.fail("The sale order should be confirmed, "
"and no ValidationError or NotNullViolation should be raised, "
"for a missing project on the milestone.")
def test_so_with_milestone_products(self):
"""
If a SO contains products invoiced based on milestones, a milestone should be created for each of them
in their project.
"""
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
products = self.product_delivery_milestones1 | self.product_delivery_milestones2 | self.product_delivery_milestones3
products.service_tracking = 'task_in_project'
self.env['sale.order.line'].create([{
'product_id': product.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
} for product in products])
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 3, "The project should have a milestone for each product.")
self.assertCountEqual({m.name for m in project.milestone_ids}, {f"[{products[0].default_code}] {p.name}" for p in products}, "The milestones should be named after the products.")
def test_project_template_with_milestones(self):
"""
If a milestone product has a project template with configured milestones, use those instead of creating
a new milestone and set a quantity equal to the quantity of the SOL divided by the number of milestones.
"""
project_template = self.env['project.project'].create({
'name': 'Project Template',
})
self.env['project.milestone'].create([{
'project_id': project_template.id,
'name': str(i),
} for i in range(4)])
self.product_delivery_milestones1.project_template_id = project_template.id
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
self.env['sale.order.line'].create({
'product_id': self.product_delivery_milestones1.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
})
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 4, "The generated project should have 4 milestones.")
self.assertEqual({m.quantity_percentage for m in project.milestone_ids}, {0.25}, "All milestones of the generated project should have a quantity percentage of 25%.")
def test_project_template_with_milestones_multiple_products(self):
"""
If multiple products use the same project template, which has configured milestones, use the first product
on those milestones, but generate the other default milestones as normal
"""
project_template = self.env['project.project'].create({
'name': 'Project Template',
})
self.env['project.milestone'].create([{
'project_id': project_template.id,
'name': str(i),
} for i in range(4)])
products = self.product_delivery_milestones1 | self.product_delivery_milestones2
products.write({
'project_template_id': project_template.id,
'service_tracking': 'task_in_project',
})
sale_order = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
})
self.env['sale.order.line'].create([{
'product_id': product.id,
'product_uom_qty': 20,
'order_id': sale_order.id,
} for product in products])
sale_order.action_confirm()
project = sale_order.project_ids
self.assertEqual(len(project.milestone_ids), 5, "The project should have 5 milestones")
|