[IMP] stock_delivery_planner: multi-rating per-package shipping backports from 14.0

This commit is contained in:
Jared Kipe
2021-10-04 10:56:49 -07:00
parent e828e3875e
commit d5caa630b7
11 changed files with 180 additions and 30 deletions

View File

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

View File

@@ -1,10 +1,10 @@
{ {
'name': 'Stock Delivery Planner', 'name': 'Stock Delivery Planner',
'summary': 'Get rates and choose carrier for delivery.', 'summary': 'Get rates and choose carrier for delivery.',
'version': '12.0.1.0.0', 'version': '12.0.1.1.0',
'author': "Hibou Corp.", 'author': "Hibou Corp.",
'category': 'Warehouse', 'category': 'Warehouse',
'license': 'AGPL-3', 'license': 'OPL-1',
'website': "https://hibou.io", 'website': "https://hibou.io",
'description': """ 'description': """
Stock Delivery Planner Stock Delivery Planner
@@ -19,6 +19,8 @@ Re-rate deliveries at packing time to find lowest-priced delivery method that st
'stock', 'stock',
], ],
'data': [ 'data': [
'security/ir.model.access.csv',
'views/res_config_settings_views.xml',
'views/stock_views.xml', 'views/stock_views.xml',
'wizard/stock_delivery_planner_views.xml', 'wizard/stock_delivery_planner_views.xml',
], ],

View File

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

View File

