[IMP] sale_planner: use multi-rating to speed up planning, graphical configuration

This commit is contained in:
Jared Kipe
2022-02-09 09:44:46 -08:00
parent 03c0b61d21
commit 89e8179625
16 changed files with 266 additions and 49 deletions

View File

@@ -1,2 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import wizard from . import wizard
from . import models from . import models

View File

@@ -1,10 +1,10 @@
{ {
'name': 'Sale Order Planner', 'name': 'Sale Order Planner',
'summary': 'Plans order dates and warehouses.', 'summary': 'Plans order dates and warehouses.',
'version': '15.0.1.0.0', 'version': '15.0.2.0.0',
'author': "Hibou Corp.", 'author': "Hibou Corp.",
'category': 'Sale', 'category': 'Sale',
'license': 'AGPL-3', 'license': 'OPL-1',
'complexity': 'expert', 'complexity': 'expert',
'images': [], 'images': [],
'website': "https://hibou.io", 'website': "https://hibou.io",
@@ -37,6 +37,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
'views/stock.xml', 'views/stock.xml',
'views/delivery.xml', 'views/delivery.xml',
'views/product.xml', 'views/product.xml',
'views/res_config_settings_views.xml',
], ],
'auto_install': False, 'auto_install': False,
'installable': True, 'installable': True,

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import delivery from . import delivery
from . import partner from . import partner
from . import planning from . import planning
@@ -5,3 +7,4 @@ from . import product
from . import resource from . import resource
from . import sale from . import sale
from . import stock from . import stock
from . import res_config_settings

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from datetime import timedelta from datetime import timedelta
from odoo import api, fields, models from odoo import api, fields, models
@@ -61,6 +63,9 @@ class DeliveryCarrier(models.Model):
def calculate_transit_days(self, date_planned, date_delivered): def calculate_transit_days(self, date_planned, date_delivered):
self.ensure_one() self.ensure_one()
if not self.delivery_calendar_id:
return 0
if isinstance(date_planned, str): if isinstance(date_planned, str):
date_planned = fields.Datetime.from_string(date_planned) date_planned = fields.Datetime.from_string(date_planned)
if isinstance(date_delivered, str): if isinstance(date_delivered, str):

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models
try: try:

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models

View File

