Initial ideas of having product level 'Planning Policy' for grouping and limiting where items can be planned from and how.

This commit is contained in:
Jared Kipe
2018-07-07 12:48:54 -07:00
parent ee6bac5f67
commit eda3eee901
9 changed files with 700 additions and 59 deletions

View File

@@ -28,6 +28,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
'base_geolocalize',
'delivery',
'resource',
'stock',
],
'demo': [],
'data': [
@@ -35,6 +36,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
'views/sale.xml',
'views/stock.xml',
'views/delivery.xml',
'views/product.xml',
],
'auto_install': False,
'installable': True,

View File

@@ -1,3 +1,6 @@
from . import sale
from . import stock
from . import delivery
from . import product
from . import planning
from . import partner

View File

@@ -36,7 +36,7 @@ class DeliveryCarrier(models.Model):
while date_planned < date_delivered:
if transit_days > 10:
break
interval = self.delivery_calendar_id.schedule_days(1, date_planned, compute_leaves=True)
interval = self.delivery_calendar_id.plan_days(1, date_planned, compute_leaves=True)
if not interval:
return self._calculate_transit_days_naive(date_planned, date_delivered)
@@ -59,7 +59,7 @@ class DeliveryCarrier(models.Model):
# date calculations needs an extra day
effective_transit_days = transit_days + 1
interval = self.delivery_calendar_id.schedule_days(effective_transit_days, date_planned, compute_leaves=True)
interval = self.delivery_calendar_id.plan_days(effective_transit_days, date_planned, compute_leaves=True)
if not interval:
return self._calculate_date_delivered_naive(date_planned, transit_days)

View File

@@ -0,0 +1,29 @@
from odoo import api, fields, models
try:
from uszipcode import ZipcodeSearchEngine
except ImportError:
ZipcodeSearchEngine = None
class Partner(models.Model):
_inherit = 'res.partner'
@api.multi
def geo_localize(self):
# We need country names in English below
for partner in self.with_context(lang='en_US'):
if ZipcodeSearchEngine and partner.zip:
with ZipcodeSearchEngine() as search:
zipcode = search.by_zipcode(partner.zip)
if zipcode:
partner.write({
'partner_latitude': zipcode['Latitude'],
'partner_longitude': zipcode['Longitude'],
'date_localization': fields.Date.context_today(partner),
})
else:
super(Partner, partner).geo_localize()
else:
super(Partner, partner).geo_localize()
return True

View File

@@ -0,0 +1,16 @@
from odoo import api, fields, models
class PlanningPolicy(models.Model):
_name = 'sale.order.planning.policy'
name = fields.Char(string='Name')
carrier_filter_id = fields.Many2one(
'ir.filters',
string='Delivery Carrier Filter',
)
warehouse_filter_id = fields.Many2one(
'ir.filters',
string='Warehouse Filter',
)
always_closest_warehouse = fields.Boolean(string='Always Plan Closest Warehouse')

View File

@@ -0,0 +1,22 @@
from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
property_planning_policy_id = fields.Many2one('sale.order.planning.policy', company_dependent=True,
string="Order Planner Policy",
help="The Order Planner Policy to use when making a sale order planner.")
@api.multi
def get_planning_policy(self):
self.ensure_one()
return self.property_planning_policy_id or self.categ_id.property_planning_policy_categ_id
class ProductCategory(models.Model):
_inherit = 'product.category'
property_planning_policy_categ_id = fields.Many2one('sale.order.planning.policy', company_dependent=True,
string="Order Planner Policy",
help="The Order Planner Policy to use when making a sale order planner.")

View File

