diff --git a/stock_delivery_planner/__init__.py b/stock_delivery_planner/__init__.py new file mode 100644 index 00000000..9b429614 --- /dev/null +++ b/stock_delivery_planner/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/stock_delivery_planner/__manifest__.py b/stock_delivery_planner/__manifest__.py new file mode 100644 index 00000000..9bd923e3 --- /dev/null +++ b/stock_delivery_planner/__manifest__.py @@ -0,0 +1,36 @@ +{ + 'name': 'Stock Delivery Planner', + 'summary': 'Get rates and choose carrier for delivery.', + 'version': '12.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'website': "https://hibou.io", + 'description': """ +Stock Delivery Planner +====================== + +Re-rate deliveries at packing time to find lowest-priced delivery method that still meets the expected delivery date. + +""", + 'depends': [ + # 'sale_sourced_by_line', + # 'base_geolocalize', + # 'delivery', + # 'resource', + 'delivery_hibou', + 'sale_planner', + 'stock', + ], + 'data': [ + # 'security/ir.model.access.csv', + # 'wizard/order_planner_views.xml', + # 'views/sale.xml', + 'views/stock_views.xml', + 'wizard/stock_delivery_planner_views.xml', + # 'views/delivery.xml', + # 'views/product.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/stock_delivery_planner/models/__init__.py b/stock_delivery_planner/models/__init__.py new file mode 100644 index 00000000..12bab770 --- /dev/null +++ b/stock_delivery_planner/models/__init__.py @@ -0,0 +1 @@ +from . import stock diff --git a/stock_delivery_planner/models/stock.py b/stock_delivery_planner/models/stock.py new file mode 100644 index 00000000..ca771553 --- /dev/null +++ b/stock_delivery_planner/models/stock.py @@ -0,0 +1,48 @@ +from odoo import api, fields, models, tools, _ +from odoo.exceptions import UserError + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + @api.multi + def action_plan_delivery(self): + context = dict(self.env.context or {}) + planner_model = self.env['stock.delivery.planner'] + for picking in self: + planner = planner_model.create({ + 'picking_id': picking.id, + }) + return { + 'name': _('Plan Delivery'), + 'type': 'ir.actions.act_window', + 'res_model': 'stock.delivery.planner', + 'res_id': planner.id, + 'view_type': 'form', + 'view_mode': 'form', + 'view_id': self.env.ref('stock_delivery_planner.view_stock_delivery_planner').id, + 'target': 'new', + 'context': context, + } + + # def get_shipping_carriers(self, carrier_id=None, domain=None): + def get_shipping_carriers(self): + Carrier = self.env['delivery.carrier'].sudo() + # if carrier_id: + # return Carrier.browse(carrier_id) + # + # if domain: + # if not isinstance(domain, (list, tuple)): + # domain = tools.safe_eval(domain) + # else: + domain = [] + + if self.env.context.get('carrier_domain'): + # potential bug here if this is textual + domain.extend(self.env.context.get('carrier_domain')) + + irconfig_parameter = self.env['ir.config_parameter'].sudo() + if irconfig_parameter.get_param('sale.order.planner.carrier_domain'): + domain.extend(tools.safe_eval(irconfig_parameter.get_param('sale.order.planner.carrier_domain'))) + + return Carrier.search(domain) diff --git a/stock_delivery_planner/tests/__init__.py b/stock_delivery_planner/tests/__init__.py new file mode 100644 index 00000000..83aa212b --- /dev/null +++ b/stock_delivery_planner/tests/__init__.py @@ -0,0 +1 @@ +from . import test_stock_delivery_planner diff --git a/stock_delivery_planner/tests/test_stock_delivery_planner.py b/stock_delivery_planner/tests/test_stock_delivery_planner.py new file mode 100644 index 00000000..c642de7b --- /dev/null +++ b/stock_delivery_planner/tests/test_stock_delivery_planner.py @@ -0,0 +1,133 @@ +from odoo import fields +from odoo.tests.common import Form, TransactionCase + + +class TestStockDeliveryPlanner(TransactionCase): + def setUp(self): + super(TestStockDeliveryPlanner, self).setUp() + try: + self.fedex_delivery = self.browse_ref('delivery_fedex.delivery_carrier_fedex_us') + except ValueError: + self.skipTest('FedEx Shipping Connector demo data is required to run this test.') + self.env['ir.config_parameter'].sudo().set_param('sale.order.planner.carrier_domain', + "[('id', 'in', (%d,))]" % self.fedex_delivery.id) + self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_domain', + "[('id', 'in', (%d,))]" % self.fedex_delivery.id) + # Does it make sense to set default package in fedex_rate_shipment_multi + # instead of relying on a correctly configured delivery method? + self.fedex_package = self.browse_ref('delivery_fedex.fedex_packaging_FEDEX_25KG_BOX') + self.default_package = self.browse_ref('delivery_fedex.fedex_packaging_YOUR_PACKAGING') + self.fedex_delivery.fedex_default_packaging_id = self.default_package + # PRIORITY_OVERNIGHT might not be available depending on time of day? + self.fedex_delivery.fedex_service_type = 'GROUND_HOME_DELIVERY' + self.fedex_delivery_express = self.fedex_delivery.fedex_find_delivery_carrier_for_service('FEDEX_EXPRESS_SAVER') + if self.fedex_delivery_express: + self.fedex_delivery_express.fedex_default_packaging_id = self.default_package + else: + self.fedex_delivery_express = self.fedex_delivery.copy() + self.fedex_delivery_express.name = 'Test FedEx Delivery' + self.fedex_delivery_express.fedex_service_type = 'FEDEX_EXPRESS_SAVER' + + delivery_calendar = self.env['resource.calendar'].create({ + 'name': 'Test Delivery Calendar', + 'tz': 'US/Central', + 'attendance_ids': [ + (0, 0, {'name': 'Monday', 'dayofweek': '0', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday', 'dayofweek': '1', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday', 'dayofweek': '2', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Thursday', 'dayofweek': '3', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday', 'dayofweek': '4', 'hour_from': 0, 'hour_to': 24, 'day_period': 'morning'}), + ], + }) + self.fedex_delivery.delivery_calendar_id = delivery_calendar + self.fedex_delivery_express.delivery_calendar_id = delivery_calendar + + # needs a valid address for sender and recipient + 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_ia = self.env['res.country.state'].search([('name', '=', 'Iowa')], limit=1) + self.env.user.company_id.partner_id.write({ + 'street': '321 1st St', + 'city': 'Ames', + 'state_id': self.state_ia.id, + 'zip': '50010', + 'country_id': self.country_usa.id, + }) + self.partner = self.env['res.partner'].create({ + 'name': 'Test Customer', + '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, + 'customer': True, + }) + + # self.product = self.browse_ref('product.product_product_27') # [FURN_8855] Drawer + # self.product.weight = 5.0 + # self.product.volume = 0.1 + self.env['ir.config_parameter'].sudo().set_param('product.weight_in_lbs', '1') + self.product = self.env['product.product'].create({ + 'name': 'Test Ship Product', + 'type': 'product', + 'weight': 1.0, + }) + self.env['stock.change.product.qty'].create({ + 'product_id': self.product.id, + 'new_quantity': 10.0, + }).change_product_qty() + + so = Form(self.env['sale.order']) + so.partner_id = self.partner + so.carrier_id = self.env['delivery.carrier'].browse() + with so.order_line.new() as line: + line.product_id = self.product + line.product_uom_qty = 5.0 + line.price_unit = 100.0 + self.sale_order = so.save() + + order_plan_action = self.sale_order.action_planorder() + order_plan = self.env[order_plan_action['res_model']].browse(order_plan_action['res_id']) + order_plan.planning_option_ids.filtered(lambda o: o.carrier_id == self.fedex_delivery).select_plan() + + self.sale_order.action_confirm() + self.picking = self.sale_order.picking_ids + + def test_00_test_one_package(self): + """Delivery is packed in one package""" + self.assertTrue(self.sale_order.requested_date, 'Order has not been planned') + self.assertEqual(len(self.picking), 1) + grp_pack = self.env.ref('stock.group_tracking_lot') + self.env.user.write({'groups_id': [(4, grp_pack.id)]}) + + self.assertEqual(self.picking.carrier_id, self.fedex_delivery, 'Carrier did not carry over to Delivery Order') + self.assertEqual(self.picking.weight, 5.0) + self.assertEqual(self.picking.shipping_weight, 0.0) + + self.picking.move_line_ids.filtered(lambda ml: ml.product_id == self.product).qty_done = 5.0 + packing_action = self.picking.put_in_pack() + packing_wizard = Form(self.env[packing_action['res_model']].with_context(packing_action['context'])) + packing_wizard.delivery_packaging_id = self.fedex_package + choose_delivery_package = packing_wizard.save() + choose_delivery_package.put_in_pack() + self.assertEqual(self.picking.shipping_weight, 5.0) + + action = self.picking.action_plan_delivery() + planner = self.env[action['res_model']].browse(action['res_id']) + + self.assertEqual(planner.picking_id, self.picking) + self.assertGreater(len(planner.plan_option_ids), 1) + + plan_option = planner.plan_option_ids.filtered(lambda o: o.carrier_id == self.fedex_delivery_express) + self.assertEqual(len(plan_option), 1) + self.assertGreater(plan_option.price, 0.0) + self.assertEqual(plan_option.date_planned.date(), fields.Date().today()) + self.assertTrue(plan_option.requested_date) + self.assertEqual(plan_option.transit_days, 3) + self.assertEqual(plan_option.sale_requested_date, self.sale_order.requested_date) + self.assertEqual(plan_option.days_different, 2) + + plan_option.select_plan() + self.assertEqual(self.picking.carrier_id, self.fedex_delivery_express) diff --git a/stock_delivery_planner/views/stock_views.xml b/stock_delivery_planner/views/stock_views.xml new file mode 100644 index 00000000..985dbd14 --- /dev/null +++ b/stock_delivery_planner/views/stock_views.xml @@ -0,0 +1,13 @@ + + + + stock.picking.form.inherit.delivery.planner + stock.picking + + + +