mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Initial commit of sale_planner for 11.0
This commit is contained in:
2
sale_planner/__init__.py
Normal file
2
sale_planner/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import wizard
|
||||||
|
from . import models
|
||||||
40
sale_planner/__manifest__.py
Normal file
40
sale_planner/__manifest__.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
'name': 'Sale Order Planner',
|
||||||
|
'summary': 'Plans order dates and warehouses.',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "Hibou Corp.",
|
||||||
|
'category': 'Sale',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'complexity': 'expert',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
Sale Order Planner
|
||||||
|
==================
|
||||||
|
|
||||||
|
Plans sales order dates based on available warehouses and shipping methods.
|
||||||
|
|
||||||
|
Adds shipping calendar to warehouse to plan delivery orders based on availability
|
||||||
|
of the warehouse or warehouse staff.
|
||||||
|
|
||||||
|
Adds shipping calendar to individual shipping methods to estimate delivery based
|
||||||
|
on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
|
||||||
|
|
||||||
|
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'sale_order_dates',
|
||||||
|
'sale_sourced_by_line',
|
||||||
|
'base_geolocalize',
|
||||||
|
'delivery',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
'wizard/order_planner_views.xml',
|
||||||
|
'views/sale.xml',
|
||||||
|
'views/stock.xml',
|
||||||
|
'views/delivery.xml',
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
3
sale_planner/models/__init__.py
Normal file
3
sale_planner/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import sale
|
||||||
|
from . import stock
|
||||||
|
from . import delivery
|
||||||
69
sale_planner/models/delivery.py
Normal file
69
sale_planner/models/delivery.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryCarrier(models.Model):
|
||||||
|
_inherit = 'delivery.carrier'
|
||||||
|
|
||||||
|
delivery_calendar_id = fields.Many2one(
|
||||||
|
'resource.calendar', 'Delivery Calendar',
|
||||||
|
help="This calendar represents days that the carrier will deliver the package.")
|
||||||
|
|
||||||
|
# -------------------------- #
|
||||||
|
# API for external providers #
|
||||||
|
# -------------------------- #
|
||||||
|
|
||||||
|
def get_shipping_price_for_plan(self, orders, date_planned):
|
||||||
|
''' For every sale order, compute the price of the shipment
|
||||||
|
|
||||||
|
:param orders: A recordset of sale orders
|
||||||
|
:param date_planned: Date to say that the shipment is leaving.
|
||||||
|
:return list: A list of floats, containing the estimated price for the shipping of the sale order
|
||||||
|
'''
|
||||||
|
self.ensure_one()
|
||||||
|
if hasattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type):
|
||||||
|
return getattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type)(orders, date_planned)
|
||||||
|
|
||||||
|
def calculate_transit_days(self, date_planned, date_delivered):
|
||||||
|
self.ensure_one()
|
||||||
|
if isinstance(date_planned, str):
|
||||||
|
date_planned = fields.Datetime.from_string(date_planned)
|
||||||
|
if isinstance(date_delivered, str):
|
||||||
|
date_delivered = fields.Datetime.from_string(date_delivered)
|
||||||
|
|
||||||
|
transit_days = 0
|
||||||
|
while date_planned < date_delivered:
|
||||||
|
if transit_days > 10:
|
||||||
|
break
|
||||||
|
interval = self.delivery_calendar_id.schedule_days(1, date_planned, compute_leaves=True)
|
||||||
|
|
||||||
|
if not interval:
|
||||||
|
return self._calculate_transit_days_naive(date_planned, date_delivered)
|
||||||
|
date_planned = interval[0][1]
|
||||||
|
transit_days += 1
|
||||||
|
|
||||||
|
if transit_days > 1:
|
||||||
|
transit_days -= 1
|
||||||
|
|
||||||
|
return transit_days
|
||||||
|
|
||||||
|
def _calculate_transit_days_naive(self, date_planned, date_delivered):
|
||||||
|
return abs((date_delivered - date_planned).days)
|
||||||
|
|
||||||
|
def calculate_date_delivered(self, date_planned, transit_days):
|
||||||
|
self.ensure_one()
|
||||||
|
if isinstance(date_planned, str):
|
||||||
|
date_planned = fields.Datetime.from_string(date_planned)
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
if not interval:
|
||||||
|
return self._calculate_date_delivered_naive(date_planned, transit_days)
|
||||||
|
|
||||||
|
return fields.Datetime.to_string(interval[-1][1])
|
||||||
|
|
||||||
|
def _calculate_date_delivered_naive(self, date_planned, transit_days):
|
||||||
|
return fields.Datetime.to_string(date_planned + timedelta(days=transit_days))
|
||||||
16
sale_planner/models/sale.py
Normal file
16
sale_planner/models/sale.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrder(models.Model):
|
||||||
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_planorder(self):
|
||||||
|
plan_obj = self.env['sale.order.make.plan']
|
||||||
|
for order in self:
|
||||||
|
plan = plan_obj.create({
|
||||||
|
'order_id': order.id,
|
||||||
|
})
|
||||||
|
action = self.env.ref('sale_planner.action_plan_sale_order').read()[0]
|
||||||
|
action['res_id'] = plan.id
|
||||||
|
return action
|
||||||
9
sale_planner/models/stock.py
Normal file
9
sale_planner/models/stock.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class Warehouse(models.Model):
|
||||||
|
_inherit = 'stock.warehouse'
|
||||||
|
|
||||||
|
shipping_calendar_id = fields.Many2one(
|
||||||
|
'resource.calendar', 'Shipping Calendar',
|
||||||
|
help="This calendar represents shipping availability from the warehouse.")
|
||||||
1
sale_planner/tests/__init__.py
Normal file
1
sale_planner/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_planner
|
||||||
204
sale_planner/tests/test_planner.py
Normal file
204
sale_planner/tests/test_planner.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
from odoo.tests import common
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
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': 'WH1',
|
||||||
|
'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',
|
||||||
|
'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_1 = self.product_1.product_variant_id
|
||||||
|
self.product_2 = self.env['product.template'].create({
|
||||||
|
'name': 'Product for WH2',
|
||||||
|
'type': 'product',
|
||||||
|
'standard_price': 1.0,
|
||||||
|
})
|
||||||
|
self.product_2 = self.product_2.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_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_both.id,
|
||||||
|
'new_quantity': 100,
|
||||||
|
}).change_product_qty()
|
||||||
|
|
||||||
|
def both_wh_ids(self):
|
||||||
|
return [self.warehouse_1.id, self.warehouse_2.id]
|
||||||
|
|
||||||
|
def test_planner_creation_internals(self):
|
||||||
|
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_planner_creation(self):
|
||||||
|
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_planner_creation_2(self):
|
||||||
|
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_planner_creation_split(self):
|
||||||
|
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.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_planner_creation_no_split(self):
|
||||||
|
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)
|
||||||
13
sale_planner/views/delivery.xml
Normal file
13
sale_planner/views/delivery.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_delivery_carrier_form_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">delivery.carrier.form.calendar</field>
|
||||||
|
<field name="model">delivery.carrier</field>
|
||||||
|
<field name="inherit_id" ref="delivery.view_delivery_carrier_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='integration_level']" position="after">
|
||||||
|
<field name="delivery_calendar_id" />
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
17
sale_planner/views/sale.xml
Normal file
17
sale_planner/views/sale.xml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_order_form_planner" model="ir.ui.view">
|
||||||
|
<field name="name">sale.order.form.planner</field>
|
||||||
|
<field name="model">sale.order</field>
|
||||||
|
<field name="inherit_id" ref="sale.view_order_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//header/button[@name='action_confirm']" position="before">
|
||||||
|
<button name="action_planorder"
|
||||||
|
type="object"
|
||||||
|
attrs="{'invisible': [('state', 'not in', ('draft'))]}"
|
||||||
|
string="Plan"
|
||||||
|
class="oe_highlight"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
13
sale_planner/views/stock.xml
Normal file
13
sale_planner/views/stock.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_warehouse_shipping_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">stock.warehouse.shipping.calendar</field>
|
||||||
|
<field name="model">stock.warehouse</field>
|
||||||
|
<field name="inherit_id" ref="stock.view_warehouse" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='partner_id']" position="after">
|
||||||
|
<field name="shipping_calendar_id" />
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
1
sale_planner/wizard/__init__.py
Normal file
1
sale_planner/wizard/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import order_planner
|
||||||
590
sale_planner/wizard/order_planner.py
Normal file
590
sale_planner/wizard/order_planner.py
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
from math import sin, cos, sqrt, atan2, radians
|
||||||
|
from json import dumps, loads
|
||||||
|
from copy import deepcopy
|
||||||
|
from datetime import datetime
|
||||||
|
from logging import getLogger
|
||||||
|
|
||||||
|
_logger = getLogger(__name__)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from uszipcode import ZipcodeSearchEngine
|
||||||
|
except ImportError:
|
||||||
|
_logger.warn('module "uszipcode" cannot be loaded, falling back to Google API')
|
||||||
|
ZipcodeSearchEngine = None
|
||||||
|
|
||||||
|
from odoo import api, fields, models, tools
|
||||||
|
from odoo.addons.base_geolocalize.models.res_partner import geo_find, geo_query_address
|
||||||
|
|
||||||
|
|
||||||
|
class FakeCollection():
|
||||||
|
def __init__(self, vals):
|
||||||
|
self.vals = vals
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for v in self.vals:
|
||||||
|
yield v
|
||||||
|
|
||||||
|
def filtered(self, f):
|
||||||
|
return filter(f, self.vals)
|
||||||
|
|
||||||
|
|
||||||
|
class FakePartner():
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
'delivery.carrier'.verify_carrier(contact) ->
|
||||||
|
country_id,
|
||||||
|
state_id,
|
||||||
|
zip
|
||||||
|
company
|
||||||
|
|
||||||
|
city,
|
||||||
|
|
||||||
|
`distance calculations` ->
|
||||||
|
date_localization,
|
||||||
|
partner_latitude,
|
||||||
|
partner_longitude
|
||||||
|
|
||||||
|
computes them when accessed
|
||||||
|
"""
|
||||||
|
self.partner_latitude = 0.0
|
||||||
|
self.partner_longitude = 0.0
|
||||||
|
self.is_company = False
|
||||||
|
for attr, value in kwargs.items():
|
||||||
|
_logger.warn(' ' + str(attr) + ': ' + str(value))
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date_localization(self):
|
||||||
|
if not hasattr(self, 'date_localization'):
|
||||||
|
self.date_localization = 'TODAY!'
|
||||||
|
# The fast way.
|
||||||
|
if ZipcodeSearchEngine and self.zip:
|
||||||
|
with ZipcodeSearchEngine() as search:
|
||||||
|
zipcode = search.by_zipcode(self.zip)
|
||||||
|
if zipcode:
|
||||||
|
self.partner_latitude = zipcode['Latitude']
|
||||||
|
self.partner_longitude = zipcode['Longitude']
|
||||||
|
return self.date_localization
|
||||||
|
|
||||||
|
# The slow way.
|
||||||
|
result = geo_find(geo_query_address(
|
||||||
|
city=self.city,
|
||||||
|
state=self.state_id.name,
|
||||||
|
country=self.country_id.name,
|
||||||
|
))
|
||||||
|
if result:
|
||||||
|
self.partner_latitude = result[0]
|
||||||
|
self.partner_longitude = result[1]
|
||||||
|
|
||||||
|
return self.date_localization
|
||||||
|
|
||||||
|
|
||||||
|
class FakeOrderLine():
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
"""
|
||||||
|
'delivery.carrier'.get_price_available(order) ->
|
||||||
|
state,
|
||||||
|
is_delivery,
|
||||||
|
product_uom._compute_quantity,
|
||||||
|
product_uom_qty,
|
||||||
|
product_id
|
||||||
|
price_total
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.state = 'draft'
|
||||||
|
self.is_delivery = False
|
||||||
|
self.product_uom = self
|
||||||
|
|
||||||
|
for attr, value in kwargs.items():
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
|
def _compute_quantity(self, qty=1, uom=None):
|
||||||
|
"""
|
||||||
|
This is a non-implementation for when someone wants to call product_uom._compute_quantity
|
||||||
|
:param qty:
|
||||||
|
:param uom:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return qty
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSaleOrder():
|
||||||
|
"""
|
||||||
|
partner_id :: used in shipping
|
||||||
|
partner_shipping_id :: is used in several places
|
||||||
|
order_line :: can be a FakeCollection of FakeOrderLine's or Odoo 'sale.order.line'
|
||||||
|
carrier_id :: can be empty, will be overwritten when walking through carriers
|
||||||
|
|
||||||
|
'delivery.carrier'.get_shipping_price_from_so(orders) ->
|
||||||
|
id, (int)
|
||||||
|
name, (String)
|
||||||
|
currency_id, (Odoo 'res.currency')
|
||||||
|
company_id, (Odoo 'res.company')
|
||||||
|
warehouse_id, (Odoo 'stock.warehouse')
|
||||||
|
carrier_id, (Odoo 'delivery.carrier')
|
||||||
|
|
||||||
|
SaleOrderMakePlan.generate_shipping_options() ->
|
||||||
|
pricelist_id, (Odoo 'product.pricelist')
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.carrier_id = None
|
||||||
|
self.id = 0
|
||||||
|
self.name = 'Quote'
|
||||||
|
self.team_id = None
|
||||||
|
self.project_id = None
|
||||||
|
self.amount_total = 0.0
|
||||||
|
for attr, value in kwargs.items():
|
||||||
|
setattr(self, attr, value)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
"""
|
||||||
|
Emulate a recordset of a single order.
|
||||||
|
"""
|
||||||
|
yield self
|
||||||
|
|
||||||
|
|
||||||
|
def distance(lat_1, lon_1, lat_2, lon_2):
|
||||||
|
R = 6373.0
|
||||||
|
|
||||||
|
lat1 = radians(lat_1)
|
||||||
|
lon1 = radians(lon_1)
|
||||||
|
lat2 = radians(lat_2)
|
||||||
|
lon2 = radians(lon_2)
|
||||||
|
|
||||||
|
dlon = lon2 - lon1
|
||||||
|
dlat = lat2 - lat1
|
||||||
|
|
||||||
|
a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
|
||||||
|
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
|
||||||
|
return R * c
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrderMakePlan(models.TransientModel):
|
||||||
|
_name = 'sale.order.make.plan'
|
||||||
|
_description = 'Plan Order'
|
||||||
|
|
||||||
|
order_id = fields.Many2one(
|
||||||
|
'sale.order', 'Sale Order',
|
||||||
|
)
|
||||||
|
planning_option_ids = fields.One2many('sale.order.planning.option', 'plan_id', 'Options')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def plan_order(self, vals):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def select_option(self, option):
|
||||||
|
for plan in self:
|
||||||
|
self.plan_order_option(plan.order_id, option)
|
||||||
|
|
||||||
|
def _order_fields_for_option(self, option):
|
||||||
|
return {
|
||||||
|
'warehouse_id': option.warehouse_id.id,
|
||||||
|
'requested_date': option.requested_date,
|
||||||
|
'date_planned': option.date_planned,
|
||||||
|
'carrier_id': option.carrier_id.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def plan_order_option(self, order, option):
|
||||||
|
if option.sub_options:
|
||||||
|
sub_options = option.sub_options
|
||||||
|
if isinstance(sub_options, str):
|
||||||
|
sub_options = loads(sub_options)
|
||||||
|
if not isinstance(sub_options, dict):
|
||||||
|
_logger.warn('Cannot apply option with corrupt sub_options')
|
||||||
|
return False
|
||||||
|
order_lines = order.order_line
|
||||||
|
for wh_id, wh_vals in sub_options.items():
|
||||||
|
wh_id = int(wh_id)
|
||||||
|
if wh_id == option.warehouse_id.id:
|
||||||
|
continue
|
||||||
|
order_lines.filtered(lambda line: line.product_id.id in wh_vals['product_ids']).write({
|
||||||
|
'warehouse_id': wh_id,
|
||||||
|
'date_planned': wh_vals.get('date_planned'),
|
||||||
|
})
|
||||||
|
|
||||||
|
order_fields = self._order_fields_for_option(option)
|
||||||
|
|
||||||
|
order.write(order_fields)
|
||||||
|
|
||||||
|
if option.carrier_id:
|
||||||
|
order._create_delivery_line(option.carrier_id, option.shipping_price)
|
||||||
|
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, values):
|
||||||
|
planner = super(SaleOrderMakePlan, self).create(values)
|
||||||
|
|
||||||
|
for option_vals in self.generate_order_options(planner.order_id):
|
||||||
|
option_vals['plan_id'] = planner.id
|
||||||
|
planner.planning_option_ids |= self.env['sale.order.planning.option'].create(option_vals)
|
||||||
|
|
||||||
|
return planner
|
||||||
|
|
||||||
|
def _fake_order(self, order):
|
||||||
|
return FakeSaleOrder(**{
|
||||||
|
'id': order.id,
|
||||||
|
'name': order.name,
|
||||||
|
'partner_id': order.partner_id,
|
||||||
|
'partner_shipping_id': order.partner_shipping_id,
|
||||||
|
'order_line': order.order_line,
|
||||||
|
'currency_id': order.currency_id,
|
||||||
|
'company_id': order.company_id,
|
||||||
|
'warehouse_id': order.warehouse_id,
|
||||||
|
'amount_total': order.amount_total,
|
||||||
|
'pricelist_id': order.pricelist_id,
|
||||||
|
'env': self.env,
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def generate_order_options(self, order, plan_shipping=True):
|
||||||
|
fake_order = self._fake_order(order)
|
||||||
|
base_option = self.generate_base_option(fake_order)
|
||||||
|
# do we need shipping?
|
||||||
|
# we need to collect it because we want multi-warehouse shipping amounts.
|
||||||
|
if order.carrier_id:
|
||||||
|
base_option['carrier_id'] = order.carrier_id.id
|
||||||
|
|
||||||
|
if plan_shipping and not self.env.context.get('skip_plan_shipping'):
|
||||||
|
options = self.generate_shipping_options(base_option, fake_order)
|
||||||
|
else:
|
||||||
|
options = [base_option]
|
||||||
|
|
||||||
|
return options
|
||||||
|
|
||||||
|
def get_warehouses(self, warehouse_id=None):
|
||||||
|
warehouse = self.env['stock.warehouse'].sudo()
|
||||||
|
if warehouse_id:
|
||||||
|
return warehouse.search([('id', '=', warehouse_id)])
|
||||||
|
|
||||||
|
if self.env.context.get('warehouse_domain'):
|
||||||
|
#potential bug here if this is textual
|
||||||
|
return warehouse.search(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'))
|
||||||
|
return warehouse.search(domain)
|
||||||
|
|
||||||
|
return warehouse.search([])
|
||||||
|
|
||||||
|
def get_shipping_carriers(self, carrier_id=None):
|
||||||
|
Carrier = self.env['delivery.carrier'].sudo()
|
||||||
|
if carrier_id:
|
||||||
|
return Carrier.search([('id', '=', carrier_id)])
|
||||||
|
|
||||||
|
if self.env.context.get('carrier_domain'):
|
||||||
|
# potential bug here if this is textual
|
||||||
|
return Carrier.search(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'))
|
||||||
|
return Carrier.search(domain)
|
||||||
|
|
||||||
|
return Carrier.search([])
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
def _find_closest_warehouse_by_partner(self, warehouses, partner):
|
||||||
|
if not partner.date_localization:
|
||||||
|
partner.geo_localize()
|
||||||
|
return self._find_closest_warehouse(warehouses, partner.partner_latitude, partner.partner_longitude)
|
||||||
|
|
||||||
|
def _find_closest_warehouse(self, warehouses, latitude, longitude):
|
||||||
|
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)]
|
||||||
|
return warehouses.filtered(lambda wh: wh.id == wh_id)
|
||||||
|
|
||||||
|
def _sort_warehouses_by_partner(self, warehouses, partner):
|
||||||
|
if not partner.date_localization:
|
||||||
|
partner.geo_localize()
|
||||||
|
return self._sort_warehouses(warehouses, partner.partner_latitude, partner.partner_longitude)
|
||||||
|
|
||||||
|
def _sort_warehouses(self, warehouses, latitude, longitude):
|
||||||
|
distances = {distance(latitude, longitude, wh.partner_id.partner_latitude, wh.partner_id.partner_longitude): wh.id for wh in warehouses}
|
||||||
|
wh_distances = sorted(distances)
|
||||||
|
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))
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _fetch_product_stock(self, warehouses, products):
|
||||||
|
output = {}
|
||||||
|
for wh in warehouses:
|
||||||
|
products = products.with_context({'location': wh.lot_stock_id.id})
|
||||||
|
output[wh.id] = {
|
||||||
|
p.id: {
|
||||||
|
'qty_available': p.qty_available,
|
||||||
|
'virtual_available': p.virtual_available,
|
||||||
|
'incoming_qty': p.incoming_qty,
|
||||||
|
'outgoing_qty': p.outgoing_qty,
|
||||||
|
'real_qty_available': p.qty_available - p.outgoing_qty,
|
||||||
|
'sku': p.default_code
|
||||||
|
} for p in products}
|
||||||
|
return output
|
||||||
|
|
||||||
|
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'))
|
||||||
|
|
||||||
|
if not base_option.get('sub_options'):
|
||||||
|
options = []
|
||||||
|
# this locic comes from "delivery.models.sale_order.SaleOrder"
|
||||||
|
for carrier in carriers:
|
||||||
|
option = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
|
||||||
|
if option:
|
||||||
|
options.append(option)
|
||||||
|
|
||||||
|
return options
|
||||||
|
else:
|
||||||
|
warehouses = self.get_warehouses()
|
||||||
|
original_order_fake_warehouse_id = order_fake.warehouse_id
|
||||||
|
original_order_fake_order_line = order_fake.order_line
|
||||||
|
options = []
|
||||||
|
for carrier in carriers:
|
||||||
|
new_base_option = deepcopy(base_option)
|
||||||
|
has_error = False
|
||||||
|
for wh_id, wh_vals in base_option['sub_options'].items():
|
||||||
|
if has_error:
|
||||||
|
continue
|
||||||
|
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))
|
||||||
|
wh_option = self._generate_shipping_carrier_option(wh_vals, order_fake, carrier)
|
||||||
|
if not wh_option:
|
||||||
|
has_error = True
|
||||||
|
else:
|
||||||
|
new_base_option['sub_options'][wh_id] = wh_option
|
||||||
|
|
||||||
|
if has_error:
|
||||||
|
continue
|
||||||
|
# now that we've collected, we can roll up some details.
|
||||||
|
|
||||||
|
new_base_option['carrier_id'] = carrier.id
|
||||||
|
new_base_option['shipping_price'] = self._get_shipping_price_for_options(new_base_option['sub_options'])
|
||||||
|
new_base_option['requested_date'] = self._get_max_requested_date(new_base_option['sub_options'])
|
||||||
|
new_base_option['transit_days'] = self._get_max_transit_days(new_base_option['sub_options'])
|
||||||
|
options.append(new_base_option)
|
||||||
|
|
||||||
|
#restore values in case more processing occurs
|
||||||
|
order_fake.warehouse_id = original_order_fake_warehouse_id
|
||||||
|
order_fake.order_line = original_order_fake_order_line
|
||||||
|
if not options:
|
||||||
|
options.append(base_option)
|
||||||
|
return options
|
||||||
|
|
||||||
|
def _get_shipping_price_for_options(self, sub_options):
|
||||||
|
return sum(wh_option.get('shipping_price', 0.0) for wh_option in sub_options.values())
|
||||||
|
|
||||||
|
def _get_max_requested_date(self, sub_options):
|
||||||
|
max_requested_date = None
|
||||||
|
for option in sub_options.values():
|
||||||
|
requested_date = option.get('requested_date')
|
||||||
|
if requested_date and isinstance(requested_date, str):
|
||||||
|
requested_date = fields.Datetime.from_string(requested_date)
|
||||||
|
if requested_date and not max_requested_date:
|
||||||
|
max_requested_date = requested_date
|
||||||
|
elif requested_date:
|
||||||
|
if requested_date > max_requested_date:
|
||||||
|
max_requested_date = requested_date
|
||||||
|
if max_requested_date:
|
||||||
|
return fields.Datetime.to_string(max_requested_date)
|
||||||
|
return max_requested_date
|
||||||
|
|
||||||
|
def _get_max_transit_days(self, sub_options):
|
||||||
|
return max(wh_option.get('transit_days', 0) for wh_option in sub_options.values())
|
||||||
|
|
||||||
|
def _generate_shipping_carrier_option(self, base_option, order_fake, carrier):
|
||||||
|
# some carriers look at the order carrier_id
|
||||||
|
order_fake.carrier_id = carrier
|
||||||
|
|
||||||
|
# this logic comes from "delivery.models.sale_order.SaleOrder"
|
||||||
|
try:
|
||||||
|
date_delivered = None
|
||||||
|
transit_days = 0
|
||||||
|
if carrier.delivery_type not in ['fixed', 'base_on_rule']:
|
||||||
|
result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned'))
|
||||||
|
if result:
|
||||||
|
price_unit, transit_days, date_delivered = result[0]
|
||||||
|
else:
|
||||||
|
price_unit = carrier.get_shipping_price_from_so(order_fake)[0]
|
||||||
|
else:
|
||||||
|
carrier = carrier.verify_carrier(order_fake.partner_shipping_id)
|
||||||
|
if not carrier:
|
||||||
|
return None
|
||||||
|
#price_unit = carrier.get_price_available(order)
|
||||||
|
# ^^ ends up calling carrier.get_price_from_picking(order_total, weight, volume, quantity)
|
||||||
|
order_total = order_fake.amount_total
|
||||||
|
weight = sum((line.product_id.weight or 0.0) * line.product_uom_qty for line in order_fake.order_line if line.product_id.type == 'product')
|
||||||
|
volume = sum((line.product_id.volume or 0.0) * line.product_uom_qty for line in order_fake.order_line if line.product_id.type == 'product')
|
||||||
|
quantity = sum((line.product_uom_qty or 0.0) for line in order_fake.order_line if line.product_id.type == 'product')
|
||||||
|
price_unit = carrier.get_price_from_picking(order_total, weight, volume, quantity)
|
||||||
|
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)
|
||||||
|
|
||||||
|
final_price = float(price_unit) * (1.0 + (float(carrier.margin) / 100.0))
|
||||||
|
option = deepcopy(base_option)
|
||||||
|
option['carrier_id'] = 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
|
||||||
|
return option
|
||||||
|
except Exception as e:
|
||||||
|
# _logger.warn("Exception collecting carrier rates: " + str(e))
|
||||||
|
# _logger.exception(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrderPlanningOption(models.TransientModel):
|
||||||
|
_name = 'sale.order.planning.option'
|
||||||
|
_description = 'Order Planning Option'
|
||||||
|
|
||||||
|
def create(self, values):
|
||||||
|
if 'sub_options' in values and not isinstance(values['sub_options'], str):
|
||||||
|
values['sub_options'] = dumps(values['sub_options'])
|
||||||
|
return super(SaleOrderPlanningOption, self).create(values)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def _compute_sub_options_text(self):
|
||||||
|
for option in self:
|
||||||
|
sub_options = option.sub_options
|
||||||
|
if sub_options and not isinstance(sub_options, dict):
|
||||||
|
sub_options = loads(sub_options)
|
||||||
|
if not isinstance(sub_options, dict):
|
||||||
|
option.sub_options_text = ''
|
||||||
|
continue
|
||||||
|
|
||||||
|
line = ''
|
||||||
|
for wh_id, wh_option in sub_options.items():
|
||||||
|
product_skus = wh_option.get('product_skus', [])
|
||||||
|
product_skus = ', '.join(product_skus)
|
||||||
|
requested_date = wh_option.get('requested_date', '')
|
||||||
|
if requested_date:
|
||||||
|
requested_date = self._context_datetime(requested_date)
|
||||||
|
date_planned = wh_option.get('date_planned', '')
|
||||||
|
if date_planned:
|
||||||
|
date_planned = self._context_datetime(date_planned)
|
||||||
|
shipping_price = float(wh_option.get('shipping_price', 0.0))
|
||||||
|
transit_days = int(wh_option.get('transit_days', 0))
|
||||||
|
|
||||||
|
line += """WH %d :: %s
|
||||||
|
Date Planned: %s
|
||||||
|
Requested Date: %s
|
||||||
|
Transit Days: %d
|
||||||
|
Shipping Price: %.2f
|
||||||
|
|
||||||
|
""" % (int(wh_id), product_skus, date_planned, requested_date, transit_days, shipping_price)
|
||||||
|
|
||||||
|
option.sub_options_text = line
|
||||||
|
|
||||||
|
plan_id = fields.Many2one('sale.order.make.plan', 'Plan', ondelete='cascade')
|
||||||
|
warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
|
||||||
|
date_planned = fields.Datetime('Planned Date')
|
||||||
|
requested_date = fields.Datetime('Requested Date')
|
||||||
|
carrier_id = fields.Many2one('delivery.carrier', string='Carrier')
|
||||||
|
transit_days = fields.Integer('Transit Days')
|
||||||
|
shipping_price = fields.Float('Shipping Price')
|
||||||
|
sub_options = fields.Text('Sub Options JSON')
|
||||||
|
sub_options_text = fields.Text('Sub Options', compute=_compute_sub_options_text)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def select_plan(self):
|
||||||
|
for option in self:
|
||||||
|
option.plan_id.select_option(option)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _context_datetime(self, date):
|
||||||
|
date = fields.Datetime.from_string(date)
|
||||||
|
date = fields.Datetime.context_timestamp(self, date)
|
||||||
|
return fields.Datetime.to_string(date)
|
||||||
40
sale_planner/wizard/order_planner_views.xml
Normal file
40
sale_planner/wizard/order_planner_views.xml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_plan_sale_order" model="ir.ui.view">
|
||||||
|
<field name="name">view.plan.sale.order</field>
|
||||||
|
<field name="model">sale.order.make.plan</field>
|
||||||
|
<field name="type">form</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<field name="planning_option_ids">
|
||||||
|
<tree>
|
||||||
|
<field name="warehouse_id" />
|
||||||
|
<field name="date_planned" />
|
||||||
|
<field name="requested_date" />
|
||||||
|
<field name="transit_days" />
|
||||||
|
<field name="carrier_id" />
|
||||||
|
<field name="shipping_price" />
|
||||||
|
<field name="sub_options_text" />
|
||||||
|
<button class="eo_highlight"
|
||||||
|
name="select_plan"
|
||||||
|
string="Select"
|
||||||
|
type="object" />
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
<footer>
|
||||||
|
<button class="oe_link"
|
||||||
|
special="cancel"
|
||||||
|
string="Cancel" />
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<record id="action_plan_sale_order" model="ir.actions.act_window">
|
||||||
|
<field name="name">Plan Sale Order</field>
|
||||||
|
<field name="res_model">sale.order.make.plan</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="view_id" ref="view_plan_sale_order" />
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Reference in New Issue
Block a user