diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index 5b7350e7..de31aeea 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': '14.0.1.0.0', + 'version': '14.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 4b478dd8..d281fb5f 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,5 +1,6 @@ from odoo import fields, models from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES +from odoo.exceptions import UserError class DeliveryCarrier(models.Model): @@ -161,11 +162,12 @@ class DeliveryCarrier(models.Model): # -------------------------- # # API for external providers # # -------------------------- # - def rate_shipment_multi(self, order=None, picking=None): + 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, @@ -176,6 +178,7 @@ class DeliveryCarrier(models.Model): '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) @@ -190,12 +193,53 @@ class DeliveryCarrier(models.Model): 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): - carrier_rates = getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, picking=picking) - res += carrier_rates + 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 725a7bbc..ec2d2664 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.send_to_shipper(packages=self) + + def cancel_shipment(self): + picking = self._get_active_picking() + picking.cancel_shipment(packages=self) class StockPicking(models.Model): @@ -11,6 +34,16 @@ 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): @@ -41,3 +74,86 @@ class StockPicking(models.Model): # 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 + + 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=None): + 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 %(carrier_name)s for shipping with tracking number %(ref)s
Cost: %(price).2f %(currency)s", + carrier_name=carrier.name, + ref=tracking_number, + price=carrier_price, + currency=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 ['']) + self._add_delivery_cost_to_so() + + # Override to provide per-package versions... + def cancel_shipment(self, packages=None): + 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() diff --git a/delivery_hibou/views/delivery_views.xml b/delivery_hibou/views/delivery_views.xml index 01208bbd..960df1c2 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 + + + + [('package_carrier_type', '!=', 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..6bb47f34 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -1,14 +1,46 @@ + + hibou.stock.quant.package.form + stock.quant.package + + + + + + + hibou.delivery.stock.picking_withcarrier.form.view stock.picking + + +