From ef86571acda63911b0cc099534c6dff5915db2fa Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 31 Oct 2022 17:05:28 +0000 Subject: [PATCH] [IMP] delivery_hibou: backport packaging discovery by volume --- delivery_hibou/__manifest__.py | 4 +- delivery_hibou/models/delivery.py | 123 +++++++++++++++++--- delivery_hibou/models/stock.py | 14 ++- delivery_hibou/tests/test_delivery_hibou.py | 82 ++++++++++--- delivery_hibou/views/stock_views.xml | 13 +++ 5 files changed, 197 insertions(+), 39 deletions(-) diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index 8618c1c4..111a9f0e 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -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': """ diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index a0678350..be9f8e8b 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -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: diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 060faf3e..f8eb9423 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -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): diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index 7a524026..e066a12d 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -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) diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml index 0989f519..c2392f39 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -52,4 +52,17 @@ + + hibou.product.packaging.form.delivery + product.packaging + + + + + + + + + +