@@ -0,0 +1,79 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
def sale_planner_warehouse_ids(env, company):
get_param = env['ir.config_parameter'].sudo().get_param
warehouse_ids = get_param('sale.planner.warehouse_ids.%s' % (company.id, )) or []
if warehouse_ids and isinstance(warehouse_ids, str):
try:
warehouse_ids = [int(i) for i in warehouse_ids.split(',')]
except:
warehouse_ids = []
return warehouse_ids
def sale_planner_carrier_ids(env, company):
get_param = env['ir.config_parameter'].sudo().get_param
carrier_ids = get_param('sale.planner.carrier_ids.%s' % (company.id, )) or []
if carrier_ids and isinstance(carrier_ids, str):
try:
carrier_ids = [int(c) for c in carrier_ids.split(',')]
except:
carrier_ids = []
return carrier_ids
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
sale_planner_warehouse_ids = fields.Many2many('stock.warehouse',
string='Sale Order Planner Warehouses',
compute='_compute_sale_planner_warehouse_ids',
inverse='_inverse_sale_planner_warehouse_ids')
sale_planner_carrier_ids = fields.Many2many('delivery.carrier',
string='Sale Order Planner Carriers',
compute='_compute_sale_planner_carrier_ids',
inverse='_inverse_sale_planner_carrier_ids')
def _compute_sale_planner_warehouse_ids_ids(self):
company = self.company_id or self.env.user.company_id
return sale_planner_warehouse_ids(self.env, company)
def _compute_sale_planner_carrier_ids_ids(self):
company = self.company_id or self.env.user.company_id
return sale_planner_carrier_ids(self.env, company)
def _compute_sale_planner_warehouse_ids(self):
for settings in self:
warehouse_ids = settings._compute_sale_planner_warehouse_ids_ids()
warehouses = self.env['stock.warehouse'].browse(warehouse_ids)
settings.sale_planner_warehouse_ids = warehouses
def _compute_sale_planner_carrier_ids(self):
for settings in self:
carrier_ids = settings._compute_sale_planner_carrier_ids_ids()
carriers = self.env['delivery.carrier'].browse(carrier_ids)
settings.sale_planner_carrier_ids = carriers
def _inverse_sale_planner_warehouse_ids(self):
set_param = self.env['ir.config_parameter'].sudo().set_param
company_id = self.company_id.id or self.env.user.company_id.id
for settings in self:
warehouse_ids = ','.join(str(i) for i in settings.sale_planner_warehouse_ids.ids)
set_param('sale.planner.warehouse_ids.%s' % (company_id, ), warehouse_ids)
def _inverse_sale_planner_carrier_ids(self):
set_param = self.env['ir.config_parameter'].sudo().set_param
company_id = self.company_id.id or self.env.user.company_id.id
for settings in self:
carrier_ids = ','.join(str(i) for i in settings.sale_planner_carrier_ids.ids)
set_param('sale.planner.carrier_ids.%s' % (company_id, ), carrier_ids)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res['sale_planner_warehouse_ids'] = [(6, 0, self._compute_sale_planner_warehouse_ids_ids())]
res['sale_planner_carrier_ids'] = [(6, 0, self._compute_sale_planner_carrier_ids_ids())]
return res

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models from odoo import api, fields, models
@@ -7,3 +9,7 @@ class Warehouse(models.Model):
shipping_calendar_id = fields.Many2one( shipping_calendar_id = fields.Many2one(
'resource.calendar', 'Shipping Calendar', 'resource.calendar', 'Shipping Calendar',
help="This calendar represents shipping availability from the warehouse.") help="This calendar represents shipping availability from the warehouse.")
sale_planner_carrier_ids = fields.Many2many('delivery.carrier',
relation='sale_planner_carrier_wh_rel',
string='Sale Order Planner Base Carriers',
help='Overrides the global carriers.')

View File

