333 lines
16 KiB
Python
333 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import timedelta
|
|
from odoo.fields import Command, Datetime
|
|
from odoo.tests import Form
|
|
from odoo.addons.sale_stock_renting.tests.test_rental_common import TestRentalCommon
|
|
|
|
|
|
class TestRentalKits(TestRentalCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
|
|
cls.env['res.config.settings'].create({'group_rental_stock_picking': True}).execute()
|
|
|
|
cls.component_1 = cls.env['product.product'].create({'name': 'compo 1', 'is_storable': True})
|
|
cls.component_2 = cls.env['product.product'].create({'name': 'compo 2', 'is_storable': True})
|
|
cls.bom = cls.env['mrp.bom'].create({
|
|
'product_id': cls.product_id.id,
|
|
'product_tmpl_id': cls.product_id.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'consumption': 'flexible',
|
|
'type': 'phantom',
|
|
'bom_line_ids': [
|
|
(0, 0, {'product_id': cls.component_1.id, 'product_qty': 1}),
|
|
(0, 0, {'product_id': cls.component_2.id, 'product_qty': 2})
|
|
]
|
|
})
|
|
|
|
quants = cls.env['stock.quant'].create({
|
|
'product_id': cls.component_1.id,
|
|
'inventory_quantity': 5.0,
|
|
'location_id': cls.warehouse_id.lot_stock_id.id
|
|
})
|
|
quants |= cls.env['stock.quant'].create({
|
|
'product_id': cls.component_2.id,
|
|
'inventory_quantity': 10.0,
|
|
'location_id': cls.warehouse_id.lot_stock_id.id
|
|
})
|
|
quants.action_apply_inventory()
|
|
|
|
def test_flow_1(self):
|
|
rental_order_1 = self.sale_order_id.copy()
|
|
rental_line_1 = rental_order_1.order_line
|
|
rental_order_1.order_line.write({'product_uom_qty': 3, 'is_rental': True})
|
|
rental_order_1.rental_start_date = self.rental_start_date
|
|
rental_order_1.rental_return_date = self.rental_return_date
|
|
rental_order_1.action_confirm()
|
|
self.assertEqual([d.date() for d in rental_order_1.picking_ids.mapped('scheduled_date')],
|
|
[rental_order_1.rental_start_date.date(), rental_order_1.rental_return_date.date()])
|
|
self.assertEqual(len(rental_order_1.picking_ids), 2)
|
|
self.assertEqual(len(rental_order_1.picking_ids.move_ids), 4)
|
|
|
|
outgoing_picking = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned')
|
|
incoming_picking = rental_order_1.picking_ids.filtered(lambda p: p.state == 'waiting')
|
|
self.assertEqual(outgoing_picking.move_ids.mapped('product_uom_qty'), [3.0, 6.0])
|
|
self.assertEqual(incoming_picking.move_ids.mapped('product_uom_qty'), [3.0, 6.0])
|
|
|
|
rental_order_1.order_line.write({'product_uom_qty': 4})
|
|
self.assertEqual(outgoing_picking.move_ids.mapped('product_uom_qty'), [4.0, 8.0])
|
|
self.assertEqual(incoming_picking.move_ids.mapped('product_uom_qty'), [4.0, 8.0])
|
|
|
|
rental_order_1.order_line.write({'product_uom_qty': 2})
|
|
self.assertEqual(outgoing_picking.move_ids.mapped('product_uom_qty'), [2.0, 4.0])
|
|
self.assertEqual(incoming_picking.move_ids.mapped('product_uom_qty'), [2.0, 4.0])
|
|
|
|
outgoing_picking.move_ids[0].quantity = 1
|
|
outgoing_picking.move_ids[1].quantity = 2
|
|
Form.from_action(self.env, outgoing_picking.button_validate()).save().process()
|
|
self.assertEqual(rental_order_1.order_line.qty_delivered, 1)
|
|
self.env.add_to_compute(rental_line_1._fields['qty_delivered'], rental_line_1)
|
|
self.assertEqual(
|
|
rental_line_1.qty_delivered, 1,
|
|
"Quantity delivered shouldn't change after recompute",
|
|
)
|
|
|
|
outgoing_picking_2 = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned' and p.picking_type_code == 'outgoing')
|
|
self.assertEqual(outgoing_picking_2.move_ids.mapped('product_uom_qty'), [1.0, 2.0])
|
|
rental_order_1.order_line.write({'product_uom_qty': 3})
|
|
self.assertEqual(outgoing_picking_2.move_ids.mapped('product_uom_qty'), [2.0, 4.0])
|
|
self.assertEqual(incoming_picking.move_ids.mapped('product_uom_qty'), [3.0, 6.0])
|
|
incoming_picking.action_assign()
|
|
self.assertEqual(incoming_picking.move_ids.mapped('quantity'), [1.0, 2.0])
|
|
|
|
outgoing_picking_2.move_ids[0].quantity = 1
|
|
outgoing_picking_2.move_ids[1].quantity = 2
|
|
Form.from_action(self.env, outgoing_picking_2.button_validate()).save().process()
|
|
self.assertEqual(rental_order_1.order_line.qty_delivered, 2)
|
|
self.env.add_to_compute(rental_line_1._fields['qty_delivered'], rental_line_1)
|
|
self.assertEqual(
|
|
rental_line_1.qty_delivered, 2,
|
|
"Quantity delivered shouldn't change after recompute",
|
|
)
|
|
|
|
incoming_picking.move_ids[0].quantity = 1
|
|
incoming_picking.move_ids[1].quantity = 2
|
|
Form.from_action(self.env, incoming_picking.button_validate()).save().process()
|
|
self.assertEqual(rental_order_1.order_line.qty_returned, 1)
|
|
|
|
outgoing_picking_3 = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned' and p.picking_type_code == 'outgoing')
|
|
incoming_picking_2 = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned' and p.picking_type_code == 'incoming')
|
|
self.assertEqual(incoming_picking_2.move_ids.mapped('product_uom_qty'), [2.0, 4.0])
|
|
|
|
outgoing_picking_3.button_validate()
|
|
incoming_picking_2.button_validate()
|
|
self.assertEqual(rental_order_1.order_line.qty_returned, 3)
|
|
|
|
def test_late_fee(self):
|
|
rental_order_1 = self.sale_order_id.copy()
|
|
rental_order_1.order_line.write({'product_uom_qty': 1, 'is_rental': True})
|
|
rental_order_1.rental_start_date = Datetime.now() - timedelta(days=7)
|
|
rental_order_1.rental_return_date = Datetime.now() - timedelta(days=3)
|
|
rental_order_1.action_confirm()
|
|
|
|
outgoing_picking = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned')
|
|
self.assertEqual(outgoing_picking.scheduled_date.date(), rental_order_1.rental_start_date.date())
|
|
outgoing_picking.button_validate()
|
|
|
|
incoming_picking = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned')
|
|
self.assertEqual(incoming_picking.scheduled_date.date(), rental_order_1.rental_return_date.date())
|
|
incoming_picking.move_ids[0].picked = True
|
|
Form.from_action(self.env, incoming_picking.button_validate()).save().process()
|
|
self.assertEqual(rental_order_1.order_line.qty_returned, 0)
|
|
self.assertEqual(len(rental_order_1.order_line), 1)
|
|
|
|
incoming_picking_2 = rental_order_1.picking_ids.filtered(lambda p: p.state == 'assigned')
|
|
self.assertEqual(incoming_picking_2.scheduled_date.date(), rental_order_1.rental_return_date.date())
|
|
incoming_picking_2.move_ids[0].quantity = 2
|
|
incoming_picking_2.button_validate()
|
|
|
|
self.assertEqual(len(rental_order_1.order_line), 2)
|
|
late_fee_order_line = rental_order_1.order_line.filtered(lambda l: l.product_id.type == 'service')
|
|
self.assertEqual(late_fee_order_line.price_unit, 30)
|
|
|
|
def test_subkits_and_same_component(self):
|
|
"""
|
|
- Kit
|
|
- 1 x C1
|
|
- 1 x C2
|
|
- 1 x C3
|
|
- 2X C2
|
|
|
|
Rental with delivery
|
|
Ensure that qties of both delivery and return are correct
|
|
"""
|
|
stock_location = self.warehouse_id.lot_stock_id
|
|
rental_location = self.env.company.rental_loc_id
|
|
|
|
component_3 = self.env['product.product'].create({
|
|
'name': 'compo 3',
|
|
'type': 'consu',
|
|
})
|
|
|
|
sub_bom = self.env['mrp.bom'].create({
|
|
'product_id': self.component_1.id,
|
|
'product_tmpl_id': self.component_1.product_tmpl_id.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom',
|
|
'bom_line_ids': [
|
|
(0, 0, {'product_id': self.component_2.id, 'product_qty': 1}),
|
|
(0, 0, {'product_id': component_3.id, 'product_qty': 1})
|
|
]
|
|
})
|
|
_bom_line_01, bom_line_02 = self.bom.bom_line_ids
|
|
bom_line_03, bom_line_04 = sub_bom.bom_line_ids
|
|
|
|
rental = self.sale_order_id.copy()
|
|
rental.order_line.write({'product_uom_qty': 1, 'is_rental': True})
|
|
rental.action_confirm()
|
|
|
|
# The BoM line sequence should be respected when exploding a BoM that contains a kit.
|
|
self.assertRecordValues(rental.picking_ids.move_ids, [
|
|
{'product_id': self.component_2.id, 'product_qty': 1, 'bom_line_id': bom_line_03.id, 'location_id': stock_location.id},
|
|
{'product_id': component_3.id, 'product_qty': 1, 'bom_line_id': bom_line_04.id, 'location_id': stock_location.id},
|
|
{'product_id': self.component_2.id, 'product_qty': 2, 'bom_line_id': bom_line_02.id, 'location_id': stock_location.id},
|
|
{'product_id': self.component_2.id, 'product_qty': 1, 'bom_line_id': bom_line_03.id, 'location_id': rental_location.id},
|
|
{'product_id': component_3.id, 'product_qty': 1, 'bom_line_id': bom_line_04.id, 'location_id': rental_location.id},
|
|
{'product_id': self.component_2.id, 'product_qty': 2, 'bom_line_id': bom_line_02.id, 'location_id': rental_location.id},
|
|
])
|
|
|
|
def test_pickup_kit_from_rental(self):
|
|
"""Create a rental with a kit and confirm it. Then, pickup the kit.
|
|
"""
|
|
# disable the setting to don't create a picking and allow the user to pickup the kit
|
|
self.env.user.group_ids -= self.env.ref('sale_stock_renting.group_rental_stock_picking')
|
|
settings = self.env['res.config.settings'].with_user(self.env.user).create({})
|
|
settings.group_rental_stock_picking = False
|
|
settings.set_values()
|
|
# create a rental with kit
|
|
rental = self.sale_order_id.copy()
|
|
rental.order_line.write({'product_uom_qty': 1, 'is_rental': True})
|
|
self.assertEqual(rental.order_line.product_id.is_kits, True)
|
|
rental.action_confirm()
|
|
# pickup the kit
|
|
self.assertEqual(rental.order_line.qty_delivered, 0)
|
|
action_dict = rental.action_open_pickup()
|
|
pickup_wizard = self.env['rental.order.wizard'].with_context(
|
|
action_dict['context']
|
|
).create({})
|
|
pickup_wizard._get_wizard_lines()
|
|
pickup_wizard.apply()
|
|
self.assertEqual(rental.order_line.qty_delivered, 1)
|
|
|
|
def test_kit_bom_created_after_picking_creation(self):
|
|
"""
|
|
With rental transfer enabled confirm a rental order for a product without a BOM
|
|
add a kit BOM for it and validate the rental transfers.
|
|
"""
|
|
rental_product = self.component_1
|
|
rental_product.rent_ok = True
|
|
rental_order = self.env['sale.order'].with_context(in_rental_app=True).create({
|
|
'partner_id': self.cust1.id,
|
|
'rental_start_date': self.rental_start_date,
|
|
'rental_return_date': self.rental_return_date,
|
|
'order_line': [Command.create({
|
|
'product_id': rental_product.id,
|
|
'product_uom_qty': 1,
|
|
})],
|
|
})
|
|
rental_order.action_confirm()
|
|
self.env['mrp.bom'].create({
|
|
'product_tmpl_id': rental_product.product_tmpl_id.id,
|
|
'product_id': rental_product.id,
|
|
'product_qty': 1.0,
|
|
'type': 'phantom',
|
|
'bom_line_ids': [Command.create({
|
|
'product_id': self.component_2.id,
|
|
'product_qty': 1,
|
|
})],
|
|
})
|
|
# invalidate the record to be considered as a kit
|
|
rental_product.invalidate_recordset()
|
|
delivery = rental_order.picking_ids[0]
|
|
self.assertEqual(delivery.move_ids.product_id, rental_product)
|
|
# try to validate which will explode the kit moves
|
|
delivery.button_validate()
|
|
self.assertEqual(delivery.move_ids.product_id, self.component_2)
|
|
delivery.move_ids.quantity = 1.0
|
|
delivery.button_validate()
|
|
self.assertRecordValues(delivery.move_ids, [
|
|
{'product_id': self.component_2.id, 'state': 'done'}
|
|
])
|
|
|
|
def test_kit_multipick_flow(self):
|
|
"""
|
|
Test that confirming a rental order with a kit product whose components
|
|
use two different pack locations does not crash when assigning a return_id.
|
|
Two pick operations are generated (one per pack location).
|
|
"""
|
|
self.warehouse_id.delivery_steps = 'pick_pack_ship'
|
|
|
|
# Locations
|
|
customer_location = self.env.ref('stock.stock_location_customers')
|
|
output_location = self.env.ref('stock.stock_location_output')
|
|
pack_location_copy = self.env.ref('stock.location_pack_zone').copy()
|
|
|
|
# Create a new pick operation
|
|
pick_operation_copy = self.warehouse_id.pick_type_id.copy({'default_location_dest_id': pack_location_copy.id})
|
|
|
|
# Create a new route to use the new pick operation
|
|
custom_route = self.env['stock.route'].create({
|
|
'name': 'Test',
|
|
'rule_ids': [
|
|
Command.create({
|
|
'name': 'test 1',
|
|
'action': 'pull',
|
|
'picking_type_id': pick_operation_copy.id,
|
|
'location_src_id': self.warehouse_id.lot_stock_id.id,
|
|
'location_dest_id': customer_location.id,
|
|
}),
|
|
Command.create({
|
|
'name': 'test 2',
|
|
'action': 'push',
|
|
'picking_type_id': self.warehouse_id.pack_type_id.id,
|
|
'location_src_id': pack_location_copy.id,
|
|
'location_dest_id': output_location.id,
|
|
}),
|
|
Command.create({
|
|
'name': 'test 3',
|
|
'action': 'push',
|
|
'picking_type_id': self.warehouse_id.out_type_id.id,
|
|
'location_src_id': output_location.id,
|
|
'location_dest_id': customer_location.id,
|
|
}),
|
|
],
|
|
})
|
|
|
|
# Add the route to a component product
|
|
self.component_1.route_ids = [Command.link(custom_route.id)]
|
|
|
|
# create a rental with the kit product
|
|
rental = self.sale_order_id.copy()
|
|
rental.order_line.write({'product_uom_qty': 1, 'is_rental': True})
|
|
self.assertEqual(rental.order_line.product_id.is_kits, True)
|
|
rental.action_confirm()
|
|
|
|
picks = rental.picking_ids.filtered(lambda pick: pick.picking_type_id in (pick_operation_copy, self.warehouse_id.pick_type_id))
|
|
self.assertEqual(len(picks), 2)
|
|
|
|
def test_rental_variant_no_bom(self):
|
|
"""Test confirming a rental order for a variant without a BoM,
|
|
while another variant of the same product has a Kit (phantom) BoM.
|
|
Ensure the order confirms successfully without raising errors.
|
|
"""
|
|
attribute = self.env['product.attribute'].create({'name': 'Color'})
|
|
product_tmpl = self.env['product.template'].create({
|
|
'name': 'computer kit',
|
|
'rent_ok': True,
|
|
'attribute_line_ids': [
|
|
Command.create({
|
|
'attribute_id': attribute.id,
|
|
'value_ids': [
|
|
Command.create({'name': 'A', 'attribute_id': attribute.id}),
|
|
Command.create({'name': 'B', 'attribute_id': attribute.id}),
|
|
],
|
|
})
|
|
]
|
|
})
|
|
self.bom.product_id = product_tmpl.product_variant_ids[0].id
|
|
self.bom.product_tmpl_id = product_tmpl.id
|
|
rental_order = self.sale_order_id.copy()
|
|
rental_order.order_line = [
|
|
Command.create({
|
|
'product_id': product_tmpl.product_variant_ids[1].id,
|
|
'product_uom_qty': 1.0,
|
|
'is_rental': True
|
|
})
|
|
]
|
|
rental_order.action_confirm()
|
|
self.assertEqual(rental_order.state, 'sale')
|