From 4d00fca6710c3a9eb4c3c0acb42f04934438f5f8 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 4 Oct 2021 10:54:08 -0700 Subject: [PATCH 01/31] [IMP] delivery_hibou: multi rating and package tracking backports from 14.0 --- delivery_hibou/__manifest__.py | 2 +- delivery_hibou/models/delivery.py | 89 +++++++++++++- delivery_hibou/models/stock.py | 123 +++++++++++++++++++- delivery_hibou/tests/test_delivery_hibou.py | 10 +- delivery_hibou/views/delivery_views.xml | 12 ++ delivery_hibou/views/stock_views.xml | 36 +++++- 6 files changed, 263 insertions(+), 9 deletions(-) diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index e53e01b9..d4380058 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Delivery Hibou', 'summary': 'Adds underlying pinnings for things like "RMA Return Labels"', - 'version': '11.0.1.0.0', + 'version': '11.0.1.1.0', 'author': "Hibou Corp.", 'category': 'Stock', 'license': 'AGPL-3', diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 50088e78..c7bff160 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,5 +1,6 @@ -from odoo import fields, models +from odoo import api, fields, models from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES +from odoo.exceptions import UserError class DeliveryCarrier(models.Model): @@ -157,3 +158,89 @@ class DeliveryCarrier(models.Model): def _get_recipient_out(self, picking): return picking.partner_id + + # -------------------------- # + # API for external providers # + # -------------------------- # + @api.multi + def rate_shipment_multi(self, order=None, picking=None, packages=None): + ''' Compute the price of the order shipment + + :param order: record of sale.order or None + :param picking: record of stock.picking or None + :param packages: recordset of stock.quant.package or None (requires picking also set) + :return list: dict: { + 'carrier': delivery.carrier(), + 'success': boolean, + 'price': a float, + 'error_message': a string containing an error message, + 'warning_message': a string containing a warning message, + 'date_planned': a datetime for when the shipment is supposed to leave, + 'date_delivered': a datetime for when the shipment is supposed to arrive, + 'transit_days': a Float for how many days it takes in transit, + 'service_code': a string that represents the service level/agreement, + 'package': stock.quant.package(), + } + + e.g. self == delivery.carrier(5, 6) + then return might be: + [ + {'carrier': delivery.carrier(5), 'price': 10.50, 'service_code': 'GROUND_HOME_DELIVERY', ...}, + {'carrier': delivery.carrier(7), 'price': 12.99, 'service_code': 'FEDEX_EXPRESS_SAVER', ...}, # NEW! + {'carrier': delivery.carrier(6), 'price': 8.0, 'service_code': 'USPS_PRI', ...}, + ] + ''' + self.ensure_one() + + if picking: + self = self.with_context(date_planned=fields.Datetime.now()) + if not packages: + 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())) + + res = [] + for carrier in self: + carrier_packages = packages.filtered(lambda p: not p.carrier_tracking_ref and + (not p.carrier_id or p.carrier_id == carrier) and + 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): + try: + res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(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) + + return res + + def cancel_shipment(self, pickings, packages=None): + ''' Cancel a shipment + + :param pickings: A recordset of pickings + :param packages: Optional recordset of packages (should be for this carrier) + ''' + self.ensure_one() + if hasattr(self, '%s_cancel_shipment' % self.delivery_type): + # 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) + 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 + # and let the original mechanisms try to work here + tracking_ref = ','.join(packages.mapped('carrier_tracking_ref')) + pickings.write({ + 'carrier_id': self.id, + 'carrier_tracking_ref': tracking_ref, + }) + + return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index c1a6a792..3dffc186 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -1,4 +1,27 @@ -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class StockQuantPackage(models.Model): + _inherit = 'stock.quant.package' + + carrier_id = fields.Many2one('delivery.carrier', string='Carrier') + carrier_tracking_ref = fields.Char(string='Tracking Reference') + + def _get_active_picking(self): + 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.') + return self.env['stock.picking'].browse(picking_id) + + def send_to_shipper(self): + picking = self._get_active_picking() + picking.with_context(packages=self).send_to_shipper() + + def cancel_shipment(self): + picking = self._get_active_picking() + picking.with_context(packages=self).cancel_shipment() class StockPicking(models.Model): @@ -11,6 +34,22 @@ class StockPicking(models.Model): ('no', 'No'), ], string='Require Insurance', default='auto', help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') + package_carrier_tracking_ref = fields.Char(string='Package Tracking Numbers', compute='_compute_package_carrier_tracking_ref') + + @api.depends('package_ids.carrier_tracking_ref') + def _compute_package_carrier_tracking_ref(self): + for picking in self: + package_refs = picking.package_ids.filtered('carrier_tracking_ref').mapped('carrier_tracking_ref') + if package_refs: + picking.package_carrier_tracking_ref = ','.join(package_refs) + else: + picking.package_carrier_tracking_ref = False + + @api.onchange('carrier_id') + def _onchange_carrier_id_for_priority(self): + for picking in self: + if picking.carrier_id and picking.carrier_id.procurement_priority: + picking.priority = picking.carrier_id.procurement_priority @api.one @api.depends('move_lines.priority', 'carrier_id') @@ -27,6 +66,11 @@ class StockPicking(models.Model): 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 + carrier_id = values.get('carrier_id') + if carrier_id: + carrier = self.env['delivery.carrier'].browse(carrier_id) + if carrier.procurement_priority: + values['priority'] = carrier.procurement_priority res = super(StockPicking, self).create(values) return res @@ -39,6 +83,83 @@ class StockPicking(models.Model): cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0]) return cost + def clear_carrier_tracking_ref(self): + self.write({'carrier_tracking_ref': False}) + + def reset_carrier_tracking_ref(self): + for picking in self: + picking.carrier_tracking_ref = picking.package_carrier_tracking_ref + + # Override to send to specific packaging carriers + def send_to_shipper(self): + packages = self._context.get('packages') + self.ensure_one() + if not packages: + packages = self.package_ids + package_carriers = packages.mapped('carrier_id') + if not package_carriers: + # Original behavior + return super().send_to_shipper() + + tracking_numbers = [] + carrier_prices = [] + order_currency = self.sale_id.currency_id or self.company_id.currency_id + for carrier in package_carriers: + self.carrier_id = carrier + carrier_packages = packages.filtered(lambda p: p.carrier_id == carrier) + res = carrier.send_shipping(self) + if res: + res = res[0] + if carrier.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= carrier.amount: + res['exact_price'] = 0.0 + carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0)) + carrier_prices.append(carrier_price) + tracking_number = '' + if res['tracking_number']: + tracking_number = res['tracking_number'] + tracking_numbers.append(tracking_number) + # Try to add tracking to the individual packages. + potential_tracking_numbers = tracking_number.split(',') + if len(potential_tracking_numbers) >= len(carrier_packages): + for t, p in zip(potential_tracking_numbers, carrier_packages): + p.carrier_tracking_ref = t + else: + carrier_packages.write({'carrier_tracking_ref': tracking_number}) + msg = _("Shipment sent to carrier %s for shipping with tracking number %s
Cost: %.2f %s") % (carrier.name, tracking_number, carrier_price, order_currency.name) + self.message_post(body=msg) + + self.carrier_price = sum(carrier_prices or [0.0]) + self.carrier_tracking_ref = ','.join(tracking_numbers or ['']) + + # Override to provide per-package versions... + def cancel_shipment(self): + packages = self._context.get('packages') + pickings_with_package_tracking = self.filtered(lambda p: p.package_carrier_tracking_ref) + for picking in pickings_with_package_tracking: + if packages: + current_packages = packages + else: + current_packages = picking.package_ids + # Packages without a carrier can just be cleared + packages_without_carrier = current_packages.filtered(lambda p: not p.carrier_id and p.carrier_tracking_ref) + packages_without_carrier.write({ + 'carrier_tracking_ref': False, + }) + # Packages with carrier can use the carrier method + packages_with_carrier = current_packages.filtered(lambda p: p.carrier_id and p.carrier_tracking_ref) + carriers = packages_with_carrier.mapped('carrier_id') + for carrier in carriers: + carrier_packages = packages_with_carrier.filtered(lambda p: p.carrier_id == carrier) + carrier.cancel_shipment(self, packages=carrier_packages) + package_refs = ','.join(carrier_packages.mapped('carrier_tracking_ref')) + msg = "Shipment %s cancelled" % package_refs + picking.message_post(body=msg) + carrier_packages.write({'carrier_tracking_ref': False}) + + pickings_without_package_tracking = self - pickings_with_package_tracking + if pickings_without_package_tracking: + # use original on these + super(StockPicking, pickings_without_package_tracking).cancel_shipment() class StockMove(models.Model): diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index 68866f45..51a738ca 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -24,11 +24,11 @@ class TestDeliveryHibou(common.TransactionCase): def test_delivery_hibou(self): # Assign a new shipping account - self.partner.shipping_account_id = self.shipping_account + self.partner.shipping_account_ids = self.shipping_account # Assign values to new Carrier test_insurance_value = 600 - test_procurement_priority = '2' + test_procurement_priority = '1' self.carrier.automatic_insurance_value = test_insurance_value self.carrier.procurement_priority = test_procurement_priority @@ -128,9 +128,9 @@ class TestDeliveryHibou(common.TransactionCase): 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.move_line_ids.mapped('qty_done'), [0.0, 0.0, 0.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0, 10.0, 12.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0, 35.0, 1700.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), diff --git a/delivery_hibou/views/delivery_views.xml b/delivery_hibou/views/delivery_views.xml index 01208bbd..638ef921 100644 --- a/delivery_hibou/views/delivery_views.xml +++ b/delivery_hibou/views/delivery_views.xml @@ -11,4 +11,16 @@ + + + hibou.choose.delivery.package.form + choose.delivery.package + + + + [('product_id', '=', False)] + + + + \ No newline at end of file diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml index 78067b01..f806127d 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -1,14 +1,48 @@ + + hibou.stock.quant.package.form + stock.quant.package + + + + + + + hibou.delivery.stock.picking_withcarrier.form.view stock.picking + - + + +