@@ -1 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import test_planner from . import test_planner

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.tests import common from odoo.tests import common
from datetime import datetime, timedelta from datetime import datetime, timedelta
from json import loads as json_decode from json import loads as json_decode

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="49"/>
<field name="inherit_id" ref="delivery.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@data-key='sale_management']" position="inside">
<h2>Sale Order Planner</h2>
<div class="col-lg-6 col-12 o_setting_box" id="sale_planner_carriers">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="sale_planner_carrier_ids" />
<div class="text-muted">
Add a carrier that represents the 'base rate' for a carrier's type. <br/>
For example, you should add 1 FedEx carrier here and let us build up the
rates for your other FedEx shipping methods.
</div>
<field name="sale_planner_carrier_ids" class="oe_inline" options="{'no_create_edit': True, 'no_create': True}" />
</div>
</div>
<div class="col-lg-6 col-12 o_setting_box" id="sale_planner_warehouses">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="sale_planner_warehouse_ids" />
<div class="text-muted">
Warehouses you typically ship inventory out of that you want to
include in the planning of sale orders.
</div>
<field name="sale_planner_warehouse_ids" class="oe_inline" options="{'no_create_edit': True, 'no_create': True}" />
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -7,6 +7,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after"> <xpath expr="//field[@name='partner_id']" position="after">
<field name="shipping_calendar_id" /> <field name="shipping_calendar_id" />
<field name="sale_planner_carrier_ids" options="{'no_create_edit': True, 'no_create': True}" />
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@@ -1 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import order_planner from . import order_planner

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from math import sin, cos, sqrt, atan2, radians from math import sin, cos, sqrt, atan2, radians
from json import dumps, loads from json import dumps, loads
from copy import deepcopy from copy import deepcopy
@@ -10,11 +12,12 @@ _logger = getLogger(__name__)
try: try:
from uszipcode import SearchEngine from uszipcode import SearchEngine
except ImportError: except ImportError:
_logger.warning('module "uszipcode" cannot be loaded, falling back to Google API') _logger.warn('module "uszipcode" cannot be loaded, falling back to Google API')
SearchEngine = None SearchEngine = None
from odoo import api, fields, models from odoo import api, fields, models
from odoo.tools.safe_eval import safe_eval from odoo.tools.safe_eval import safe_eval
from ..models.res_config_settings import sale_planner_warehouse_ids, sale_planner_carrier_ids
class FakeCollection(): class FakeCollection():
@@ -26,7 +29,12 @@ class FakeCollection():
yield v yield v
def filtered(self, f): def filtered(self, f):
return filter(f, self.vals) return self.__class__([v for v in self.vals if f(v)])
# return filter(f, self.vals)
def mapped(self, s):
# note this only maps to one level and doesn't really support recordset
return [v[s] for v in self.vals]
def sudo(self, *args, **kwargs): def sudo(self, *args, **kwargs):
return self return self
@@ -179,6 +187,12 @@ class FakeSaleOrder(FakeCollection):
return str(datetime.now()) return str(datetime.now())
return getattr(self, item) return getattr(self, item)
def _get_estimated_weight(self):
weight = 0.0
for order_line in self.order_line.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not l.is_delivery and not l.display_type):
weight += order_line.product_qty * order_line.product_id.weight
return weight
def distance(lat_1, lon_1, lat_2, lon_2): def distance(lat_1, lon_1, lat_2, lon_2):
R = 6373.0 R = 6373.0
@@ -308,15 +322,17 @@ class SaleOrderMakePlan(models.TransientModel):
domain.append(('company_id', 'in', self.env.context['allowed_company_ids'])) domain.append(('company_id', 'in', self.env.context['allowed_company_ids']))
if self.env.context.get('warehouse_domain'): if self.env.context.get('warehouse_domain'):
if not domain:
domain = []
domain.extend(self.env.context.get('warehouse_domain')) domain.extend(self.env.context.get('warehouse_domain'))
if domain:
irconfig_parameter = self.env['ir.config_parameter'].sudo()
if irconfig_parameter.get_param('sale.order.planner.warehouse_domain'):
domain.extend(safe_eval(irconfig_parameter.get_param('sale.order.planner.warehouse_domain')))
return warehouse.search(domain) return warehouse.search(domain)
def get_shipping_carriers(self, carrier_id=None, domain=None): # no domain, use global
warehouse_ids = sale_planner_warehouse_ids(self.env, self.env.user.company_id)
return warehouse.browse(warehouse_ids)
def get_shipping_carriers(self, carrier_id=None, domain=None, warehouse_id=None):
Carrier = self.env['delivery.carrier'].sudo() Carrier = self.env['delivery.carrier'].sudo()
if carrier_id: if carrier_id:
return Carrier.browse(carrier_id) return Carrier.browse(carrier_id)
@@ -324,19 +340,22 @@ class SaleOrderMakePlan(models.TransientModel):
if domain: if domain:
if not isinstance(domain, (list, tuple)): if not isinstance(domain, (list, tuple)):
domain = safe_eval(domain) domain = safe_eval(domain)
else:
domain = []
if self.env.context.get('carrier_domain'): if self.env.context.get('carrier_domain'):
# potential bug here if this is textual if not domain:
domain = []
domain.extend(self.env.context.get('carrier_domain')) domain.extend(self.env.context.get('carrier_domain'))
if domain:
irconfig_parameter = self.env['ir.config_parameter'].sudo()
if irconfig_parameter.get_param('sale.order.planner.carrier_domain'):
domain.extend(safe_eval(irconfig_parameter.get_param('sale.order.planner.carrier_domain')))
return Carrier.search(domain) return Carrier.search(domain)
# no domain, use global
if warehouse_id:
warehouse = self.env['stock.warehouse'].sudo().browse(warehouse_id)
if warehouse.sale_planner_carrier_ids:
return warehouse.sale_planner_carrier_ids.sudo()
carrier_ids = sale_planner_carrier_ids(self.env, self.env.user.company_id)
return Carrier.browse(carrier_ids)
def _generate_base_option(self, order_fake, policy_group): def _generate_base_option(self, order_fake, policy_group):
flag_force_closest = False flag_force_closest = False
warehouse_domain = False warehouse_domain = False
@@ -616,6 +635,8 @@ class SaleOrderMakePlan(models.TransientModel):
return self._find_closest_warehouse(warehouses, partner.partner_latitude, partner.partner_longitude) return self._find_closest_warehouse(warehouses, partner.partner_latitude, partner.partner_longitude)
def _find_closest_warehouse(self, warehouses, latitude, longitude): def _find_closest_warehouse(self, warehouses, latitude, longitude):
if not warehouses:
return warehouses
distances = {distance(latitude, longitude, wh.partner_id.partner_latitude, wh.partner_id.partner_longitude): wh.id for wh in warehouses} distances = {distance(latitude, longitude, wh.partner_id.partner_latitude, wh.partner_id.partner_longitude): wh.id for wh in warehouses}
wh_id = distances[min(distances)] wh_id = distances[min(distances)]
return warehouses.filtered(lambda wh: wh.id == wh_id) return warehouses.filtered(lambda wh: wh.id == wh_id)
@@ -662,19 +683,19 @@ class SaleOrderMakePlan(models.TransientModel):
policy = line.product_id.product_tmpl_id.get_planning_policy() policy = line.product_id.product_tmpl_id.get_planning_policy()
if policy and policy.carrier_filter_id: if policy and policy.carrier_filter_id:
domain.extend(safe_eval(policy.carrier_filter_id.domain)) domain.extend(safe_eval(policy.carrier_filter_id.domain))
carriers = self.get_shipping_carriers(base_option.get('carrier_id'), domain=domain) carriers = self.get_shipping_carriers(base_option.get('carrier_id'), domain=domain, warehouse_id=base_option.get('warehouse_id'))
_logger.info('generate_shipping_options:: base_option: ' + str(base_option) + ' order_fake: ' + str(order_fake) + ' carriers: ' + str(carriers)) _logger.info('generate_shipping_options:: base_option: ' + str(base_option) + ' order_fake: ' + str(order_fake) + ' carriers: ' + str(carriers))
if not carriers: if not carriers:
return base_option return [base_option]
if not base_option.get('sub_options'): if not base_option.get('sub_options'):
options = [] options = []
# this locic comes from "delivery.models.sale_order.SaleOrder" # this locic comes from "delivery.models.sale_order.SaleOrder"
for carrier in carriers: for carrier in carriers:
option = self._generate_shipping_carrier_option(base_option, order_fake, carrier) carrier_options = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
if option: if carrier_options:
options.append(option) options += carrier_options
if options: if options:
return options return options
return [base_option] return [base_option]
@@ -686,26 +707,40 @@ class SaleOrderMakePlan(models.TransientModel):
for carrier in carriers: for carrier in carriers:
new_base_option = deepcopy(base_option) new_base_option = deepcopy(base_option)
has_error = False has_error = False
found_carrier_ids = set()
for wh_id, wh_vals in base_option['sub_options'].items(): for wh_id, wh_vals in base_option['sub_options'].items():
if has_error: if has_error:
continue continue
order_fake.warehouse_id = warehouses.filtered(lambda wh: wh.id == wh_id) order_fake.warehouse_id = warehouses.filtered(lambda wh: wh.id == wh_id)
order_fake.order_line = FakeCollection(filter(lambda line: line.product_id.id in wh_vals['product_ids'], original_order_fake_order_line)) order_fake.order_line = FakeCollection(list(filter(lambda line: line.product_id.id in wh_vals['product_ids'], original_order_fake_order_line)))
wh_option = self._generate_shipping_carrier_option(wh_vals, order_fake, carrier) wh_carrier_options = self._generate_shipping_carrier_option(wh_vals, order_fake, carrier)
if not wh_option: if not wh_carrier_options:
has_error = True has_error = True
else: else:
new_base_option['sub_options'][wh_id] = wh_option for _option in wh_carrier_options:
if _option.get('carrier_id'):
found_carrier_ids.add(_option['carrier_id'])
new_base_option['sub_options'][wh_id] = wh_carrier_options
if has_error: if has_error:
continue continue
# now that we've collected, we can roll up some details.
new_base_option['carrier_id'] = carrier.id # now that we've collected details for this carrier, we likely have more than one carrier's rates
new_base_option['shipping_price'] = self._get_shipping_price_for_options(new_base_option['sub_options']) _logger.info(' from ' + str(carrier) + ' found ' + str(found_carrier_ids))
new_base_option['requested_date'] = self._get_max_requested_date(new_base_option['sub_options']) for carrier_id in found_carrier_ids:
new_base_option['transit_days'] = self._get_max_transit_days(new_base_option['sub_options']) carrier_option = deepcopy(base_option)
options.append(new_base_option) carrier_option['carrier_id'] = False
for wh_id, wh_vals in base_option['sub_options'].items():
for co in new_base_option['sub_options'].get(wh_id, []):
if co.get('carrier_id') == carrier_id:
# we have found the rate!
carrier_option['carrier_id'] = carrier_id
carrier_option['sub_options'][wh_id] = co
if carrier_option['carrier_id']:
carrier_option['shipping_price'] = self._get_shipping_price_for_options(carrier_option['sub_options'])
carrier_option['requested_date'] = self._get_max_requested_date(carrier_option['sub_options'])
carrier_option['transit_days'] = self._get_max_transit_days(carrier_option['sub_options'])
options.append(carrier_option)
#restore values in case more processing occurs #restore values in case more processing occurs
order_fake.warehouse_id = original_order_fake_warehouse_id order_fake.warehouse_id = original_order_fake_warehouse_id
@@ -734,6 +769,8 @@ class SaleOrderMakePlan(models.TransientModel):
def _generate_shipping_carrier_option(self, base_option, order_fake, carrier): def _generate_shipping_carrier_option(self, base_option, order_fake, carrier):
# some carriers look at the order carrier_id # some carriers look at the order carrier_id
order_fake.carrier_id = carrier order_fake.carrier_id = carrier
date_planned = base_option.get('date_planned')
order_fake.date_planned = date_planned
# this logic comes from "delivery.models.sale_order.SaleOrder" # this logic comes from "delivery.models.sale_order.SaleOrder"
try: try:
@@ -741,7 +778,9 @@ class SaleOrderMakePlan(models.TransientModel):
date_delivered = None date_delivered = None
transit_days = 0 transit_days = 0
if carrier.delivery_type not in ['fixed', 'base_on_rule']: if carrier.delivery_type not in ['fixed', 'base_on_rule']:
if hasattr(carrier, 'rate_shipment_date_planned'): if hasattr(carrier, 'rate_shipment_multi'):
result = carrier.rate_shipment_multi(order=order_fake)
elif hasattr(carrier, 'rate_shipment_date_planned'):
# New API # New API
result = carrier.rate_shipment_date_planned(order_fake, base_option.get('date_planned')) result = carrier.rate_shipment_date_planned(order_fake, base_option.get('date_planned'))
if result: if result:
@@ -751,7 +790,8 @@ class SaleOrderMakePlan(models.TransientModel):
elif hasattr(carrier, 'get_shipping_price_for_plan'): elif hasattr(carrier, 'get_shipping_price_for_plan'):
# Old API # Old API
result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned')) result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned'))
if result and isinstance(result, list): if result and isinstance(result, list) and not isinstance(result[0], dict):
# this detects the above only if it isn't a list of dictionaries (aka multi-rating result)
price_unit, transit_days, date_delivered = result[0] price_unit, transit_days, date_delivered = result[0]
elif not result: elif not result:
rate = carrier.rate_shipment(order_fake) rate = carrier.rate_shipment(order_fake)
@@ -762,7 +802,7 @@ class SaleOrderMakePlan(models.TransientModel):
if rate.get('date_delivered'): if rate.get('date_delivered'):
date_delivered = rate.get('date_delivered') date_delivered = rate.get('date_delivered')
else: else:
_logger.warning('returning None because carrier: ' + str(carrier)) _logger.warning('returning None because carrier: ' + str(carrier) + ' returned rate: ' + str(rate))
return None return None
else: else:
carrier = carrier.available_carriers(order_fake.partner_shipping_id) carrier = carrier.available_carriers(order_fake.partner_shipping_id)
@@ -773,13 +813,38 @@ class SaleOrderMakePlan(models.TransientModel):
if order_fake.company_id.currency_id.id != order_fake.pricelist_id.currency_id.id: if order_fake.company_id.currency_id.id != order_fake.pricelist_id.currency_id.id:
price_unit = order_fake.company_id.currency_id.with_context(date=order_fake.date_order).compute(price_unit, order_fake.pricelist_id.currency_id) price_unit = order_fake.company_id.currency_id.with_context(date=order_fake.date_order).compute(price_unit, order_fake.pricelist_id.currency_id)
if result and isinstance(result, list):
res = []
for rate in result:
rate_carrier = rate.get('carrier')
if not rate_carrier:
continue
price_unit = rate['price']
date_delivered = rate.get('date_delivered')
transit_days = rate.get('transit_days')
if date_planned and transit_days and not date_delivered:
# compute from planned date anc current rate carrier
date_delivered = rate_carrier.calculate_date_delivered(date_planned, transit_days)
elif date_planned and date_delivered and not transit_days:
transit_days = rate_carrier.calculate_transit_days(date_planned, date_delivered)
final_price = float(price_unit) * (1.0 + (float(rate_carrier.margin) / 100.0))
option = deepcopy(base_option)
option['carrier_id'] = rate_carrier.id
option['shipping_price'] = final_price
option['requested_date'] = fields.Datetime.to_string(date_delivered) if (date_delivered and isinstance(date_delivered, datetime)) else date_delivered
option['transit_days'] = transit_days
res.append(option)
return res
else:
final_price = float(price_unit) * (1.0 + (float(carrier.margin) / 100.0)) final_price = float(price_unit) * (1.0 + (float(carrier.margin) / 100.0))
option = deepcopy(base_option) option = deepcopy(base_option)
option['carrier_id'] = carrier.id option['carrier_id'] = carrier.id
option['shipping_price'] = final_price option['shipping_price'] = final_price
option['requested_date'] = date_delivered option['requested_date'] = fields.Datetime.to_string(date_delivered) if (date_delivered and isinstance(date_delivered, datetime)) else date_delivered
option['transit_days'] = transit_days option['transit_days'] = transit_days
return option return [option]
except Exception as e: except Exception as e:
_logger.info("Exception collecting carrier rates: " + str(e)) _logger.info("Exception collecting carrier rates: " + str(e))
# Want to see more? # Want to see more?
@@ -796,12 +861,14 @@ class SaleOrderPlanningOption(models.TransientModel):
def datetime_converter(o): def datetime_converter(o):
if isinstance(o, datetime): if isinstance(o, datetime):
return str(o) return str(o)
if not isinstance(values, list):
if 'sub_options' in values and not isinstance(values['sub_options'], str): values = [values]
for wh_id, option in values['sub_options'].items(): for option_values in values:
if 'sub_options' in option_values and not isinstance(option_values['sub_options'], str):
for wh_id, option in option_values['sub_options'].items():
if option.get('date_planned'): if option.get('date_planned'):
option['date_planned'] = str(option['date_planned']) option['date_planned'] = str(option['date_planned'])
values['sub_options'] = dumps(values['sub_options'], default=datetime_converter) option_values['sub_options'] = dumps(option_values['sub_options'], default=datetime_converter)
return super(SaleOrderPlanningOption, self).create(values) return super(SaleOrderPlanningOption, self).create(values)
def _compute_sub_options_text(self): def _compute_sub_options_text(self):