@@ -0,0 +1,43 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
stock_delivery_planner_base_carrier_ids = fields.Many2many('delivery.carrier',
string='Delivery Planner Base Carriers',
compute='_compute_stock_delivery_planner_base_carrier_ids',
inverse='_inverse_stock_delivery_planner_base_carrier_ids')
def _compute_stock_delivery_planner_base_carrier_ids_ids(self):
# used to compute the field and update in get_values
get_param = self.env['ir.config_parameter'].sudo().get_param
company_id = self.company_id.id or self.env.user.company_id.id
carrier_ids = get_param('stock.delivery.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
def _compute_stock_delivery_planner_base_carrier_ids(self):
for settings in self:
carrier_ids = settings._compute_stock_delivery_planner_base_carrier_ids_ids()
carriers = self.env['delivery.carrier'].browse(carrier_ids)
settings.stock_delivery_planner_base_carrier_ids = carriers
def _inverse_stock_delivery_planner_base_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.stock_delivery_planner_base_carrier_ids.ids)
set_param('stock.delivery.planner.carrier_ids.%s' % (company_id, ), carrier_ids)
@api.model
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res['stock_delivery_planner_base_carrier_ids'] = [(6, 0, self._compute_stock_delivery_planner_base_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, tools, _ from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError from odoo.exceptions import UserError

View File

@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_stock_delivery_planner_user,stock.delivery.planner user,model_stock_delivery_planner,stock.group_stock_user,1,1,1,0
access_stock_delivery_planner_option_user,stock.delivery.planner.option user,model_stock_delivery_planner_option,stock.group_stock_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_stock_delivery_planner_user stock.delivery.planner user model_stock_delivery_planner stock.group_stock_user 1 1 1 0
3 access_stock_delivery_planner_option_user stock.delivery.planner.option user model_stock_delivery_planner_option stock.group_stock_user 1 1 1 0

View File

@@ -1,3 +1,5 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import fields from odoo import fields
from odoo.tests.common import Form, TransactionCase from odoo.tests.common import Form, TransactionCase
@@ -15,8 +17,8 @@ class TestStockDeliveryPlanner(TransactionCase):
self.skipTest('FedEx Shipping Connector demo data is required to run this test.') 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', self.env['ir.config_parameter'].sudo().set_param('sale.order.planner.carrier_domain',
"[('id', 'in', (%d,))]" % self.fedex_ground.id) "[('id', 'in', (%d,))]" % self.fedex_ground.id)
self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_domain', self.env['ir.config_parameter'].sudo().set_param('stock.delivery.planner.carrier_ids.%s' % (self.env.user.company_id.id, ),
"[('id', 'in', (%d,))]" % self.fedex_ground.id) "%d" % self.fedex_ground.id)
# Does it make sense to set default package in fedex_rate_shipment_multi # Does it make sense to set default package in fedex_rate_shipment_multi
# instead of relying on a correctly configured delivery method? # instead of relying on a correctly configured delivery method?
self.fedex_package = self.browse_ref('delivery_fedex.fedex_packaging_FEDEX_25KG_BOX') self.fedex_package = self.browse_ref('delivery_fedex.fedex_packaging_FEDEX_25KG_BOX')
@@ -109,11 +111,11 @@ class TestStockDeliveryPlanner(TransactionCase):
self.assertEqual(self.picking.shipping_weight, 0.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 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_action = self.picking.action_put_in_pack()
packing_wizard = Form(self.env[packing_action['res_model']].with_context(packing_action['context'])) packing_wizard = Form(self.env[packing_action['res_model']].with_context(packing_action['context']))
packing_wizard.delivery_packaging_id = self.fedex_package packing_wizard.delivery_packaging_id = self.fedex_package
choose_delivery_package = packing_wizard.save() choose_delivery_package = packing_wizard.save()
choose_delivery_package.put_in_pack() choose_delivery_package.action_put_in_pack()
self.assertEqual(self.picking.shipping_weight, 5.0) self.assertEqual(self.picking.shipping_weight, 5.0)
action = self.picking.action_plan_delivery() action = self.picking.action_plan_delivery()

View File

@@ -0,0 +1,28 @@
<?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="50"/>
<field name="inherit_id" ref="delivery.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sale_ebay']" position="after">
<h2>Delivery Planner</h2>
<div class="col-lg-6 col-12 o_setting_box" id="stock_delivery_planner">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<label for="stock_delivery_planner_base_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="stock_delivery_planner_base_carrier_ids" class="oe_inline" />
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -6,7 +6,7 @@
<field name="inherit_id" ref="stock.view_picking_form"/> <field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//header" position="inside"> <xpath expr="//header" position="inside">
<button name="action_plan_delivery" type="object" string="Plan Shipment" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/> <button name="action_plan_delivery" type="object" string="Plan Shipment" class="oe_highlight" attrs="{'invisible': [('picking_type_code', '!=', 'outgoing')]}"/>
</xpath> </xpath>
</field> </field>
</record> </record>

View File

@@ -1,4 +1,7 @@
from odoo import api, fields, models, tools # Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import safe_eval
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -9,14 +12,28 @@ class StockDeliveryPlanner(models.TransientModel):
picking_id = fields.Many2one('stock.picking', 'Transfer') picking_id = fields.Many2one('stock.picking', 'Transfer')
plan_option_ids = fields.One2many('stock.delivery.planner.option', 'plan_id', 'Options') plan_option_ids = fields.One2many('stock.delivery.planner.option', 'plan_id', 'Options')
packages_planned = fields.Boolean(compute='_compute_packages_planned')
@api.depends('plan_option_ids.selection')
def _compute_packages_planned(self):
for wiz in self:
packages = wiz.picking_id.package_ids
if not packages:
wiz.packages_planned = False
selected_options = wiz.plan_option_ids.filtered(lambda p: p.selection == 'selected')
wiz.packages_planned = len(selected_options) == len(packages)
def create(self, values): def create(self, values):
planner = super(StockDeliveryPlanner, self).create(values) planner = super(StockDeliveryPlanner, self).create(values)
base_carriers = self.env['delivery.carrier'] base_carriers = self.env['delivery.carrier']
carrier_domain = self.env['ir.config_parameter'].sudo().get_param('stock.delivery.planner.carrier_domain') carrier_ids = self.env['ir.config_parameter'].sudo().get_param('stock.delivery.planner.carrier_ids.%s' % (self.env.user.company_id.id, ))
if carrier_domain: if carrier_ids:
base_carriers = base_carriers.search(tools.safe_eval(carrier_domain)) try:
carrier_ids = [int(c) for c in carrier_ids.split(',')]
base_carriers = base_carriers.browse(carrier_ids)
except:
pass
for carrier in base_carriers: for carrier in base_carriers:
rates = carrier.rate_shipment_multi(picking=planner.picking_id) rates = carrier.rate_shipment_multi(picking=planner.picking_id)
@@ -24,9 +41,12 @@ class StockDeliveryPlanner(models.TransientModel):
_logger.warning(rate.get('error_message')) _logger.warning(rate.get('error_message'))
for rate in filter(lambda r: r.get('success'), rates): for rate in filter(lambda r: r.get('success'), rates):
rate = self.calculate_delivery_window(rate) rate = self.calculate_delivery_window(rate)
# added late in API dev cycle
package = rate.get('package') or self.env['stock.quant.package'].browse()
planner.plan_option_ids |= planner.plan_option_ids.create({ planner.plan_option_ids |= planner.plan_option_ids.create({
'plan_id': self.id, 'plan_id': self.id,
'carrier_id': rate['carrier'].id, 'carrier_id': rate['carrier'].id,
'package_id': package.id,
'price': rate['price'], 'price': rate['price'],
'date_planned': rate['date_planned'], 'date_planned': rate['date_planned'],
'requested_date': rate['date_delivered'], 'requested_date': rate['date_delivered'],
@@ -47,6 +67,12 @@ class StockDeliveryPlanner(models.TransientModel):
rate['date_delivered'] = carrier.calculate_date_delivered(date_planned, rate.get('transit_days')) rate['date_delivered'] = carrier.calculate_date_delivered(date_planned, rate.get('transit_days'))
return rate return rate
def action_plan(self):
self.ensure_one()
selected_package_options = self.plan_option_ids.filtered(lambda o: o.package_id and o.selection == 'selected')
selected_package_options._plan()
return {"type": "ir.actions.act_window_close"}
class StockDeliveryOption(models.TransientModel): class StockDeliveryOption(models.TransientModel):
_name = 'stock.delivery.planner.option' _name = 'stock.delivery.planner.option'
@@ -54,18 +80,46 @@ class StockDeliveryOption(models.TransientModel):
plan_id = fields.Many2one('stock.delivery.planner', 'Plan', ondelete='cascade') plan_id = fields.Many2one('stock.delivery.planner', 'Plan', ondelete='cascade')
carrier_id = fields.Many2one('delivery.carrier', 'Delivery Method') carrier_id = fields.Many2one('delivery.carrier', 'Delivery Method')
package_id = fields.Many2one('stock.quant.package', 'Package')
price = fields.Float('Shipping Price') price = fields.Float('Shipping Price')
date_planned = fields.Datetime('Planned Date') date_planned = fields.Datetime('Planned Date')
requested_date = fields.Datetime('Expected Delivery Date') requested_date = fields.Datetime('Expected Delivery Date')
transit_days = fields.Integer('Transit Days') transit_days = fields.Integer('Transit Days')
sale_requested_date = fields.Datetime('Sale Order Delivery Date', related='plan_id.picking_id.sale_id.requested_date') sale_requested_date = fields.Datetime('Sale Order Delivery Date', related='plan_id.picking_id.sale_id.requested_date')
days_different = fields.Float('Days Different', compute='_compute_days_different') # use carrier calendar days_different = fields.Float('Days Different', compute='_compute_days_different') # use carrier calendar
selection = fields.Selection([
('', 'None'),
('selected', 'Selected'),
('deselected', 'De-selected')
])
def _plan(self):
# this is intended to be used during selecting a whole plan
for option in self:
option.package_id.write({
'carrier_id': option.carrier_id.id,
})
@api.multi @api.multi
def select_plan(self): def select_plan(self):
for option in self.filtered('carrier_id'): self.ensure_one()
option.plan_id.picking_id.carrier_id = option.carrier_id self.selection = 'selected'
return if self.package_id:
# need to deselect other options for this package
deselected = self.plan_id.plan_option_ids.filtered(lambda o: o.package_id == self.package_id and o != self)
deselected.write({'selection': 'deselected'})
return {
'name': _('Delivery Rate Planner'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'stock.delivery.planner',
'res_id': self.plan_id.id,
'target': 'new',
}
else:
# Select plan for whole shipment
self.plan_id.picking_id.carrier_id = self.carrier_id
return {"type": "ir.actions.act_window_close"}
@api.depends('requested_date', 'sale_requested_date', 'carrier_id') @api.depends('requested_date', 'sale_requested_date', 'carrier_id')
def _compute_days_different(self): def _compute_days_different(self):

View File

@@ -6,22 +6,33 @@
<field name="type">form</field> <field name="type">form</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form> <form>
<field name="plan_option_ids"> <field name="packages_planned" invisible="1" />
<tree decoration-success="days_different &lt; 0.0" decoration-danger="days_different &gt; 0.0" <group>
create="false" edit="false" delete="false"> <field name="plan_option_ids" nolabel="1">
<field name="carrier_id"/> <tree decoration-info="selection == 'selected'"
<field name="date_planned"/> decoration-muted="selection == 'deselected'"
<field name="requested_date"/> default_order="package_id, price"
<field name="transit_days"/> create="false" edit="false" delete="false">
<field name="sale_requested_date"/> <field name="package_id" />
<field name="days_different"/> <field name="carrier_id" />
<field name="price"/> <field name="date_planned" invisible="1" />
<button class="eo_highlight" <field name="requested_date" decoration-danger="days_different &gt; 1.0" decoration-warning="days_different &gt; 0.0" decoration-success="days_different &lt; 0.0" />
name="select_plan" <field name="transit_days" decoration-warning="transit_days > 2.0" decoration-success="transit_days == 1.0"/>
string="Select" <field name="sale_requested_date" decoration-danger="days_different &gt; 1.0" decoration-warning="days_different &gt; 0.0" decoration-success="days_different &lt; 0.0" />
type="object" /> <field name="days_different" decoration-danger="days_different &gt; 1.0" decoration-warning="days_different &gt; 0.0" decoration-success="days_different &lt; 0.0" />
</tree> <field name="price" decoration-success="price &lt; 10.99" decoration-warning="price > 31.00 and price &lt; 50.00" decoration-danger="price >= 50.00" />
</field> <field name="selection" invisible="1" />
<button class="eo_highlight"
name="select_plan"
string="Select"
type="object" />
</tree>
</field>
</group>
<footer>
<button type="object" name="action_plan" string="Plan" class="btn-primary" attrs="{'invisible': [('packages_planned', '=', False)]}"/>
<button string="Discard" special="cancel" class="btn-secondary"/>
</footer>
</form> </form>
</field> </field>
</record> </record>