mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch '11.0' into 11.0-test
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -35,3 +35,6 @@
|
|||||||
[submodule "external/hibou-oca/stock-logistics-workflow"]
|
[submodule "external/hibou-oca/stock-logistics-workflow"]
|
||||||
path = external/hibou-oca/stock-logistics-workflow
|
path = external/hibou-oca/stock-logistics-workflow
|
||||||
url = https://github.com/hibou-io/oca-stock-logistics-workflow.git
|
url = https://github.com/hibou-io/oca-stock-logistics-workflow.git
|
||||||
|
[submodule "external/hibou-oca/stock-logistics-warehouse"]
|
||||||
|
path = external/hibou-oca/stock-logistics-warehouse
|
||||||
|
url = https://github.com/hibou-io/stock-logistics-warehouse.git
|
||||||
|
|||||||
28
account_invoice_margin/README.rst
Normal file
28
account_invoice_margin/README.rst
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
******************************
|
||||||
|
Hibou - Account Invoice Margin
|
||||||
|
******************************
|
||||||
|
|
||||||
|
Include a margin calculation on invoices.
|
||||||
|
|
||||||
|
For more information and add-ons, visit `Hibou.io <https://hibou.io/docs/hibou-odoo-suite-1/invoice-margin-156>`_.
|
||||||
|
|
||||||
|
|
||||||
|
=============
|
||||||
|
Main Features
|
||||||
|
=============
|
||||||
|
|
||||||
|
* Adds computed field `margin` to invoices to give the profitability by calculating the difference between the Unit Price and the Cost.
|
||||||
|
|
||||||
|
.. image:: https://user-images.githubusercontent.com/15882954/45578631-880c0000-b837-11e8-9c4d-d2f15c3c0592.png
|
||||||
|
:alt: 'Customer Invoice'
|
||||||
|
:width: 988
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
|
||||||
|
=======
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
|
||||||
|
|
||||||
|
Copyright Hibou Corp. 2018
|
||||||
1
delivery_hibou/__init__.py
Normal file
1
delivery_hibou/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import models
|
||||||
24
delivery_hibou/__manifest__.py
Normal file
24
delivery_hibou/__manifest__.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
'name': 'Delivery Hibou',
|
||||||
|
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
||||||
|
'version': '11.0.1.0.0',
|
||||||
|
'author': "Hibou Corp.",
|
||||||
|
'category': 'Stock',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'images': [],
|
||||||
|
'website': "https://hibou.io",
|
||||||
|
'description': """
|
||||||
|
This is a collection of "typical" carrier needs, and a bridge into Hibou modules like `delivery_partner` and `sale_planner`.
|
||||||
|
""",
|
||||||
|
'depends': [
|
||||||
|
'delivery',
|
||||||
|
'delivery_partner',
|
||||||
|
],
|
||||||
|
'demo': [],
|
||||||
|
'data': [
|
||||||
|
'views/delivery_views.xml',
|
||||||
|
'views/stock_views.xml',
|
||||||
|
],
|
||||||
|
'auto_install': False,
|
||||||
|
'installable': True,
|
||||||
|
}
|
||||||
2
delivery_hibou/models/__init__.py
Normal file
2
delivery_hibou/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import delivery
|
||||||
|
from . import stock
|
||||||
152
delivery_hibou/models/delivery.py
Normal file
152
delivery_hibou/models/delivery.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryCarrier(models.Model):
|
||||||
|
_inherit = 'delivery.carrier'
|
||||||
|
|
||||||
|
automatic_insurance_value = fields.Float(string='Automatic Insurance Value',
|
||||||
|
help='Will be used during shipping to determine if the '
|
||||||
|
'picking\'s value warrants insurance being added.')
|
||||||
|
procurement_priority = fields.Selection(PROCUREMENT_PRIORITIES,
|
||||||
|
string='Procurement Priority',
|
||||||
|
help='Priority for this carrier. Will affect pickings '
|
||||||
|
'and procurements related to this carrier.')
|
||||||
|
|
||||||
|
def get_insurance_value(self, order=None, picking=None):
|
||||||
|
value = 0.0
|
||||||
|
if order:
|
||||||
|
if order.order_line:
|
||||||
|
value = sum(order.order_line.filtered(lambda l: l.type != 'service').mapped('price_subtotal'))
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
if picking:
|
||||||
|
value = picking.declared_value()
|
||||||
|
if picking.require_insurance == 'no':
|
||||||
|
value = 0.0
|
||||||
|
elif picking.require_insurance == 'auto' and self.automatic_insurance_value and self.automatic_insurance_value > value:
|
||||||
|
value = 0.0
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_third_party_account(self, order=None, picking=None):
|
||||||
|
if order and order.shipping_account_id:
|
||||||
|
return order.shipping_account_id
|
||||||
|
if picking and picking.shipping_account_id:
|
||||||
|
return picking.shipping_account_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_order_name(self, order=None, picking=None):
|
||||||
|
if order:
|
||||||
|
return order.name
|
||||||
|
if picking:
|
||||||
|
if picking.sale_id:
|
||||||
|
return picking.sale_id.name # + ' - ' + picking.name
|
||||||
|
return picking.name
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def get_attn(self, order=None, picking=None):
|
||||||
|
if order:
|
||||||
|
return order.client_order_ref
|
||||||
|
if picking and picking.sale_id:
|
||||||
|
return picking.sale_id.client_order_ref
|
||||||
|
# If Picking has a reference, decide what it is.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _classify_picking(self, picking):
|
||||||
|
if picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'supplier' and picking.location_dest_id.usage == 'customer':
|
||||||
|
return 'dropship'
|
||||||
|
elif picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'customer' and picking.location_dest_id.usage == 'supplier':
|
||||||
|
return 'dropship_in'
|
||||||
|
elif picking.picking_type_id.code == 'incoming':
|
||||||
|
return 'in'
|
||||||
|
return 'out'
|
||||||
|
|
||||||
|
# Shipper Company
|
||||||
|
|
||||||
|
def get_shipper_company(self, order=None, picking=None):
|
||||||
|
"""
|
||||||
|
Shipper Company: The `res.partner` that provides the name of where the shipment is coming from.
|
||||||
|
"""
|
||||||
|
if order:
|
||||||
|
return order.company_id.partner_id
|
||||||
|
if picking:
|
||||||
|
return getattr(self, ('_get_shipper_company_%s' % (self._classify_picking(picking),)),
|
||||||
|
self._get_shipper_company_out)(picking)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_shipper_company_dropship(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_company_dropship_in(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_company_in(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_company_out(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
# Shipper Warehouse
|
||||||
|
|
||||||
|
def get_shipper_warehouse(self, order=None, picking=None):
|
||||||
|
"""
|
||||||
|
Shipper Warehouse: The `res.partner` that is basically the physical address a shipment is coming from.
|
||||||
|
"""
|
||||||
|
if order:
|
||||||
|
return order.warehouse_id.partner_id
|
||||||
|
if picking:
|
||||||
|
return getattr(self, ('_get_shipper_warehouse_%s' % (self._classify_picking(picking),)),
|
||||||
|
self._get_shipper_warehouse_out)(picking)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_shipper_warehouse_dropship(self, picking):
|
||||||
|
return picking.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_warehouse_dropship_in(self, picking):
|
||||||
|
if picking.sale_id:
|
||||||
|
picking.sale_id.partner_shipping_id
|
||||||
|
return self._get_shipper_warehouse_dropship_in_no_sale(picking)
|
||||||
|
|
||||||
|
def _get_shipper_warehouse_dropship_in_no_sale(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_warehouse_in(self, picking):
|
||||||
|
return picking.partner_id
|
||||||
|
|
||||||
|
def _get_shipper_warehouse_out(self, picking):
|
||||||
|
return picking.picking_type_id.warehouse_id.partner_id
|
||||||
|
|
||||||
|
# Recipient
|
||||||
|
|
||||||
|
def get_recipient(self, order=None, picking=None):
|
||||||
|
"""
|
||||||
|
Recipient: The `res.partner` receiving the shipment.
|
||||||
|
"""
|
||||||
|
if order:
|
||||||
|
return order.partner_shipping_id
|
||||||
|
if picking:
|
||||||
|
return getattr(self, ('_get_recipient_%s' % (self._classify_picking(picking),)),
|
||||||
|
self._get_recipient_out)(picking)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_recipient_dropship(self, picking):
|
||||||
|
if picking.sale_id:
|
||||||
|
return picking.sale_id.partner_shipping_id
|
||||||
|
return picking.sale_id.partner_shipping_id
|
||||||
|
|
||||||
|
def _get_recipient_dropship_no_sale(self, picking):
|
||||||
|
return picking.company_id.partner_id
|
||||||
|
|
||||||
|
def _get_recipient_dropship_in(self, picking):
|
||||||
|
return picking.picking_type_id.warehouse_id.partner_id
|
||||||
|
|
||||||
|
def _get_recipient_in(self, picking):
|
||||||
|
return picking.picking_type_id.warehouse_id.partner_id
|
||||||
|
|
||||||
|
def _get_recipient_out(self, picking):
|
||||||
|
return picking.partner_id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
50
delivery_hibou/models/stock.py
Normal file
50
delivery_hibou/models/stock.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class StockPicking(models.Model):
|
||||||
|
_inherit = 'stock.picking'
|
||||||
|
|
||||||
|
shipping_account_id = fields.Many2one('partner.shipping.account', string='Shipping Account')
|
||||||
|
require_insurance = fields.Selection([
|
||||||
|
('auto', 'Automatic'),
|
||||||
|
('yes', 'Yes'),
|
||||||
|
('no', 'No'),
|
||||||
|
], string='Require Insurance', default='auto',
|
||||||
|
help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.')
|
||||||
|
|
||||||
|
@api.one
|
||||||
|
@api.depends('move_lines.priority', 'carrier_id')
|
||||||
|
def _compute_priority(self):
|
||||||
|
if self.carrier_id.procurement_priority:
|
||||||
|
self.priority = self.carrier_id.procurement_priority
|
||||||
|
else:
|
||||||
|
super(StockPicking, self)._compute_priority()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, values):
|
||||||
|
origin = values.get('origin')
|
||||||
|
if origin and not values.get('shipping_account_id'):
|
||||||
|
so = self.env['sale.order'].search([('name', '=', str(origin))], limit=1)
|
||||||
|
if so and so.shipping_account_id:
|
||||||
|
values['shipping_account_id'] = so.shipping_account_id.id
|
||||||
|
|
||||||
|
res = super(StockPicking, self).create(values)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def declared_value(self):
|
||||||
|
self.ensure_one()
|
||||||
|
cost = sum([(l.product_id.standard_price * l.qty_done) for l in self.move_line_ids] or [0.0])
|
||||||
|
if not cost:
|
||||||
|
# Assume Full Value
|
||||||
|
cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0])
|
||||||
|
return cost
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class StockMove(models.Model):
|
||||||
|
_inherit = 'stock.move'
|
||||||
|
|
||||||
|
def _prepare_procurement_values(self):
|
||||||
|
res = super(StockMove, self)._prepare_procurement_values()
|
||||||
|
res['priority'] = self.picking_id.priority or self.priority
|
||||||
|
return res
|
||||||
1
delivery_hibou/tests/__init__.py
Normal file
1
delivery_hibou/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from . import test_delivery_hibou
|
||||||
160
delivery_hibou/tests/test_delivery_hibou.py
Normal file
160
delivery_hibou/tests/test_delivery_hibou.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
from odoo.tests import common
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeliveryHibou(common.TransactionCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestDeliveryHibou, self).setUp()
|
||||||
|
self.partner = self.env.ref('base.res_partner_address_13')
|
||||||
|
self.product = self.env.ref('product.product_product_7')
|
||||||
|
# Create Shipping Account
|
||||||
|
self.shipping_account = self.env['partner.shipping.account'].create({
|
||||||
|
'name': '123123',
|
||||||
|
'delivery_type': 'other',
|
||||||
|
})
|
||||||
|
# Create Carrier
|
||||||
|
self.delivery_product = self.env['product.product'].create({
|
||||||
|
'name': 'Test Carrier1 Delivery',
|
||||||
|
'type': 'service',
|
||||||
|
})
|
||||||
|
self.carrier = self.env['delivery.carrier'].create({
|
||||||
|
'name': 'Test Carrier1',
|
||||||
|
'product_id': self.delivery_product.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_delivery_hibou(self):
|
||||||
|
# Assign a new shipping account
|
||||||
|
self.partner.shipping_account_id = self.shipping_account
|
||||||
|
|
||||||
|
# Assign values to new Carrier
|
||||||
|
test_insurance_value = 600
|
||||||
|
test_procurement_priority = '2'
|
||||||
|
self.carrier.automatic_insurance_value = test_insurance_value
|
||||||
|
self.carrier.procurement_priority = test_procurement_priority
|
||||||
|
|
||||||
|
|
||||||
|
sale_order = self.env['sale.order'].create({
|
||||||
|
'partner_id': self.partner.id,
|
||||||
|
'partner_shipping_id': self.partner.id,
|
||||||
|
'partner_invoice_id': self.partner.id,
|
||||||
|
'carrier_id': self.carrier.id,
|
||||||
|
'shipping_account_id': self.shipping_account.id,
|
||||||
|
'order_line': [(0, 0, {
|
||||||
|
'product_id': self.product.id,
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
sale_order.get_delivery_price()
|
||||||
|
sale_order.set_delivery_line()
|
||||||
|
sale_order.action_confirm()
|
||||||
|
# Make sure 3rd party Shipping Account is set.
|
||||||
|
self.assertEqual(sale_order.shipping_account_id, self.shipping_account)
|
||||||
|
|
||||||
|
self.assertTrue(sale_order.picking_ids)
|
||||||
|
# Priority coming from Carrier procurement_priority
|
||||||
|
self.assertEqual(sale_order.picking_ids.priority, test_procurement_priority)
|
||||||
|
# 3rd party Shipping Account copied from Sale Order
|
||||||
|
self.assertEqual(sale_order.picking_ids.shipping_account_id, self.shipping_account)
|
||||||
|
self.assertEqual(sale_order.carrier_id.get_third_party_account(order=sale_order), self.shipping_account)
|
||||||
|
|
||||||
|
# Test attn
|
||||||
|
test_ref = 'TEST100'
|
||||||
|
self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), False)
|
||||||
|
sale_order.client_order_ref = test_ref
|
||||||
|
self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), test_ref)
|
||||||
|
# The picking should get this ref as well
|
||||||
|
self.assertEqual(sale_order.picking_ids.carrier_id.get_attn(picking=sale_order.picking_ids), test_ref)
|
||||||
|
|
||||||
|
# Test order_name
|
||||||
|
self.assertEqual(sale_order.carrier_id.get_order_name(order=sale_order), sale_order.name)
|
||||||
|
# The picking should get the same 'order_name'
|
||||||
|
self.assertEqual(sale_order.picking_ids.carrier_id.get_order_name(picking=sale_order.picking_ids), sale_order.name)
|
||||||
|
|
||||||
|
def test_carrier_hibou_out(self):
|
||||||
|
test_insurance_value = 4000
|
||||||
|
self.carrier.automatic_insurance_value = test_insurance_value
|
||||||
|
|
||||||
|
picking_out = self.env.ref('stock.outgoing_shipment_main_warehouse')
|
||||||
|
self.assertEqual(picking_out.state, 'assigned')
|
||||||
|
|
||||||
|
picking_out.carrier_id = self.carrier
|
||||||
|
|
||||||
|
# This relies heavily on the 'stock' demo data.
|
||||||
|
# Should only have a single move_line_ids and it should not be done at all.
|
||||||
|
self.assertEqual(picking_out.move_line_ids.mapped('qty_done'), [0.0])
|
||||||
|
self.assertEqual(picking_out.move_line_ids.mapped('product_uom_qty'), [15.0])
|
||||||
|
self.assertEqual(picking_out.move_line_ids.mapped('product_id.standard_price'), [3300.0])
|
||||||
|
|
||||||
|
# The 'value' is assumed to be all of the product value from the initial demand.
|
||||||
|
self.assertEqual(picking_out.declared_value(), 15.0 * 3300.0)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), picking_out.declared_value())
|
||||||
|
|
||||||
|
# Workflow where user explicitly opts out of insurance on the picking level.
|
||||||
|
picking_out.require_insurance = 'no'
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
|
||||||
|
picking_out.require_insurance = 'auto'
|
||||||
|
|
||||||
|
# Lets choose to only delivery one piece at the moment.
|
||||||
|
# This does not meet the minimum on the carrier to have insurance value.
|
||||||
|
picking_out.move_line_ids.qty_done = 1.0
|
||||||
|
self.assertEqual(picking_out.declared_value(), 3300.0)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
|
||||||
|
# Workflow where user opts in to insurance.
|
||||||
|
picking_out.require_insurance = 'yes'
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 3300.0)
|
||||||
|
picking_out.require_insurance = 'auto'
|
||||||
|
|
||||||
|
# Test with picking having 3rd party account.
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), None)
|
||||||
|
picking_out.shipping_account_id = self.shipping_account
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), self.shipping_account)
|
||||||
|
|
||||||
|
# Shipment Time Methods!
|
||||||
|
self.assertEqual(picking_out.carrier_id._classify_picking(picking=picking_out), 'out')
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_shipper_company(picking=picking_out),
|
||||||
|
picking_out.company_id.partner_id)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_shipper_warehouse(picking=picking_out),
|
||||||
|
picking_out.picking_type_id.warehouse_id.partner_id)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_recipient(picking=picking_out),
|
||||||
|
picking_out.partner_id)
|
||||||
|
# This picking has no `sale_id`
|
||||||
|
# Right now ATTN requires a sale_id, which this picking doesn't have (none of the stock ones do)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_attn(picking=picking_out), False)
|
||||||
|
self.assertEqual(picking_out.carrier_id.get_order_name(picking=picking_out), picking_out.name)
|
||||||
|
|
||||||
|
def test_carrier_hibou_in(self):
|
||||||
|
picking_in = self.env.ref('stock.incomming_shipment1')
|
||||||
|
self.assertEqual(picking_in.state, 'assigned')
|
||||||
|
|
||||||
|
picking_in.carrier_id = self.carrier
|
||||||
|
# This relies heavily on the 'stock' demo data.
|
||||||
|
# Should only have a single move_line_ids and it should not be done at all.
|
||||||
|
self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0])
|
||||||
|
self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0])
|
||||||
|
self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0])
|
||||||
|
|
||||||
|
self.assertEqual(picking_in.carrier_id._classify_picking(picking=picking_in), 'in')
|
||||||
|
self.assertEqual(picking_in.carrier_id.get_shipper_company(picking=picking_in),
|
||||||
|
picking_in.company_id.partner_id)
|
||||||
|
self.assertEqual(picking_in.carrier_id.get_shipper_warehouse(picking=picking_in),
|
||||||
|
picking_in.partner_id)
|
||||||
|
self.assertEqual(picking_in.carrier_id.get_recipient(picking=picking_in),
|
||||||
|
picking_in.picking_type_id.warehouse_id.partner_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
14
delivery_hibou/views/delivery_views.xml
Normal file
14
delivery_hibou/views/delivery_views.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_delivery_carrier_form" model="ir.ui.view">
|
||||||
|
<field name="name">hibou.delivery.carrier.form</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="automatic_insurance_value"/>
|
||||||
|
<field name="procurement_priority"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
16
delivery_hibou/views/stock_views.xml
Normal file
16
delivery_hibou/views/stock_views.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_picking_withcarrier_out_form" model="ir.ui.view">
|
||||||
|
<field name="name">hibou.delivery.stock.picking_withcarrier.form.view</field>
|
||||||
|
<field name="model">stock.picking</field>
|
||||||
|
<field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='carrier_id']" position="before">
|
||||||
|
<field name="require_insurance" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||||
|
<field name="shipping_account_id" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -36,6 +36,7 @@ class ProductPackaging(models.Model):
|
|||||||
_inherit = 'product.packaging'
|
_inherit = 'product.packaging'
|
||||||
|
|
||||||
package_carrier_type = fields.Selection(selection_add=[('stamps', 'Stamps.com')])
|
package_carrier_type = fields.Selection(selection_add=[('stamps', 'Stamps.com')])
|
||||||
|
stamps_cubic_pricing = fields.Boolean(string="Stamps.com Use Cubic Pricing")
|
||||||
|
|
||||||
|
|
||||||
class ProviderStamps(models.Model):
|
class ProviderStamps(models.Model):
|
||||||
@@ -72,6 +73,18 @@ class ProviderStamps(models.Model):
|
|||||||
return self.stamps_default_packaging_id.shipper_package_code
|
return self.stamps_default_packaging_id.shipper_package_code
|
||||||
return package.packaging_id.shipper_package_code if package.packaging_id.shipper_package_code in STAMPS_PACKAGE_TYPES else 'Package'
|
return package.packaging_id.shipper_package_code if package.packaging_id.shipper_package_code in STAMPS_PACKAGE_TYPES else 'Package'
|
||||||
|
|
||||||
|
def _stamps_package_is_cubic_pricing(self, package=None):
|
||||||
|
if not package:
|
||||||
|
return self.stamps_default_packaging_id.stamps_cubic_pricing
|
||||||
|
return package.packaging_id.stamps_cubic_pricing
|
||||||
|
|
||||||
|
def _stamps_package_dimensions(self, package=None):
|
||||||
|
if not package:
|
||||||
|
package_type = self.stamps_default_packaging_id
|
||||||
|
else:
|
||||||
|
package_type = package.packaging_id
|
||||||
|
return package_type.length, package_type.width, package_type.height
|
||||||
|
|
||||||
def _get_stamps_service(self):
|
def _get_stamps_service(self):
|
||||||
sudoself = self.sudo()
|
sudoself = self.sudo()
|
||||||
config = StampsConfiguration(integration_id=sudoself.stamps_integration_id,
|
config = StampsConfiguration(integration_id=sudoself.stamps_integration_id,
|
||||||
@@ -136,23 +149,33 @@ class ProviderStamps(models.Model):
|
|||||||
|
|
||||||
for package in picking.package_ids:
|
for package in picking.package_ids:
|
||||||
weight = self._stamps_convert_weight(package.shipping_weight)
|
weight = self._stamps_convert_weight(package.shipping_weight)
|
||||||
|
l, w, h = self._stamps_package_dimensions(package=package)
|
||||||
|
|
||||||
ret_val = service.create_shipping()
|
ret_val = service.create_shipping()
|
||||||
ret_val.ShipDate = date.today().isoformat()
|
ret_val.ShipDate = date.today().isoformat()
|
||||||
ret_val.FromZIPCode = from_partner.zip
|
ret_val.FromZIPCode = from_partner.zip
|
||||||
ret_val.ToZIPCode = to_partner.zip
|
ret_val.ToZIPCode = to_partner.zip
|
||||||
ret_val.PackageType = self._stamps_package_type(package=package)
|
ret_val.PackageType = self._stamps_package_type(package=package)
|
||||||
|
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing(package=package)
|
||||||
|
ret_val.Length = l
|
||||||
|
ret_val.Width = w
|
||||||
|
ret_val.Height = h
|
||||||
ret_val.ServiceType = self.stamps_service_type
|
ret_val.ServiceType = self.stamps_service_type
|
||||||
ret_val.WeightLb = weight
|
ret_val.WeightLb = weight
|
||||||
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
ret.append((package.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
||||||
if not ret:
|
if not ret:
|
||||||
weight = self._stamps_convert_weight(picking.shipping_weight)
|
weight = self._stamps_convert_weight(picking.shipping_weight)
|
||||||
|
l, w, h = self._stamps_package_dimensions()
|
||||||
|
|
||||||
ret_val = service.create_shipping()
|
ret_val = service.create_shipping()
|
||||||
ret_val.ShipDate = date.today().isoformat()
|
ret_val.ShipDate = date.today().isoformat()
|
||||||
ret_val.FromZIPCode = from_partner.zip
|
ret_val.FromZIPCode = from_partner.zip
|
||||||
ret_val.ToZIPCode = to_partner.zip
|
ret_val.ToZIPCode = to_partner.zip
|
||||||
ret_val.PackageType = self._stamps_package_type()
|
ret_val.PackageType = self._stamps_package_type()
|
||||||
|
ret_val.CubicPricing = self._stamps_package_is_cubic_pricing()
|
||||||
|
ret_val.Length = l
|
||||||
|
ret_val.Width = w
|
||||||
|
ret_val.Height = h
|
||||||
ret_val.ServiceType = self.stamps_service_type
|
ret_val.ServiceType = self.stamps_service_type
|
||||||
ret_val.WeightLb = weight
|
ret_val.WeightLb = weight
|
||||||
ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
ret.append((picking.name + ret_val.ShipDate + str(ret_val.WeightLb), ret_val))
|
||||||
@@ -168,11 +191,6 @@ class ProviderStamps(models.Model):
|
|||||||
service = self._get_stamps_service()
|
service = self._get_stamps_service()
|
||||||
|
|
||||||
for order in orders:
|
for order in orders:
|
||||||
# has product with usps_exclude
|
|
||||||
if sum(1 for l in order.order_line if l.product_id.usps_exclude):
|
|
||||||
res.append(None)
|
|
||||||
continue
|
|
||||||
|
|
||||||
shipping = self._get_stamps_shipping_for_order(service, order, date_planned)
|
shipping = self._get_stamps_shipping_for_order(service, order, date_planned)
|
||||||
rates = service.get_rates(shipping)
|
rates = service.get_rates(shipping)
|
||||||
if rates and len(rates) >= 1:
|
if rates and len(rates) >= 1:
|
||||||
@@ -198,6 +216,30 @@ class ProviderStamps(models.Model):
|
|||||||
res = res + [(0.0, 0, None)]
|
res = res + [(0.0, 0, None)]
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def stamps_rate_shipment(self, order):
|
||||||
|
self.ensure_one()
|
||||||
|
result = {
|
||||||
|
'success': False,
|
||||||
|
'price': 0.0,
|
||||||
|
'error_message': 'Error Retrieving Response from Stamps.com',
|
||||||
|
'warning_message': False
|
||||||
|
}
|
||||||
|
date_planned = None
|
||||||
|
if self.env.context.get('date_planned'):
|
||||||
|
date_planned = self.env.context.get('date_planned')
|
||||||
|
rate = self.stamps_get_shipping_price_for_plan(order, date_planned)
|
||||||
|
if rate:
|
||||||
|
price, transit_time, date_delivered = rate[0]
|
||||||
|
result.update({
|
||||||
|
'success': True,
|
||||||
|
'price': price,
|
||||||
|
'error_message': False,
|
||||||
|
'transit_time': transit_time,
|
||||||
|
'date_delivered': date_delivered,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
return result
|
||||||
|
|
||||||
def stamps_send_shipping(self, pickings):
|
def stamps_send_shipping(self, pickings):
|
||||||
res = []
|
res = []
|
||||||
service = self._get_stamps_service()
|
service = self._get_stamps_service()
|
||||||
|
|||||||
@@ -25,4 +25,15 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="product_packaging_delivery_form" model="ir.ui.view">
|
||||||
|
<field name="name">stamps.product.packaging.form.delivery</field>
|
||||||
|
<field name="model">product.packaging</field>
|
||||||
|
<field name="inherit_id" ref="delivery.product_packaging_delivery_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='max_weight']" position='after'>
|
||||||
|
<field name="stamps_cubic_pricing"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|||||||
1
external/hibou-oca/stock-logistics-warehouse
vendored
Submodule
1
external/hibou-oca/stock-logistics-warehouse
vendored
Submodule
Submodule external/hibou-oca/stock-logistics-warehouse added at eb04e0ba5f
@@ -39,10 +39,11 @@ class HrPayrollRegisterPaymentWizard(models.TransientModel):
|
|||||||
context = dict(self._context or {})
|
context = dict(self._context or {})
|
||||||
active_ids = context.get('active_ids', [])
|
active_ids = context.get('active_ids', [])
|
||||||
payslip = self.env['hr.payslip'].browse(active_ids)
|
payslip = self.env['hr.payslip'].browse(active_ids)
|
||||||
amount = 0.0
|
amount = -sum(payslip.move_id.line_ids.filtered(lambda l: (
|
||||||
for line in payslip.move_id.line_ids:
|
l.account_id.internal_type == 'payable'
|
||||||
if line.account_id.internal_type == 'payable' and line.partner_id.id == payslip.employee_id.address_home_id.id:
|
and l.partner_id.id == payslip.employee_id.address_home_id.id
|
||||||
amount += abs(line.balance)
|
and not l.reconciled)
|
||||||
|
).mapped('balance'))
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
@@ -142,7 +143,7 @@ class HrPayrollRegisterPaymentWizard(models.TransientModel):
|
|||||||
if line.account_id.internal_type == 'payable':
|
if line.account_id.internal_type == 'payable':
|
||||||
account_move_lines_to_reconcile |= line
|
account_move_lines_to_reconcile |= line
|
||||||
for line in payslip.move_id.line_ids:
|
for line in payslip.move_id.line_ids:
|
||||||
if line.account_id.internal_type == 'payable' and line.partner_id.id == self.partner_id.id:
|
if line.account_id.internal_type == 'payable' and line.partner_id.id == self.partner_id.id and not line.reconciled:
|
||||||
account_move_lines_to_reconcile |= line
|
account_move_lines_to_reconcile |= line
|
||||||
|
|
||||||
account_move_lines_to_reconcile.reconcile()
|
account_move_lines_to_reconcile.reconcile()
|
||||||
|
|||||||
@@ -9,5 +9,18 @@ class StockPicking(models.Model):
|
|||||||
def send_to_shipper(self):
|
def send_to_shipper(self):
|
||||||
res = False
|
res = False
|
||||||
for pick in self.filtered(lambda p: not p.carrier_tracking_ref):
|
for pick in self.filtered(lambda p: not p.carrier_tracking_ref):
|
||||||
|
# deliver full order if no items are done.
|
||||||
|
pick_has_no_done = sum(pick.move_line_ids.mapped('qty_done')) == 0
|
||||||
|
if pick_has_no_done:
|
||||||
|
pick._rma_complete()
|
||||||
res = super(StockPicking, pick).send_to_shipper()
|
res = super(StockPicking, pick).send_to_shipper()
|
||||||
|
if pick_has_no_done:
|
||||||
|
pick._rma_complete_reverse()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def _rma_complete(self):
|
||||||
|
for line in self.move_line_ids:
|
||||||
|
line.qty_done = line.product_uom_qty
|
||||||
|
|
||||||
|
def _rma_complete_reverse(self):
|
||||||
|
self.move_line_ids.write({'qty_done': 0.0})
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
|
|||||||
'base_geolocalize',
|
'base_geolocalize',
|
||||||
'delivery',
|
'delivery',
|
||||||
'resource',
|
'resource',
|
||||||
|
'stock',
|
||||||
],
|
],
|
||||||
'demo': [],
|
'demo': [],
|
||||||
'data': [
|
'data': [
|
||||||
@@ -35,6 +36,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?)
|
|||||||
'views/sale.xml',
|
'views/sale.xml',
|
||||||
'views/stock.xml',
|
'views/stock.xml',
|
||||||
'views/delivery.xml',
|
'views/delivery.xml',
|
||||||
|
'views/product.xml',
|
||||||
],
|
],
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
'installable': True,
|
'installable': True,
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
from . import sale
|
from . import sale
|
||||||
from . import stock
|
from . import stock
|
||||||
from . import delivery
|
from . import delivery
|
||||||
|
from . import product
|
||||||
|
from . import planning
|
||||||
|
from . import partner
|
||||||
|
|||||||
@@ -25,6 +25,36 @@ class DeliveryCarrier(models.Model):
|
|||||||
if hasattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type):
|
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)
|
return getattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type)(orders, date_planned)
|
||||||
|
|
||||||
|
def rate_shipment_date_planned(self, order, date_planned=None):
|
||||||
|
"""
|
||||||
|
For every sale order, compute the price of the shipment and potentially when it will arrive.
|
||||||
|
:param order: `sale.order`
|
||||||
|
:param date_planned: The date the shipment is expected to leave/be picked up by carrier.
|
||||||
|
:return: rate in the same form the normal `rate_shipment` method does BUT
|
||||||
|
-- Additional keys
|
||||||
|
- transit_days: int
|
||||||
|
- date_delivered: string
|
||||||
|
"""
|
||||||
|
self.ensure_one()
|
||||||
|
if hasattr(self, '%s_rate_shipment_date_planned' % self.delivery_type):
|
||||||
|
# New API Odoo 11 - Carrier specific override.
|
||||||
|
return getattr(self, '%s_rate_shipment_date_planned' % self.delivery_type)(order, date_planned)
|
||||||
|
|
||||||
|
rate = self.with_context(date_planned=date_planned).rate_shipment(order)
|
||||||
|
if rate and date_planned:
|
||||||
|
if rate.get('date_delivered'):
|
||||||
|
date_delivered = rate['date_delivered']
|
||||||
|
transit_days = self.calculate_transit_days(date_planned, date_delivered)
|
||||||
|
if not rate.get('transit_days') or transit_days < rate.get('transit_days'):
|
||||||
|
rate['transit_days'] = transit_days
|
||||||
|
elif rate.get('transit_days'):
|
||||||
|
rate['date_delivered'] = self.calculate_date_delivered(date_planned, rate.get('transit_days'))
|
||||||
|
elif rate:
|
||||||
|
if rate.get('date_delivered'):
|
||||||
|
rate.pop('date_delivered')
|
||||||
|
|
||||||
|
return rate
|
||||||
|
|
||||||
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 isinstance(date_planned, str):
|
if isinstance(date_planned, str):
|
||||||
@@ -36,11 +66,14 @@ class DeliveryCarrier(models.Model):
|
|||||||
while date_planned < date_delivered:
|
while date_planned < date_delivered:
|
||||||
if transit_days > 10:
|
if transit_days > 10:
|
||||||
break
|
break
|
||||||
interval = self.delivery_calendar_id.schedule_days(1, date_planned, compute_leaves=True)
|
|
||||||
|
|
||||||
if not interval:
|
current_date_planned = self.delivery_calendar_id.plan_days(1, date_planned, compute_leaves=True)
|
||||||
|
if not current_date_planned:
|
||||||
return self._calculate_transit_days_naive(date_planned, date_delivered)
|
return self._calculate_transit_days_naive(date_planned, date_delivered)
|
||||||
date_planned = interval[0][1]
|
if current_date_planned == date_planned:
|
||||||
|
date_planned += timedelta(days=1)
|
||||||
|
else:
|
||||||
|
date_planned = current_date_planned
|
||||||
transit_days += 1
|
transit_days += 1
|
||||||
|
|
||||||
if transit_days > 1:
|
if transit_days > 1:
|
||||||
@@ -59,11 +92,11 @@ class DeliveryCarrier(models.Model):
|
|||||||
# date calculations needs an extra day
|
# date calculations needs an extra day
|
||||||
effective_transit_days = transit_days + 1
|
effective_transit_days = transit_days + 1
|
||||||
|
|
||||||
interval = self.delivery_calendar_id.schedule_days(effective_transit_days, date_planned, compute_leaves=True)
|
last_day = self.delivery_calendar_id.plan_days(effective_transit_days, date_planned, compute_leaves=True)
|
||||||
if not interval:
|
if not last_day:
|
||||||
return self._calculate_date_delivered_naive(date_planned, transit_days)
|
return self._calculate_date_delivered_naive(date_planned, transit_days)
|
||||||
|
|
||||||
return fields.Datetime.to_string(interval[-1][1])
|
return fields.Datetime.to_string(last_day)
|
||||||
|
|
||||||
def _calculate_date_delivered_naive(self, date_planned, transit_days):
|
def _calculate_date_delivered_naive(self, date_planned, transit_days):
|
||||||
return fields.Datetime.to_string(date_planned + timedelta(days=transit_days))
|
return fields.Datetime.to_string(date_planned + timedelta(days=transit_days))
|
||||||
|
|||||||
29
sale_planner/models/partner.py
Normal file
29
sale_planner/models/partner.py
Normal 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
|
||||||
16
sale_planner/models/planning.py
Normal file
16
sale_planner/models/planning.py
Normal 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')
|
||||||
22
sale_planner/models/product.py
Normal file
22
sale_planner/models/product.py
Normal 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.")
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
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 logging import getLogger
|
||||||
|
|
||||||
|
_logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TestPlanner(common.TransactionCase):
|
class TestPlanner(common.TransactionCase):
|
||||||
@@ -69,13 +73,13 @@ class TestPlanner(common.TransactionCase):
|
|||||||
self.warehouse_1 = self.env['stock.warehouse'].create({
|
self.warehouse_1 = self.env['stock.warehouse'].create({
|
||||||
'name': 'Washington Warehouse',
|
'name': 'Washington Warehouse',
|
||||||
'partner_id': self.warehouse_partner_1.id,
|
'partner_id': self.warehouse_partner_1.id,
|
||||||
'code': 'WH1',
|
'code': 'TWH1',
|
||||||
'shipping_calendar_id': self.warehouse_calendar_1.id,
|
'shipping_calendar_id': self.warehouse_calendar_1.id,
|
||||||
})
|
})
|
||||||
self.warehouse_2 = self.env['stock.warehouse'].create({
|
self.warehouse_2 = self.env['stock.warehouse'].create({
|
||||||
'name': 'Colorado Warehouse',
|
'name': 'Colorado Warehouse',
|
||||||
'partner_id': self.warehouse_partner_2.id,
|
'partner_id': self.warehouse_partner_2.id,
|
||||||
'code': 'WH2',
|
'code': 'TWH2',
|
||||||
'shipping_calendar_id': self.warehouse_calendar_2.id,
|
'shipping_calendar_id': self.warehouse_calendar_2.id,
|
||||||
})
|
})
|
||||||
self.so = self.env['sale.order'].create({
|
self.so = self.env['sale.order'].create({
|
||||||
@@ -87,13 +91,25 @@ class TestPlanner(common.TransactionCase):
|
|||||||
'type': 'product',
|
'type': 'product',
|
||||||
'standard_price': 1.0,
|
'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_1 = self.product_1.product_variant_id
|
||||||
|
self.product_12 = self.product_12.product_variant_id
|
||||||
self.product_2 = self.env['product.template'].create({
|
self.product_2 = self.env['product.template'].create({
|
||||||
'name': 'Product for WH2',
|
'name': 'Product for WH2',
|
||||||
'type': 'product',
|
'type': 'product',
|
||||||
'standard_price': 1.0,
|
'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_2 = self.product_2.product_variant_id
|
||||||
|
self.product_22 = self.product_22.product_variant_id
|
||||||
self.product_both = self.env['product.template'].create({
|
self.product_both = self.env['product.template'].create({
|
||||||
'name': 'Product for Both',
|
'name': 'Product for Both',
|
||||||
'type': 'product',
|
'type': 'product',
|
||||||
@@ -105,6 +121,11 @@ class TestPlanner(common.TransactionCase):
|
|||||||
'product_id': self.product_1.id,
|
'product_id': self.product_1.id,
|
||||||
'new_quantity': 100,
|
'new_quantity': 100,
|
||||||
}).change_product_qty()
|
}).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({
|
self.env['stock.change.product.qty'].create({
|
||||||
'location_id': self.warehouse_1.lot_stock_id.id,
|
'location_id': self.warehouse_1.lot_stock_id.id,
|
||||||
'product_id': self.product_both.id,
|
'product_id': self.product_both.id,
|
||||||
@@ -115,16 +136,45 @@ class TestPlanner(common.TransactionCase):
|
|||||||
'product_id': self.product_2.id,
|
'product_id': self.product_2.id,
|
||||||
'new_quantity': 100,
|
'new_quantity': 100,
|
||||||
}).change_product_qty()
|
}).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({
|
self.env['stock.change.product.qty'].create({
|
||||||
'location_id': self.warehouse_2.lot_stock_id.id,
|
'location_id': self.warehouse_2.lot_stock_id.id,
|
||||||
'product_id': self.product_both.id,
|
'product_id': self.product_both.id,
|
||||||
'new_quantity': 100,
|
'new_quantity': 100,
|
||||||
}).change_product_qty()
|
}).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):
|
def both_wh_ids(self):
|
||||||
return [self.warehouse_1.id, self.warehouse_2.id]
|
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({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_1.id,
|
'product_id': self.product_1.id,
|
||||||
@@ -138,7 +188,11 @@ class TestPlanner(common.TransactionCase):
|
|||||||
self.assertTrue(base_option, 'Must have base option.')
|
self.assertTrue(base_option, 'Must have base option.')
|
||||||
self.assertEqual(self.warehouse_1.id, base_option['warehouse_id'])
|
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({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_1.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.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1)
|
||||||
self.assertFalse(planner.planning_option_ids[0].sub_options)
|
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({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_2.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.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2)
|
||||||
self.assertFalse(planner.planning_option_ids[0].sub_options)
|
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({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_1.id,
|
'product_id': self.product_1.id,
|
||||||
@@ -175,7 +237,7 @@ class TestPlanner(common.TransactionCase):
|
|||||||
self.env['sale.order.line'].create({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_2.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_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)
|
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, 'Must have one or more plans.')
|
||||||
self.assertTrue(planner.planning_option_ids[0].sub_options)
|
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({
|
self.env['sale.order.line'].create({
|
||||||
'order_id': self.so.id,
|
'order_id': self.so.id,
|
||||||
'product_id': self.product_both.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.assertTrue(planner.planning_option_ids, 'Must have one or more plans.')
|
||||||
self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2)
|
self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2)
|
||||||
self.assertFalse(planner.planning_option_ids[0].sub_options)
|
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_1, 'If this fails, it will probably pass next time.')
|
||||||
|
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)
|
||||||
|
|||||||
23
sale_planner/views/product.xml
Normal file
23
sale_planner/views/product.xml
Normal 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>
|
||||||
@@ -2,6 +2,7 @@ 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
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from collections import defaultdict
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
_logger = getLogger(__name__)
|
_logger = getLogger(__name__)
|
||||||
@@ -50,7 +51,6 @@ class FakePartner():
|
|||||||
self.partner_longitude = 0.0
|
self.partner_longitude = 0.0
|
||||||
self.is_company = False
|
self.is_company = False
|
||||||
for attr, value in kwargs.items():
|
for attr, value in kwargs.items():
|
||||||
_logger.warn(' ' + str(attr) + ': ' + str(value))
|
|
||||||
setattr(self, attr, value)
|
setattr(self, attr, value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -219,6 +219,8 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
planner = super(SaleOrderMakePlan, self).create(values)
|
planner = super(SaleOrderMakePlan, self).create(values)
|
||||||
|
|
||||||
for option_vals in self.generate_order_options(planner.order_id):
|
for option_vals in self.generate_order_options(planner.order_id):
|
||||||
|
if type(option_vals) != dict:
|
||||||
|
continue
|
||||||
option_vals['plan_id'] = planner.id
|
option_vals['plan_id'] = planner.id
|
||||||
planner.planning_option_ids |= self.env['sale.order.planning.option'].create(option_vals)
|
planner.planning_option_ids |= self.env['sale.order.planning.option'].create(option_vals)
|
||||||
|
|
||||||
@@ -255,59 +257,70 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
|
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def get_warehouses(self, warehouse_id=None):
|
def get_warehouses(self, warehouse_id=None, domain=None):
|
||||||
warehouse = self.env['stock.warehouse'].sudo()
|
warehouse = self.env['stock.warehouse'].sudo()
|
||||||
if warehouse_id:
|
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'):
|
if self.env.context.get('warehouse_domain'):
|
||||||
#potential bug here if this is textual
|
domain.extend(self.env.context.get('warehouse_domain'))
|
||||||
return warehouse.search(self.env.context.get('warehouse_domain'))
|
|
||||||
|
|
||||||
irconfig_parameter = self.env['ir.config_parameter'].sudo()
|
irconfig_parameter = self.env['ir.config_parameter'].sudo()
|
||||||
if irconfig_parameter.get_param('sale.order.planner.warehouse_domain'):
|
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(domain)
|
||||||
|
|
||||||
return warehouse.search([])
|
def get_shipping_carriers(self, carrier_id=None, domain=None):
|
||||||
|
|
||||||
def get_shipping_carriers(self, carrier_id=None):
|
|
||||||
Carrier = self.env['delivery.carrier'].sudo()
|
Carrier = self.env['delivery.carrier'].sudo()
|
||||||
if carrier_id:
|
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'):
|
if self.env.context.get('carrier_domain'):
|
||||||
# potential bug here if this is textual
|
# 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()
|
irconfig_parameter = self.env['ir.config_parameter'].sudo()
|
||||||
if irconfig_parameter.get_param('sale.order.planner.carrier_domain'):
|
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(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):
|
warehouses = self.get_warehouses(domain=warehouse_domain)
|
||||||
product_lines = list(filter(lambda line: line.product_id.type == 'product', order_fake.order_line))
|
if flag_force_closest:
|
||||||
if not product_lines:
|
warehouses = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id)
|
||||||
return {}
|
product_stock = self._fetch_product_stock(warehouses, policy_group['products'])
|
||||||
|
|
||||||
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 = {}
|
sub_options = {}
|
||||||
wh_date_planning = {}
|
wh_date_planning = {}
|
||||||
|
|
||||||
p_len = len(products)
|
p_len = len(policy_group['products'])
|
||||||
full_candidates = set()
|
full_candidates = set()
|
||||||
partial_candidates = set()
|
partial_candidates = set()
|
||||||
for wh_id, stock in product_stock.items():
|
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:
|
if available == p_len:
|
||||||
full_candidates.add(wh_id)
|
full_candidates.add(wh_id)
|
||||||
elif available > 0:
|
elif available > 0:
|
||||||
@@ -316,32 +329,35 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
if full_candidates:
|
if full_candidates:
|
||||||
if len(full_candidates) == 1:
|
if len(full_candidates) == 1:
|
||||||
warehouse = warehouses.filtered(lambda wh: wh.id in full_candidates)
|
warehouse = warehouses.filtered(lambda wh: wh.id in full_candidates)
|
||||||
date_planned = self._next_warehouse_shipping_date(warehouse)
|
else:
|
||||||
order_fake.warehouse_id = warehouse
|
|
||||||
return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
|
|
||||||
|
|
||||||
warehouse = self._find_closest_warehouse_by_partner(
|
warehouse = self._find_closest_warehouse_by_partner(
|
||||||
warehouses.filtered(lambda wh: wh.id in full_candidates), order_fake.partner_shipping_id)
|
warehouses.filtered(lambda wh: wh.id in full_candidates), order_fake.partner_shipping_id)
|
||||||
date_planned = self._next_warehouse_shipping_date(warehouse)
|
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}
|
return {'warehouse_id': warehouse.id, 'date_planned': date_planned}
|
||||||
|
|
||||||
|
_logger.error(' partial_candidates: ' + str(partial_candidates))
|
||||||
if partial_candidates:
|
if partial_candidates:
|
||||||
|
_logger.error(' using...')
|
||||||
if len(partial_candidates) == 1:
|
if len(partial_candidates) == 1:
|
||||||
warehouse = warehouses.filtered(lambda wh: wh.id in partial_candidates)
|
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}
|
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 = sorted_warehouses[0] # partial_candidates means there is at least one warehouse
|
||||||
primary_wh_date_planned = self._next_warehouse_shipping_date(primary_wh)
|
primary_wh_date_planned = self._next_warehouse_shipping_date(primary_wh)
|
||||||
wh_date_planning[primary_wh.id] = primary_wh_date_planned
|
wh_date_planning[primary_wh.id] = primary_wh_date_planned
|
||||||
for wh in sorted_warehouses:
|
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
|
continue
|
||||||
stock = product_stock[wh.id]
|
stock = product_stock[wh.id]
|
||||||
for p_id, p_vals in stock.items():
|
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:
|
if wh.id not in sub_options:
|
||||||
sub_options[wh.id] = {
|
sub_options[wh.id] = {
|
||||||
'date_planned': self._next_warehouse_shipping_date(wh),
|
'date_planned': self._next_warehouse_shipping_date(wh),
|
||||||
@@ -350,23 +366,211 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
}
|
}
|
||||||
sub_options[wh.id]['product_ids'].append(p_id)
|
sub_options[wh.id]['product_ids'].append(p_id)
|
||||||
sub_options[wh.id]['product_skus'].append(p_vals['sku'])
|
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.
|
# item_details can fulfil all items.
|
||||||
# this is good!!
|
# this is good!!
|
||||||
order_fake.warehouse_id = primary_wh
|
#order_fake.warehouse_id = primary_wh
|
||||||
return {'warehouse_id': primary_wh.id, 'date_planned': primary_wh_date_planned, 'sub_options': sub_options}
|
return {'warehouse_id': primary_wh.id, 'date_planned': primary_wh_date_planned,
|
||||||
|
'sub_options': sub_options}
|
||||||
|
|
||||||
# warehouses cannot fulfil all requested items!!
|
# warehouses cannot fulfil all requested items!!
|
||||||
order_fake.warehouse_id = primary_wh
|
#order_fake.warehouse_id = primary_wh
|
||||||
return {'warehouse_id': primary_wh.id}
|
return {'warehouse_id': primary_wh.id}
|
||||||
|
|
||||||
# nobody has stock!
|
# nobody has stock!
|
||||||
primary_wh = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id)
|
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}
|
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
|
||||||
|
|
||||||
def _is_in_stock(self, p_stock, buy_qty):
|
def _is_in_stock(self, p_stock, buy_qty):
|
||||||
return p_stock['real_qty_available'] >= buy_qty
|
return p_stock['real_qty_available'] >= buy_qty
|
||||||
|
|
||||||
@@ -391,9 +595,9 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
return [warehouses.filtered(lambda wh: wh.id == distances[d]) for d in wh_distances]
|
return [warehouses.filtered(lambda wh: wh.id == distances[d]) for d in wh_distances]
|
||||||
|
|
||||||
def _next_warehouse_shipping_date(self, warehouse):
|
def _next_warehouse_shipping_date(self, warehouse):
|
||||||
return fields.Datetime.to_string(warehouse.shipping_calendar_id.plan_days(0.01,
|
if warehouse.shipping_calendar_id:
|
||||||
fields.Datetime.from_string(fields.Datetime.now()),
|
return fields.Datetime.to_string(warehouse.shipping_calendar_id.plan_days(0.01, fields.Datetime.from_string(fields.Datetime.now()), compute_leaves=True))
|
||||||
compute_leaves=True))
|
return False
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _fetch_product_stock(self, warehouses, products):
|
def _fetch_product_stock(self, warehouses, products):
|
||||||
@@ -414,7 +618,17 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
def generate_shipping_options(self, base_option, order_fake):
|
def generate_shipping_options(self, base_option, order_fake):
|
||||||
# generate a carrier_id, amount, requested_date (promise date)
|
# 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.
|
# 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)
|
||||||
|
_logger.info('generate_shipping_options:: base_optoin: ' + str(base_option) + ' order_fake: ' + str(order_fake) + ' carriers: ' + str(carriers))
|
||||||
|
|
||||||
|
if not carriers:
|
||||||
|
return base_option
|
||||||
|
|
||||||
if not base_option.get('sub_options'):
|
if not base_option.get('sub_options'):
|
||||||
options = []
|
options = []
|
||||||
@@ -423,8 +637,9 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
option = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
|
option = self._generate_shipping_carrier_option(base_option, order_fake, carrier)
|
||||||
if option:
|
if option:
|
||||||
options.append(option)
|
options.append(option)
|
||||||
|
if options:
|
||||||
return options
|
return options
|
||||||
|
return [base_option]
|
||||||
else:
|
else:
|
||||||
warehouses = self.get_warehouses()
|
warehouses = self.get_warehouses()
|
||||||
original_order_fake_warehouse_id = order_fake.warehouse_id
|
original_order_fake_warehouse_id = order_fake.warehouse_id
|
||||||
@@ -488,14 +703,33 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
|
|
||||||
# this logic comes from "delivery.models.sale_order.SaleOrder"
|
# this logic comes from "delivery.models.sale_order.SaleOrder"
|
||||||
try:
|
try:
|
||||||
|
result = None
|
||||||
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']:
|
||||||
result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned'))
|
if hasattr(carrier, 'rate_shipment_date_planned'):
|
||||||
|
# New API
|
||||||
|
result = carrier.rate_shipment_date_planned(order_fake, base_option.get('date_planned'))
|
||||||
if result:
|
if result:
|
||||||
|
if not result.get('success'):
|
||||||
|
return None
|
||||||
|
price_unit, transit_days, date_delivered = result['price'], result.get('transit_days'), result.get('date_delivered')
|
||||||
|
elif hasattr(carrier, 'get_shipping_price_for_plan'):
|
||||||
|
# Old API
|
||||||
|
result = carrier.get_shipping_price_for_plan(order_fake, base_option.get('date_planned'))
|
||||||
|
if result and isinstance(result, list):
|
||||||
price_unit, transit_days, date_delivered = result[0]
|
price_unit, transit_days, date_delivered = result[0]
|
||||||
|
elif not result:
|
||||||
|
rate = carrier.rate_shipment(order_fake)
|
||||||
|
if rate and rate.get('success'):
|
||||||
|
price_unit = rate['price']
|
||||||
|
if rate.get('transit_days'):
|
||||||
|
transit_days = rate.get('transit_days')
|
||||||
|
if rate.get('date_delivered'):
|
||||||
|
date_delivered = rate.get('date_delivered')
|
||||||
else:
|
else:
|
||||||
price_unit = carrier.get_shipping_price_from_so(order_fake)[0]
|
_logger.warn('returning None because carrier: ' + str(carrier))
|
||||||
|
return None
|
||||||
else:
|
else:
|
||||||
carrier = carrier.verify_carrier(order_fake.partner_shipping_id)
|
carrier = carrier.verify_carrier(order_fake.partner_shipping_id)
|
||||||
if not carrier:
|
if not carrier:
|
||||||
@@ -514,13 +748,13 @@ class SaleOrderMakePlan(models.TransientModel):
|
|||||||
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'] = fields.Datetime.to_string(date_delivered) if date_delivered and isinstance(date_delivered, datetime) else 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.warn("Exception collecting carrier rates: " + str(e))
|
_logger.info("Exception collecting carrier rates: " + str(e))
|
||||||
|
# Want to see more?
|
||||||
# _logger.exception(e)
|
# _logger.exception(e)
|
||||||
pass
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
1
stock_mts_mto_rule
Symbolic link
1
stock_mts_mto_rule
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
external/hibou-oca/stock-logistics-warehouse/stock_mts_mto_rule
|
||||||
28
stock_warehouse_procurement/README.rst
Normal file
28
stock_warehouse_procurement/README.rst
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
***********************************
|
||||||
|
Hibou - Reorder Rules Per Warehouse
|
||||||
|
***********************************
|
||||||
|
|
||||||
|
Run the inventory scheduler per warehouse.
|
||||||
|
|
||||||
|
For more information and add-ons, visit `Hibou.io <https://hibou.io/docs/hibou-odoo-suite-1/reorder-rules-per-warehouse-163>`_.
|
||||||
|
|
||||||
|
|
||||||
|
=============
|
||||||
|
Main Features
|
||||||
|
=============
|
||||||
|
|
||||||
|
* Extends the `stock.scheduler.compute` wizard to allow the inventory scheduler to run on demand per warehouse.
|
||||||
|
|
||||||
|
.. image:: https://user-images.githubusercontent.com/15882954/45578023-f353d300-b833-11e8-8007-48fa3d96495a.png
|
||||||
|
:alt: 'Run Scheduler Wizard'
|
||||||
|
:width: 988
|
||||||
|
:align: left
|
||||||
|
|
||||||
|
|
||||||
|
=======
|
||||||
|
License
|
||||||
|
=======
|
||||||
|
|
||||||
|
Please see `LICENSE <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
|
||||||
|
|
||||||
|
Copyright Hibou Corp. 2018
|
||||||
Reference in New Issue
Block a user