@@ -1,5 +1,9 @@
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):
@@ -69,13 +73,13 @@ class TestPlanner(common.TransactionCase):
self.warehouse_1 = self.env['stock.warehouse'].create({
'name': 'Washington Warehouse',
'partner_id': self.warehouse_partner_1.id,
'code': 'WH1',
'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': 'WH2',
'code': 'TWH2',
'shipping_calendar_id': self.warehouse_calendar_2.id,
})
self.so = self.env['sale.order'].create({
@@ -87,13 +91,25 @@ class TestPlanner(common.TransactionCase):
'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',
@@ -105,6 +121,11 @@ class TestPlanner(common.TransactionCase):
'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,
@@ -115,16 +136,45 @@ class TestPlanner(common.TransactionCase):
'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_planner_creation_internals(self):
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,
@@ -138,7 +188,11 @@ class TestPlanner(common.TransactionCase):
self.assertTrue(base_option, 'Must have base option.')
self.assertEqual(self.warehouse_1.id, base_option['warehouse_id'])
def test_planner_creation(self):
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,
@@ -152,7 +206,11 @@ class TestPlanner(common.TransactionCase):
self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1)
self.assertFalse(planner.planning_option_ids[0].sub_options)
def test_planner_creation_2(self):
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,
@@ -166,7 +224,11 @@ class TestPlanner(common.TransactionCase):
self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2)
self.assertFalse(planner.planning_option_ids[0].sub_options)
def test_planner_creation_split(self):
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,
@@ -175,7 +237,7 @@ class TestPlanner(common.TransactionCase):
self.env['sale.order.line'].create({
'order_id': self.so.id,
'product_id': self.product_2.id,
'name': 'demo',
'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)
@@ -184,7 +246,12 @@ class TestPlanner(common.TransactionCase):
self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.')
self.assertTrue(planner.planning_option_ids[0].sub_options)
def test_planner_creation_no_split(self):
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,
@@ -202,3 +269,194 @@ class TestPlanner(common.TransactionCase):
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_2)
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)

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="product_template_form_view_inherit" model="ir.ui.view">
<field name="name">product.template.common.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='categ_id']" position="after">
<field name="property_planning_policy_id"/>
</xpath>
</field>
</record>
<record id="product_category_form_view_inherit" model="ir.ui.view">
<field name="name">product.category.form.inherit</field>
<field name="model">product.category</field>
<field name="inherit_id" ref="product.product_category_form_view" />
<field name="arch" type="xml">
<xpath expr="//field[@name='parent_id']" position="after">
<field name="property_planning_policy_categ_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -2,6 +2,7 @@ from math import sin, cos, sqrt, atan2, radians
from json import dumps, loads
from copy import deepcopy
from datetime import datetime
from collections import defaultdict
from logging import getLogger
_logger = getLogger(__name__)
@@ -255,59 +256,70 @@ class SaleOrderMakePlan(models.TransientModel):
return options
def get_warehouses(self, warehouse_id=None):
def get_warehouses(self, warehouse_id=None, domain=None):
warehouse = self.env['stock.warehouse'].sudo()
if warehouse_id:
return warehouse.search([('id', '=', warehouse_id)])
return warehouse.browse(warehouse_id)
if domain:
if not isinstance(domain, (list, tuple)):
domain = tools.safe_eval(domain)
else:
domain = []
if self.env.context.get('warehouse_domain'):
#potential bug here if this is textual
return warehouse.search(self.env.context.get('warehouse_domain'))
domain.extend(self.env.context.get('warehouse_domain'))
irconfig_parameter = self.env['ir.config_parameter'].sudo()
if irconfig_parameter.get_param('sale.order.planner.warehouse_domain'):
domain = tools.safe_eval(irconfig_parameter.get_param('sale.order.planner.warehouse_domain'))
domain.extend(tools.safe_eval(irconfig_parameter.get_param('sale.order.planner.warehouse_domain')))
return warehouse.search(domain)
return warehouse.search([])
def get_shipping_carriers(self, carrier_id=None):
def get_shipping_carriers(self, carrier_id=None, domain=None):
Carrier = self.env['delivery.carrier'].sudo()
if carrier_id:
return Carrier.search([('id', '=', 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
return Carrier.search(self.env.context.get('carrier_domain'))
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 = tools.safe_eval(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)
return Carrier.search([])
def _generate_base_option(self, order_fake, policy_group):
policy = False
flag_force_closest = False
warehouse_domain = False
if 'policy' in policy_group:
policy = policy_group['policy']
flag_force_closest = policy.always_closest_warehouse
warehouse_domain = policy.warehouse_filter_id.domain
# Need to look at warehouse filter.
# Eventually need to look at shipping filter....
def generate_base_option(self, order_fake):
product_lines = list(filter(lambda line: line.product_id.type == 'product', order_fake.order_line))
if not product_lines:
return {}
buy_qty = {line.product_id.id: line.product_uom_qty for line in product_lines}
products = self.env['product.product']
for line in product_lines:
products |= line.product_id
warehouses = self.get_warehouses()
product_stock = self._fetch_product_stock(warehouses, products)
warehouses = self.get_warehouses(domain=warehouse_domain)
if flag_force_closest:
warehouses = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id)
product_stock = self._fetch_product_stock(warehouses, policy_group['products'])
sub_options = {}
wh_date_planning = {}
p_len = len(products)
p_len = len(policy_group['products'])
full_candidates = set()
partial_candidates = set()
for wh_id, stock in product_stock.items():
available = sum(1 for p_id, p_vals in stock.items() if self._is_in_stock(p_vals, buy_qty[p_id]))
available = sum(1 for p_id, p_vals in stock.items() if self._is_in_stock(p_vals, policy_group['buy_qty'][p_id]))
if available == p_len:
full_candidates.add(wh_id)
elif available > 0:
@@ -316,32 +328,35 @@ class SaleOrderMakePlan(models.TransientModel):
if full_candidates:
if len(full_candidates) == 1:
warehouse = warehouses.filtered(lambda wh: wh.id in full_candidates)
date_planned = self._next_warehouse_shipping_date(warehouse)
order_fake.warehouse_id = warehouse
return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
else:
warehouse = self._find_closest_warehouse_by_partner(
warehouses.filtered(lambda wh: wh.id in full_candidates), order_fake.partner_shipping_id)
date_planned = self._next_warehouse_shipping_date(warehouse)
order_fake.warehouse_id = warehouse
#order_fake.warehouse_id = warehouse
return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
_logger.error(' partial_candidates: ' + str(partial_candidates))
if partial_candidates:
_logger.error(' using...')
if len(partial_candidates) == 1:
warehouse = warehouses.filtered(lambda wh: wh.id in partial_candidates)
order_fake.warehouse_id = warehouse
#order_fake.warehouse_id = warehouse
return {'warehouse_id': warehouse.id}
sorted_warehouses = self._sort_warehouses_by_partner(warehouses.filtered(lambda wh: wh.id in partial_candidates), order_fake.partner_shipping_id)
sorted_warehouses = self._sort_warehouses_by_partner(
warehouses.filtered(lambda wh: wh.id in partial_candidates), order_fake.partner_shipping_id)
_logger.error(' sorted_warehouses: ' + str(sorted_warehouses) + ' warehouses: ' + str(warehouses))
primary_wh = sorted_warehouses[0] # partial_candidates means there is at least one warehouse
primary_wh_date_planned = self._next_warehouse_shipping_date(primary_wh)
wh_date_planning[primary_wh.id] = primary_wh_date_planned
for wh in sorted_warehouses:
if not buy_qty:
_logger.error(' wh: ' + str(wh) + ' buy_qty: ' + str(policy_group['buy_qty']))
if not policy_group['buy_qty']:
continue
stock = product_stock[wh.id]
for p_id, p_vals in stock.items():
if p_id in buy_qty and self._is_in_stock(p_vals, buy_qty[p_id]):
_logger.error(' p_id: ' + str(p_id) + ' p_vals: ' + str(p_vals))
if p_id in policy_group['buy_qty'] and self._is_in_stock(p_vals, policy_group['buy_qty'][p_id]):
if wh.id not in sub_options:
sub_options[wh.id] = {
'date_planned': self._next_warehouse_shipping_date(wh),
@@ -350,23 +365,287 @@ class SaleOrderMakePlan(models.TransientModel):
}
sub_options[wh.id]['product_ids'].append(p_id)
sub_options[wh.id]['product_skus'].append(p_vals['sku'])
del buy_qty[p_id]
_logger.error(' removing: ' + str(p_id))
del policy_group['buy_qty'][p_id]
if not buy_qty:
if not policy_group['buy_qty']:
# item_details can fulfil all items.
# this is good!!
order_fake.warehouse_id = primary_wh
return {'warehouse_id': primary_wh.id, 'date_planned': primary_wh_date_planned, 'sub_options': sub_options}
#order_fake.warehouse_id = primary_wh
return {'warehouse_id': primary_wh.id, 'date_planned': primary_wh_date_planned,
'sub_options': sub_options}
# warehouses cannot fulfil all requested items!!
order_fake.warehouse_id = primary_wh
#order_fake.warehouse_id = primary_wh
return {'warehouse_id': primary_wh.id}
# nobody has stock!
primary_wh = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id)
order_fake.warehouse_id = primary_wh
#order_fake.warehouse_id = primary_wh
return {'warehouse_id': primary_wh.id}
def generate_base_option(self, order_fake):
_logger.error('generate_base_option:')
product_lines = list(filter(lambda line: line.product_id.type == 'product', order_fake.order_line))
if not product_lines:
return {}
buy_qty = defaultdict(int)
for line in product_lines:
buy_qty[line.product_id.id] += line.product_uom_qty
products = self.env['product.product']
for line in product_lines:
products |= line.product_id
policy_groups = defaultdict(lambda: {'products': [], 'buy_qty': {}})
for p in products:
policy = p.product_tmpl_id.get_planning_policy()
if policy:
policy_groups[policy.id]['products'].append(p)
policy_groups[policy.id]['buy_qty'][p.id] = buy_qty[p.id]
policy_groups[policy.id]['policy'] = policy
else:
policy_groups[0]['products'].append(p)
policy_groups[0]['buy_qty'][p.id] = buy_qty[p.id]
for _, policy_group in policy_groups.items():
product_set = self.env['product.product'].browse()
for p in policy_group['products']:
product_set += p
policy_group['products'] = product_set
policy_group['base_option'] = self._generate_base_option(order_fake, policy_group)
option_policy_groups = defaultdict(lambda: {'products': self.env['product.product'].browse(), 'policies': self.env['sale.order.planning.policy'].browse(), 'date_planned': '1900', 'sub_options': [],})
for policy_id, policy_group in policy_groups.items():
base_option = policy_group['base_option']
_logger.error(' base_option: ' + str(base_option))
b_wh_id = base_option['warehouse_id']
if 'policy' in policy_group:
option_policy_groups[b_wh_id]['policies'] += policy_group['policy']
if option_policy_groups[b_wh_id].get('date_planned'):
# The first base_option without a date clears it
if base_option.get('date_planned'):
if base_option['date_planned'] > option_policy_groups[b_wh_id]['date_planned']:
option_policy_groups[b_wh_id]['date_planned'] = base_option['date_planned']
else:
# One of our options has no plan date. Remove it.
del option_policy_groups[b_wh_id]['date_planned']
if 'sub_options' in base_option:
option_policy_groups[b_wh_id]['sub_options'].append(base_option['sub_options'])
option_policy_groups[b_wh_id]['products'] += policy_group['products']
option_policy_groups[b_wh_id]['warehouse_id'] = b_wh_id
# clean up unused sub_options and collapse used ones
for o_wh_id, option_group in option_policy_groups.items():
if not option_group['sub_options']:
del option_group['sub_options']
else:
sub_options = defaultdict(lambda: {'date_planned': '1900', 'product_ids': [], 'product_skus': []})
remaining_products = option_group['products']
for options in option_group['sub_options']:
for wh_id, option in options.items():
if sub_options[wh_id].get('date_planned'):
# The first option without a date clears it
if option.get('date_planned'):
if option['date_planned'] > sub_options[wh_id]['date_planned']:
sub_options[wh_id]['date_planned'] = option['date_planned']
else:
del sub_options[wh_id]['date_planned']
sub_options[wh_id]['product_ids'] += option['product_ids']
sub_options[wh_id]['product_skus'] += option['product_skus']
remaining_products = remaining_products.filtered(lambda p: p.id not in sub_options[wh_id]['product_ids'])
option_group['sub_options'] = sub_options
if remaining_products:
option_group['sub_options'][o_wh_id]['product_ids'] += remaining_products.ids
option_group['sub_options'][o_wh_id]['product_skus'] += remaining_products.mapped('default_code')
# At this point we should have all of the policy options collapsed.
# Collapse warehouse options.
base_option = {'date_planned': '1900', 'products': self.env['product.product'].browse()}
for wh_id, intermediate_option in option_policy_groups.items():
_logger.error(' base_option: ' + str(base_option))
_logger.error(' intermediate_option: ' + str(intermediate_option))
if 'warehouse_id' not in base_option:
base_option['warehouse_id'] = wh_id
b_wh_id = base_option['warehouse_id']
if base_option.get('date_planned'):
if intermediate_option.get('date_planned'):
if intermediate_option['date_planned'] > base_option['date_planned']:
base_option['date_planned'] = intermediate_option['date_planned']
else:
del base_option['date_planned']
if 'sub_options' in base_option:
for _, option in base_option['sub_options'].items():
del option['date_planned']
if b_wh_id == wh_id:
if 'sub_options' in intermediate_option and 'sub_options' not in base_option:
# Base option will get new sub_options
intermediate_option['sub_options'][wh_id]['product_ids'] += base_option['products'].ids
intermediate_option['sub_options'][wh_id]['product_skus'] += base_option['products'].mapped('default_code')
base_option['sub_options'] = intermediate_option['sub_options']
elif 'sub_options' in intermediate_option and 'sub_options' in base_option:
# Both have sub_options, merge
for o_wh_id, option in intermediate_option['sub_options'].items():
if o_wh_id not in base_option['sub_options']:
base_option['sub_options'][o_wh_id] = option
else:
base_option['sub_options'][o_wh_id]['product_ids'] += option['product_ids']
base_option['sub_options'][o_wh_id]['product_skus'] += option['product_skus']
if base_option.get('date_planned'):
if option['date_planned'] > base_option['sub_options'][o_wh_id]['date_planned']:
base_option['sub_options'][o_wh_id]['date_planned'] = intermediate_option['date_planned']
elif 'sub_options' in base_option:
# merge products from intermediate into base_option's sub_options
base_option['sub_options'][wh_id]['product_ids'] += intermediate_option['products'].ids
base_option['sub_options'][wh_id]['product_skus'] += intermediate_option['products'].mapped('default_code')
base_option['products'] += intermediate_option['products']
else:
# Promote
if 'sub_options' not in intermediate_option and 'sub_options' not in base_option:
base_option['sub_options'] = {
wh_id: {
'product_ids': intermediate_option['products'].ids,
'product_skus': intermediate_option['products'].mapped('default_code'),
},
b_wh_id: {
'product_ids': base_option['products'].ids,
'product_skus': base_option['products'].mapped('default_code'),
},
}
if base_option.get('date_planned'):
base_option['sub_options'][wh_id]['date_planned'] = intermediate_option['date_planned']
base_option['sub_options'][b_wh_id]['date_planned'] = base_option['date_planned']
elif 'sub_options' in base_option and 'sub_options' not in intermediate_option:
if wh_id not in base_option['sub_options']:
base_option['sub_options'][wh_id] = {
'product_ids': intermediate_option['products'].ids,
'product_skus': intermediate_option['products'].mapped('default_code'),
}
if base_option.get('date_planned'):
base_option['sub_options'][wh_id]['date_planned'] = intermediate_option['date_planned']
else:
base_option['sub_options'][wh_id]['product_ids'] += intermediate_option['products'].ids
base_option['sub_options'][wh_id]['product_skus'] += intermediate_option['products'].mapped('default_code')
if base_option.get('date_planned'):
if intermediate_option['date_planned'] > base_option['sub_options'][wh_id]['date_planned']:
base_option['sub_options'][wh_id]['date_planned'] = intermediate_option['date_planned']
elif 'sub_options' in intermediate_option and 'sub_options' in base_option:
# Both have sub_options, merge
for o_wh_id, option in intermediate_option['sub_options'].items():
if o_wh_id not in base_option['sub_options']:
base_option['sub_options'][o_wh_id] = option
else:
base_option['sub_options'][o_wh_id]['product_ids'] += option['product_ids']
base_option['sub_options'][o_wh_id]['product_skus'] += option['product_skus']
if base_option.get('date_planned'):
if option['date_planned'] > base_option['sub_options'][o_wh_id]['date_planned']:
base_option['sub_options'][o_wh_id]['date_planned'] = intermediate_option['date_planned']
else:
# intermediate_option has sub_options but base_option doesn't
base_option['sub_options'] = {
b_wh_id: {
'product_ids': base_option['products'].ids,
'product_skus': base_option['products'].mapped('default_code'),
}
}
if base_option.get('date_planned'):
base_option['sub_options'][b_wh_id]['date_planned'] = base_option['date_planned']
for o_wh_id, option in intermediate_option['sub_options'].items():
if o_wh_id not in base_option['sub_options']:
base_option['sub_options'][o_wh_id] = option
else:
base_option['sub_options'][o_wh_id]['product_ids'] += option['product_ids']
base_option['sub_options'][o_wh_id]['product_skus'] += option['product_skus']
if base_option.get('date_planned'):
if option['date_planned'] > base_option['sub_options'][o_wh_id]['date_planned']:
base_option['sub_options'][o_wh_id]['date_planned'] = intermediate_option['date_planned']
del base_option['products']
_logger.error(' returning: ' + str(base_option))
order_fake.warehouse_id = self.get_warehouses(warehouse_id=base_option['warehouse_id'])
return base_option
# Collapse by warehouse_id
# warehouses = self.get_warehouses()
# product_stock = self._fetch_product_stock(warehouses, products)
# sub_options = {}
# wh_date_planning = {}
#
# p_len = len(products)
# full_candidates = set()
# partial_candidates = set()
# for wh_id, stock in product_stock.items():
# available = sum(1 for p_id, p_vals in stock.items() if self._is_in_stock(p_vals, buy_qty[p_id]))
# if available == p_len:
# full_candidates.add(wh_id)
# elif available > 0:
# partial_candidates.add(wh_id)
#
# if full_candidates:
# if len(full_candidates) == 1:
# warehouse = warehouses.filtered(lambda wh: wh.id in full_candidates)
# date_planned = self._next_warehouse_shipping_date(warehouse)
# order_fake.warehouse_id = warehouse
# return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
#
# warehouse = self._find_closest_warehouse_by_partner(
# warehouses.filtered(lambda wh: wh.id in full_candidates), order_fake.partner_shipping_id)
# date_planned = self._next_warehouse_shipping_date(warehouse)
# order_fake.warehouse_id = warehouse
# return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
#
# if partial_candidates:
# if len(partial_candidates) == 1:
# warehouse = warehouses.filtered(lambda wh: wh.id in partial_candidates)
# order_fake.warehouse_id = warehouse
# return {'warehouse_id': warehouse.id}
#
# sorted_warehouses = self._sort_warehouses_by_partner(warehouses.filtered(lambda wh: wh.id in partial_candidates), order_fake.partner_shipping_id)
# primary_wh = sorted_warehouses[0] #partial_candidates means there is at least one warehouse
# primary_wh_date_planned = self._next_warehouse_shipping_date(primary_wh)
# wh_date_planning[primary_wh.id] = primary_wh_date_planned
# for wh in sorted_warehouses:
# if not buy_qty:
# continue
# stock = product_stock[wh.id]
# for p_id, p_vals in stock.items():
# if p_id in buy_qty and self._is_in_stock(p_vals, buy_qty[p_id]):
# if wh.id not in sub_options:
# sub_options[wh.id] = {
# 'date_planned': self._next_warehouse_shipping_date(wh),
# 'product_ids': [],
# 'product_skus': [],
# }
# sub_options[wh.id]['product_ids'].append(p_id)
# sub_options[wh.id]['product_skus'].append(p_vals['sku'])
# del buy_qty[p_id]
#
# if not buy_qty:
# # item_details can fulfil all items.
# # this is good!!
# order_fake.warehouse_id = primary_wh
# return {'warehouse_id': primary_wh.id, 'date_planned': primary_wh_date_planned, 'sub_options': sub_options}
#
# # warehouses cannot fulfil all requested items!!
# order_fake.warehouse_id = primary_wh
# return {'warehouse_id': primary_wh.id}
#
# # nobody has stock!
# primary_wh = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id)
# order_fake.warehouse_id = primary_wh
# return {'warehouse_id': primary_wh.id}
def _is_in_stock(self, p_stock, buy_qty):
return p_stock['real_qty_available'] >= buy_qty
@@ -391,9 +670,9 @@ class SaleOrderMakePlan(models.TransientModel):
return [warehouses.filtered(lambda wh: wh.id == distances[d]) for d in wh_distances]
def _next_warehouse_shipping_date(self, warehouse):
return fields.Datetime.to_string(warehouse.shipping_calendar_id.plan_days(0.01,
fields.Datetime.from_string(fields.Datetime.now()),
compute_leaves=True))
if warehouse.shipping_calendar_id:
return fields.Datetime.to_string(warehouse.shipping_calendar_id.plan_days(0.01, fields.Datetime.from_string(fields.Datetime.now()), compute_leaves=True))
return False
@api.model
def _fetch_product_stock(self, warehouses, products):
@@ -414,7 +693,16 @@ class SaleOrderMakePlan(models.TransientModel):
def generate_shipping_options(self, base_option, order_fake):
# generate a carrier_id, amount, requested_date (promise date)
# if base_option['carrier_id'] then that is the only carrier we want to collect rates for.
carriers = self.get_shipping_carriers(base_option.get('carrier_id'))
product_lines = list(filter(lambda line: line.product_id.type == 'product', order_fake.order_line))
domain = []
for line in product_lines:
policy = line.product_id.product_tmpl_id.get_planning_policy()
if policy and policy.carrier_filter_id:
domain.extend(tools.safe_eval(policy.carrier_filter_id.domain))
carriers = self.get_shipping_carriers(base_option.get('carrier_id'), domain=domain)
if not carriers:
return base_option
if not base_option.get('sub_options'):
options = []