from odoo.tests import common from datetime import datetime, timedelta from json import loads as json_decode from logging import getLogger _logger = getLogger(__name__) class TestPlanner(common.TransactionCase): # @todo Test date planning! def setUp(self): super(TestPlanner, self).setUp() self.today = datetime.today() self.tomorrow = datetime.today() + timedelta(days=1) # This partner has a parent self.country_usa = self.env['res.country'].search([('name', '=', 'United States')], limit=1) self.state_wa = self.env['res.country.state'].search([('name', '=', 'Washington')], limit=1) self.state_co = self.env['res.country.state'].search([('name', '=', 'Colorado')], limit=1) self.partner_wa = self.env['res.partner'].create({ 'name': 'Jared', 'street': '1234 Test Street', 'city': 'Marysville', 'state_id': self.state_wa.id, 'zip': '98270', 'country_id': self.country_usa.id, 'partner_latitude': 48.05636, 'partner_longitude': -122.14922, }) self.warehouse_partner_1 = self.env['res.partner'].create({ 'name': 'WH1', 'street': '1234 Test Street', 'city': 'Lynnwood', 'state_id': self.state_wa.id, 'zip': '98036', 'country_id': self.country_usa.id, 'partner_latitude': 47.82093, 'partner_longitude': -122.31513, }) self.warehouse_partner_2 = self.env['res.partner'].create({ 'name': 'WH2', 'street': '1234 Test Street', 'city': 'Craig', 'state_id': self.state_co.id, 'zip': '81625', 'country_id': self.country_usa.id, 'partner_latitude': 40.51525, 'partner_longitude': -107.54645, }) self.warehouse_calendar_1 = self.env['resource.calendar'].create({ 'name': 'Washington Warehouse Hours', 'attendance_ids': [ (0, 0, {'name': 'today', 'dayofweek': str(self.today.weekday()), 'hour_from': (self.today.hour - 1) % 24, 'hour_to': (self.today.hour + 1) % 24}), (0, 0, {'name': 'tomorrow', 'dayofweek': str(self.tomorrow.weekday()), 'hour_from': (self.tomorrow.hour - 1) % 24, 'hour_to': (self.tomorrow.hour + 1) % 24}), ] }) self.warehouse_calendar_2 = self.env['resource.calendar'].create({ 'name': 'Colorado Warehouse Hours', 'attendance_ids': [ (0, 0, {'name': 'tomorrow', 'dayofweek': str(self.tomorrow.weekday()), 'hour_from': (self.tomorrow.hour - 1) % 24, 'hour_to': (self.tomorrow.hour + 1) % 24}), ] }) self.warehouse_1 = self.env['stock.warehouse'].create({ 'name': 'Washington Warehouse', 'partner_id': self.warehouse_partner_1.id, 'code': 'TWH1', 'shipping_calendar_id': self.warehouse_calendar_1.id, }) self.warehouse_2 = self.env['stock.warehouse'].create({ 'name': 'Colorado Warehouse', 'partner_id': self.warehouse_partner_2.id, 'code': 'TWH2', 'shipping_calendar_id': self.warehouse_calendar_2.id, }) self.so = self.env['sale.order'].create({ 'partner_id': self.partner_wa.id, 'warehouse_id': self.warehouse_1.id, }) self.product_1 = self.env['product.template'].create({ 'name': 'Product for WH1', 'type': 'product', 'standard_price': 1.0, }) self.product_12 = self.env['product.template'].create({ 'name': 'Product for WH1 Second', 'type': 'product', 'standard_price': 1.0, }) self.product_1 = self.product_1.product_variant_id self.product_12 = self.product_12.product_variant_id self.product_2 = self.env['product.template'].create({ 'name': 'Product for WH2', 'type': 'product', 'standard_price': 1.0, }) self.product_22 = self.env['product.template'].create({ 'name': 'Product for WH2 Second', 'type': 'product', 'standard_price': 1.0, }) self.product_2 = self.product_2.product_variant_id self.product_22 = self.product_22.product_variant_id self.product_both = self.env['product.template'].create({ 'name': 'Product for Both', 'type': 'product', 'standard_price': 1.0, }) self.product_both = self.product_both.product_variant_id self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_1.lot_stock_id.id, 'product_id': self.product_1.id, 'new_quantity': 100, }).change_product_qty() self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_1.lot_stock_id.id, 'product_id': self.product_12.id, 'new_quantity': 100, }).change_product_qty() self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_1.lot_stock_id.id, 'product_id': self.product_both.id, 'new_quantity': 100, }).change_product_qty() self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_2.lot_stock_id.id, 'product_id': self.product_2.id, 'new_quantity': 100, }).change_product_qty() self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_2.lot_stock_id.id, 'product_id': self.product_22.id, 'new_quantity': 100, }).change_product_qty() self.env['stock.change.product.qty'].create({ 'location_id': self.warehouse_2.lot_stock_id.id, 'product_id': self.product_both.id, 'new_quantity': 100, }).change_product_qty() self.policy_closest = self.env['sale.order.planning.policy'].create({ 'always_closest_warehouse': True, }) self.policy_other = self.env['sale.order.planning.policy'].create({}) self.wh_filter_1 = self.env['ir.filters'].create({ 'name': 'TWH1 Only', 'domain': "[('id', '=', %d)]" % (self.warehouse_1.id, ), 'model_id': 'stock.warehouse', }) self.wh_filter_2 = self.env['ir.filters'].create({ 'name': 'TWH2 Only', 'domain': "[('id', '=', %d)]" % (self.warehouse_2.id,), 'model_id': 'stock.warehouse', }) self.policy_wh_1 = self.env['sale.order.planning.policy'].create({ 'warehouse_filter_id': self.wh_filter_1.id, }) self.policy_wh_2 = self.env['sale.order.planning.policy'].create({ 'warehouse_filter_id': self.wh_filter_2.id, }) def both_wh_ids(self): return [self.warehouse_1.id, self.warehouse_2.id] def test_10_planner_creation_internals(self): """ Tests certain internal representations and that we can create a basic plan. """ self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertEqual(set(both_wh_ids), set(planner.get_warehouses().ids)) fake_order = planner._fake_order(self.so) base_option = planner.generate_base_option(fake_order) self.assertTrue(base_option, 'Must have base option.') self.assertEqual(self.warehouse_1.id, base_option['warehouse_id']) def test_21_planner_creation(self): """ Scenario where only one warehouse has inventory on the order line. This is "the closest" warehouse. """ self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_1.id).qty_available, 100) self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_2.id).qty_available, 0) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1) self.assertFalse(planner.planning_option_ids[0].sub_options) def test_22_planner_creation(self): """ Scenario where only one warehouse has inventory on the order line. This is "the further" warehouse. """ self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_1.id).qty_available, 0) self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2) self.assertFalse(planner.planning_option_ids[0].sub_options) def test_31_planner_creation_split(self): """ Scenario where only one warehouse has inventory on each of the order line. This will cause two pickings to be created, one for each warehouse. """ self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo2', }) self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_1.id).qty_available, 100) self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertTrue(planner.planning_option_ids[0].sub_options) def test_32_planner_creation_no_split(self): """ Scenario where only "the further" warehouse has inventory on whole order, but the "closest" warehouse only has inventory on one item. This will simply plan out of the "the further" warehouse. """ self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.assertEqual(self.product_both.with_context(warehouse=self.warehouse_2.id).qty_available, 100) self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2) self.assertFalse(planner.planning_option_ids[0].sub_options) def test_42_policy_force_closest(self): """ Scenario where an item may not be in stock at "the closest" warehouse, but an item is only allowed to come from "the closest" warehouse. """ self.product_2.property_planning_policy_id = self.policy_closest self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.assertEqual(self.product_both.with_context(warehouse=self.warehouse_1.id).qty_available, 100) # Close warehouse doesn't have product. self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_1.id).qty_available, 0) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1) self.assertFalse(planner.planning_option_ids[0].sub_options) def test_43_policy_merge(self): """ Scenario that will make a complicated scenario specifically: - 3 policy groups - 2 base options with sub_options (all base options with same warehouse) """ self.product_both.property_planning_policy_id = self.policy_closest self.product_12.property_planning_policy_id = self.policy_other self.product_22.property_planning_policy_id = self.policy_other self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_12.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_22.id, 'name': 'demo', }) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1) self.assertTrue(planner.planning_option_ids.sub_options) sub_options = json_decode(planner.planning_option_ids.sub_options) _logger.error(sub_options) wh_1_ids = sorted([self.product_both.id, self.product_1.id, self.product_12.id]) wh_2_ids = sorted([self.product_2.id, self.product_22.id]) self.assertEqual(sorted(sub_options[str(self.warehouse_1.id)]['product_ids']), wh_1_ids) self.assertEqual(sorted(sub_options[str(self.warehouse_2.id)]['product_ids']), wh_2_ids) def test_44_policy_merge_2(self): """ Scenario that will make a complicated scenario specifically: - 3 policy groups - 2 base options from different warehouses """ self.product_both.property_planning_policy_id = self.policy_other self.product_12.property_planning_policy_id = self.policy_closest self.product_22.property_planning_policy_id = self.policy_other self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_12.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_22.id, 'name': 'demo', }) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1, 'If this fails, it will probably pass next time.') self.assertTrue(planner.planning_option_ids.sub_options) sub_options = json_decode(planner.planning_option_ids.sub_options) _logger.error(sub_options) wh_1_ids = sorted([self.product_1.id, self.product_12.id]) wh_2_ids = sorted([self.product_both.id, self.product_2.id, self.product_22.id]) self.assertEqual(sorted(sub_options[str(self.warehouse_1.id)]['product_ids']), wh_1_ids) self.assertEqual(sorted(sub_options[str(self.warehouse_2.id)]['product_ids']), wh_2_ids) def test_45_policy_merge_3(self): """ Different order of products for test_44 - 3 policy groups - 2 base options from different warehouses """ self.product_both.property_planning_policy_id = self.policy_other self.product_12.property_planning_policy_id = self.policy_closest self.product_22.property_planning_policy_id = self.policy_other self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_1.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_2.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_12.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_22.id, 'name': 'demo', }) self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1) self.assertTrue(planner.planning_option_ids.sub_options) sub_options = json_decode(planner.planning_option_ids.sub_options) _logger.error(sub_options) wh_1_ids = sorted([self.product_1.id, self.product_12.id]) wh_2_ids = sorted([self.product_both.id, self.product_2.id, self.product_22.id]) self.assertEqual(sorted(sub_options[str(self.warehouse_1.id)]['product_ids']), wh_1_ids) self.assertEqual(sorted(sub_options[str(self.warehouse_2.id)]['product_ids']), wh_2_ids) def test_51_policy_specific_warehouse(self): """ Force one item to TWH2. """ self.product_both.property_planning_policy_id = self.policy_wh_2 self.env['sale.order.line'].create({ 'order_id': self.so.id, 'product_id': self.product_both.id, 'name': 'demo', }) both_wh_ids = self.both_wh_ids() planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2)