mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
[IMP] delivery_hibou: backport packaging discovery by volume
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
'name': 'Delivery Hibou',
|
||||
'summary': 'Adds underlying pinnings for things like "RMA Return Labels"',
|
||||
'version': '14.0.1.2.0',
|
||||
'version': '14.0.1.3.0',
|
||||
'author': "Hibou Corp.",
|
||||
'category': 'Stock',
|
||||
'license': 'AGPL-3',
|
||||
'license': 'LGPL-3',
|
||||
'images': [],
|
||||
'website': "https://hibou.io",
|
||||
'description': """
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
from odoo import api, fields, models
|
||||
import logging
|
||||
from math import ceil
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeliveryCarrier(models.Model):
|
||||
_inherit = 'delivery.carrier'
|
||||
|
||||
@@ -17,9 +22,86 @@ class DeliveryCarrier(models.Model):
|
||||
string='Procurement Priority',
|
||||
help='Priority for this carrier. Will affect pickings '
|
||||
'and procurements related to this carrier.')
|
||||
package_by_field = fields.Selection([
|
||||
('', 'Use Default Package Type'),
|
||||
('weight', 'Weight'),
|
||||
('volume', 'Volume'),
|
||||
], string='Packaging by Product Field')
|
||||
|
||||
# Package selection
|
||||
def get_package_type_for_order(self, order):
|
||||
if self.package_by_field == 'weight':
|
||||
res = self._get_package_type_for_order(order, 'max_weight', 'weight')
|
||||
_logger.info(' get_package_type_for_order package by weight (%s) %s' % (res.id, res.name))
|
||||
return res
|
||||
elif self.package_by_field == 'volume':
|
||||
res = self._get_package_type_for_order(order, 'package_volume', 'volume')
|
||||
_logger.info(' get_package_type_for_order package by volume (%s) %s' % (res.id, res.name))
|
||||
return res
|
||||
attr = getattr(self, '%s_default_packaging_id' % (self.delivery_type, ), None)
|
||||
if attr:
|
||||
_logger.info(' get_package_type_for_order package by default_packaging_id (%s) %s' % (attr.id, attr.name))
|
||||
return attr
|
||||
attr = getattr(self, '%s_default_package_type_id' % (self.delivery_type, ), None)
|
||||
if attr:
|
||||
_logger.info(' get_package_type_for_order package by default_package_type_id (%s) %s' % (attr.id, attr.name))
|
||||
return attr
|
||||
_logger.info(' package by NULL')
|
||||
return self.env['product.packaging']
|
||||
|
||||
def get_package_count_for_order(self, order, package_type=None):
|
||||
if package_type is None:
|
||||
package_type = self.get_package_type_for_order(order)
|
||||
|
||||
if self.package_by_field == 'volume':
|
||||
return self._get_package_count_for_order(order, package_type, 'package_volume', 'volume')
|
||||
return self._get_package_count_for_order(order, package_type, 'max_weight', 'weight')
|
||||
|
||||
def _get_package_type_for_order(self, order, package_type_field, product_field):
|
||||
# NOTE do not optimize this into non-loop.
|
||||
# this may be an orderfake
|
||||
order_total = 0.0
|
||||
for ol in order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')):
|
||||
order_total += ol.product_id[product_field] * ol.product_uom_qty
|
||||
_logger.info(' _get_package_type_for_order order_total ' + str(order_total))
|
||||
if order_total:
|
||||
package_types = self.env['product.packaging'].search([
|
||||
('package_carrier_type', 'in', ('none', False, self.delivery_type)),
|
||||
('use_in_package_selection', '=', True),
|
||||
], order=package_type_field)
|
||||
package_type = None
|
||||
for package_type in package_types:
|
||||
if package_type[package_type_field] >= order_total:
|
||||
return package_type
|
||||
return package_types if not package_type else package_type
|
||||
return self.env['product.packaging']
|
||||
|
||||
def _get_package_count_for_order(self, order, package_type, package_type_field, product_field):
|
||||
# NOTE do not optimize this into non-loop.
|
||||
# this may be an orderfake
|
||||
order_total = 0.0
|
||||
for ol in order.order_line.filtered(lambda ol: ol.product_id.type in ('product', 'consu')):
|
||||
order_total += ol.product_id[product_field] * ol.product_uom_qty
|
||||
package_type_field_value = package_type[package_type_field]
|
||||
if not package_type_field_value or package_type_field_value >= order_total:
|
||||
return 1
|
||||
return ceil(order_total / package_type_field_value)
|
||||
|
||||
# Utility
|
||||
def get_to_ship_picking_packages(self, picking):
|
||||
# Will return a stock.quant.package record set if the picking has packages
|
||||
# in the case of multi-packing and none applicable, will return None
|
||||
# Additionally, will not return packages that have a tracking number (because they have shipped)
|
||||
picking_packages = picking.package_ids
|
||||
package_carriers = picking_packages.mapped('carrier_id')
|
||||
if package_carriers:
|
||||
# only ship ours
|
||||
picking_packages = picking_packages.filtered(lambda p: p.carrier_id == self and not p.carrier_tracking_ref)
|
||||
|
||||
if package_carriers and not picking_packages:
|
||||
return None
|
||||
return picking_packages
|
||||
|
||||
def get_insurance_value(self, order=None, picking=None, package=None):
|
||||
value = 0.0
|
||||
if order:
|
||||
@@ -142,7 +224,7 @@ class DeliveryCarrier(models.Model):
|
||||
|
||||
def _get_shipper_warehouse_dropship_in(self, picking):
|
||||
if picking.sale_id:
|
||||
picking.sale_id.partner_shipping_id
|
||||
return 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):
|
||||
@@ -222,8 +304,8 @@ class DeliveryCarrier(models.Model):
|
||||
packages = picking.package_ids
|
||||
else:
|
||||
if packages:
|
||||
raise UserError('Cannot rate package without picking.')
|
||||
self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now()))
|
||||
raise UserError(_('Cannot rate package without picking.'))
|
||||
self = self.with_context(date_planned=('date_planned' in self.env['sale.order']._fields and order.date_planned or fields.Datetime.now()))
|
||||
|
||||
res = []
|
||||
for carrier in self:
|
||||
@@ -232,16 +314,16 @@ class DeliveryCarrier(models.Model):
|
||||
p.packaging_id.package_carrier_type in (False, '', 'none', carrier.delivery_type))
|
||||
if packages and not carrier_packages:
|
||||
continue
|
||||
if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type):
|
||||
attr = getattr(carrier, '%s_rate_shipment_multi' % self.delivery_type, None)
|
||||
if attr:
|
||||
try:
|
||||
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||
picking=picking,
|
||||
packages=carrier_packages)
|
||||
res += attr(order=order, picking=picking, packages=carrier_packages)
|
||||
except TypeError:
|
||||
# TODO remove catch if after Odoo 14
|
||||
# This is intended to find ones that don't support packages= kwarg
|
||||
res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order,
|
||||
picking=picking)
|
||||
res2 = attr(order=order, picking=picking)
|
||||
if res2:
|
||||
res += res2
|
||||
|
||||
return res
|
||||
|
||||
@@ -252,11 +334,12 @@ class DeliveryCarrier(models.Model):
|
||||
:param packages: Optional recordset of packages (should be for this carrier)
|
||||
'''
|
||||
self.ensure_one()
|
||||
if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
|
||||
attr = getattr(self, '%s_cancel_shipment' % self.delivery_type, None)
|
||||
if attr:
|
||||
# No good way to tell if this method takes the kwarg for packages
|
||||
if packages:
|
||||
try:
|
||||
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings, packages=packages)
|
||||
return attr(pickings, packages=packages)
|
||||
except TypeError:
|
||||
# we won't be able to cancel the packages properly
|
||||
# here we will TRY to make a good call here where we put the package references into the picking
|
||||
@@ -267,7 +350,7 @@ class DeliveryCarrier(models.Model):
|
||||
'carrier_tracking_ref': tracking_ref,
|
||||
})
|
||||
|
||||
return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings)
|
||||
return attr(pickings)
|
||||
|
||||
|
||||
class ChooseDeliveryPackage(models.TransientModel):
|
||||
@@ -313,16 +396,18 @@ class ChooseDeliveryPackage(models.TransientModel):
|
||||
picking_move_lines = self.picking_id.move_line_nosuggest_ids
|
||||
|
||||
move_line_ids = picking_move_lines.filtered(lambda ml:
|
||||
float_compare(ml.qty_done, 0.0, precision_rounding=ml.product_uom_id.rounding) > 0
|
||||
and not ml.result_package_id
|
||||
)
|
||||
float_compare(ml.qty_done, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) > 0
|
||||
and not ml.result_package_id
|
||||
)
|
||||
if not move_line_ids:
|
||||
move_line_ids = picking_move_lines.filtered(lambda ml: float_compare(ml.product_uom_qty, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) > 0 and float_compare(ml.qty_done, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) == 0)
|
||||
precision_rounding=ml.product_uom_id.rounding) > 0 and float_compare(
|
||||
ml.qty_done, 0.0,
|
||||
precision_rounding=ml.product_uom_id.rounding) == 0)
|
||||
|
||||
delivery_package = self.picking_id._put_in_pack(move_line_ids)
|
||||
# write shipping weight and product_packaging on 'stock_quant_package' if needed
|
||||
# write shipping weight and package type on 'stock_quant_package' if needed
|
||||
if self.delivery_packaging_id:
|
||||
delivery_package.packaging_id = self.delivery_packaging_id
|
||||
if self.shipping_weight:
|
||||
|
||||
@@ -2,6 +2,18 @@ from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockPackageType(models.Model):
|
||||
_inherit = 'product.packaging'
|
||||
|
||||
use_in_package_selection = fields.Boolean()
|
||||
package_volume = fields.Float(compute='_compute_package_volume', store=True)
|
||||
|
||||
@api.depends('packaging_length', 'width', 'height')
|
||||
def _compute_package_volume(self):
|
||||
for pt in self:
|
||||
pt.package_volume = pt.packaging_length * pt.width * pt.height
|
||||
|
||||
|
||||
class StockQuantPackage(models.Model):
|
||||
_inherit = 'stock.quant.package'
|
||||
|
||||
@@ -15,7 +27,7 @@ class StockQuantPackage(models.Model):
|
||||
picking_id = self._context.get('active_id')
|
||||
picking_model = self._context.get('active_model')
|
||||
if not picking_id or picking_model != 'stock.picking':
|
||||
raise UserError('Cannot cancel package other than through shipment/picking.')
|
||||
raise UserError(_('Cannot cancel package other than through shipment/picking.'))
|
||||
return self.env['stock.picking'].browse(picking_id)
|
||||
|
||||
def send_to_shipper(self):
|
||||
|
||||
@@ -7,6 +7,11 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
super(TestDeliveryHibou, self).setUp()
|
||||
self.partner = self.env.ref('base.res_partner_address_13')
|
||||
self.product = self.env.ref('product.product_product_7')
|
||||
self.product.write({
|
||||
'type': 'product',
|
||||
'weight': 1.0,
|
||||
'volume': 15.0,
|
||||
})
|
||||
# Create Shipping Account
|
||||
self.shipping_account = self.env['partner.shipping.account'].create({
|
||||
'name': '123123',
|
||||
@@ -20,6 +25,26 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
self.carrier = self.env['delivery.carrier'].create({
|
||||
'name': 'Test Carrier1',
|
||||
'product_id': self.delivery_product.id,
|
||||
'delivery_type': 'fixed',
|
||||
})
|
||||
# update all other package types to have
|
||||
self.package_type_large = self.env['product.packaging'].create({
|
||||
'name': 'Large 15x15x15',
|
||||
'packaging_length': 15.0,
|
||||
'height': 15.0,
|
||||
'width': 15.0,
|
||||
'max_weight': 50.0,
|
||||
'package_carrier_type': 'none',
|
||||
'use_in_package_selection': True,
|
||||
})
|
||||
self.package_type_small = self.env['product.packaging'].create({
|
||||
'name': 'Small 2x2x4',
|
||||
'packaging_length': 4.0,
|
||||
'height': 2.0,
|
||||
'width': 2.0,
|
||||
'max_weight': 1.0,
|
||||
'package_carrier_type': 'none',
|
||||
'use_in_package_selection': True,
|
||||
})
|
||||
|
||||
def test_delivery_hibou(self):
|
||||
@@ -28,10 +53,8 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
|
||||
# Assign values to new Carrier
|
||||
test_insurance_value = 600
|
||||
test_sig_req_value = 300
|
||||
test_procurement_priority = '1'
|
||||
self.carrier.automatic_insurance_value = test_insurance_value
|
||||
self.carrier.automatic_sig_req_value = test_sig_req_value
|
||||
self.carrier.procurement_priority = test_procurement_priority
|
||||
|
||||
|
||||
@@ -42,6 +65,7 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
'shipping_account_id': self.shipping_account.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 2.0,
|
||||
})]
|
||||
})
|
||||
self.assertFalse(sale_order.carrier_id)
|
||||
@@ -64,6 +88,35 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
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 Package selection
|
||||
default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order)
|
||||
self.assertFalse(default_package_type, 'Fixed should not have a default packaging type.')
|
||||
|
||||
# by product weight
|
||||
sale_order.carrier_id.package_by_field = 'weight'
|
||||
|
||||
default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order)
|
||||
self.assertTrue(default_package_type)
|
||||
self.assertEqual(default_package_type, self.package_type_large)
|
||||
|
||||
# change qty ordered to try to get the small package type
|
||||
sale_order.order_line.write({
|
||||
'product_uom_qty': 1.0,
|
||||
})
|
||||
default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order)
|
||||
self.assertEqual(default_package_type, self.package_type_small)
|
||||
|
||||
# by product volume
|
||||
sale_order.carrier_id.package_by_field = 'volume'
|
||||
default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order)
|
||||
self.assertEqual(default_package_type, self.package_type_small)
|
||||
|
||||
sale_order.order_line.write({
|
||||
'product_uom_qty': 2.0,
|
||||
})
|
||||
default_package_type = sale_order.carrier_id.get_package_type_for_order(sale_order)
|
||||
self.assertEqual(default_package_type, self.package_type_large)
|
||||
|
||||
# Test attn
|
||||
test_ref = 'TEST100'
|
||||
self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), False)
|
||||
@@ -79,9 +132,7 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
|
||||
def test_carrier_hibou_out(self):
|
||||
test_insurance_value = 4000
|
||||
test_sig_req_value = 4000
|
||||
self.carrier.automatic_insurance_value = test_insurance_value
|
||||
self.carrier.automatic_sig_req_value = test_sig_req_value
|
||||
|
||||
picking_out = self.env.ref('stock.outgoing_shipment_main_warehouse')
|
||||
picking_out.action_assign()
|
||||
@@ -92,35 +143,32 @@ class TestDeliveryHibou(common.TransactionCase):
|
||||
# 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])
|
||||
test_one_value = 3300.0
|
||||
test_qty = 15.0
|
||||
test_whole_value = test_qty * test_one_value
|
||||
|
||||
self.assertEqual(picking_out.move_line_ids.mapped('product_uom_qty'), [test_qty])
|
||||
self.assertEqual(picking_out.move_line_ids.mapped('product_id.standard_price'), [test_one_value])
|
||||
|
||||
|
||||
# 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.declared_value(), test_whole_value)
|
||||
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), picking_out.declared_value())
|
||||
self.assertTrue(picking_out.carrier_id.get_signature_required(picking=picking_out))
|
||||
|
||||
# Workflow where user explicitly opts out of insurance on the picking level.
|
||||
picking_out.require_insurance = 'no'
|
||||
picking_out.require_signature = 'no'
|
||||
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
|
||||
self.assertFalse(picking_out.carrier_id.get_signature_required(picking=picking_out))
|
||||
picking_out.require_insurance = 'auto'
|
||||
picking_out.require_signature = '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.declared_value(), test_one_value)
|
||||
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0)
|
||||
self.assertFalse(picking_out.carrier_id.get_signature_required(picking=picking_out))
|
||||
# Workflow where user opts in to insurance.
|
||||
picking_out.require_insurance = 'yes'
|
||||
picking_out.require_signature = 'yes'
|
||||
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 3300.0)
|
||||
self.assertTrue(picking_out.carrier_id.get_signature_required(picking=picking_out))
|
||||
self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), test_one_value)
|
||||
picking_out.require_insurance = 'auto'
|
||||
picking_out.require_signature = 'auto'
|
||||
|
||||
# Test with picking having 3rd party account.
|
||||
self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), None)
|
||||
|
||||
@@ -52,4 +52,17 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_packaging_delivery_form" model="ir.ui.view">
|
||||
<field name="name">hibou.product.packaging.form.delivery</field>
|
||||
<field name="model">product.packaging</field>
|
||||
<field name="inherit_id" ref="delivery.product_packaging_delivery_form" />
|
||||
<field name="priority" eval="20" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='barcode']" position="after">
|
||||
<field name="package_volume" />
|
||||
<field name="use_in_package_selection" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user