# Part of Odoo. See LICENSE file for full copyright and licensing details. import json from odoo import _, api, fields, models from odoo.exceptions import UserError class SaleOrder(models.Model): _inherit = 'sale.order' pickup_location_data = fields.Json() carrier_id = fields.Many2one('delivery.carrier', string="Delivery Method", check_company=True, help="Fill this field if you plan to invoice the shipping based on picking.") delivery_message = fields.Char(readonly=True, copy=False) delivery_set = fields.Boolean(compute='_compute_delivery_state') recompute_delivery_price = fields.Boolean('Delivery cost should be recomputed') is_all_service = fields.Boolean("Service Product", compute="_compute_is_service_products") shipping_weight = fields.Float("Shipping Weight", compute="_compute_shipping_weight", store=True, readonly=False) @api.depends('order_line') def _compute_is_service_products(self): for so in self: so.is_all_service = all(line.product_id.type == 'service' for line in so.order_line.filtered(lambda x: not x.display_type)) def _compute_amount_total_without_delivery(self): self.ensure_one() delivery_cost = sum([l.price_total for l in self.order_line if l.is_delivery]) return self.amount_total - delivery_cost @api.depends('order_line') def _compute_delivery_state(self): for order in self: order.delivery_set = any(line.is_delivery for line in order.order_line) @api.onchange('order_line', 'partner_id', 'partner_shipping_id') def onchange_order_line(self): self.ensure_one() delivery_line = self.order_line.filtered('is_delivery') if delivery_line: self.recompute_delivery_price = True def _get_update_prices_lines(self): """ Exclude delivery lines from price list recomputation based on product instead of carrier """ lines = super()._get_update_prices_lines() return lines.filtered(lambda line: not line.is_delivery) def _remove_delivery_line(self): """Remove delivery products from the sales orders""" delivery_lines = self.order_line.filtered("is_delivery") if not delivery_lines: return to_delete = delivery_lines.filtered(lambda x: x.qty_invoiced == 0) if not to_delete: raise UserError( _('You can not update the shipping costs on an order where it was already invoiced!\n\nThe following delivery lines (product, invoiced quantity and price) have already been processed:\n\n') + '\n'.join(['- %s: %s x %s' % (line.product_id.with_context(display_default_code=False).display_name, line.qty_invoiced, line.price_unit) for line in delivery_lines]) ) to_delete.unlink() def set_delivery_line(self, carrier, amount): self._remove_delivery_line() for order in self: order.carrier_id = carrier.id order._create_delivery_line(carrier, amount) return True def _set_pickup_location(self, pickup_location_data): """ Set the pickup location on the current order. Note: self.ensure_one() :param str pickup_location_data: The JSON-formatted pickup location address. :return: None """ self.ensure_one() use_locations_fname = f'{self.carrier_id.delivery_type}_use_locations' if hasattr(self.carrier_id, use_locations_fname): use_location = getattr(self.carrier_id, use_locations_fname) if use_location and pickup_location_data: pickup_location = json.loads(pickup_location_data) else: pickup_location = None self.pickup_location_data = pickup_location def _get_pickup_locations(self, zip_code=None, country=None, **kwargs): """ Return the pickup locations of the delivery method close to a given zip code. Use provided `zip_code` and `country` or the order's delivery address to determine the zip code and the country to use. Note: self.ensure_one() :param int zip_code: The zip code to look up to, optional. :param res.country country: The country to look up to, required if `zip_code` is provided. :return: The close pickup locations data. :rtype: dict """ self.ensure_one() if zip_code: assert country # country is required if zip_code is provided. partner_address = self.env['res.partner'].new({ 'active': False, 'country_id': country.id, 'zip': zip_code, }) else: partner_address = self.partner_shipping_id try: error = {'error': _("No pick-up points are available for this delivery address.")} function_name = f'_{self.carrier_id.delivery_type}_get_close_locations' if not hasattr(self.carrier_id, function_name): return error pickup_locations = getattr(self.carrier_id, function_name)(partner_address, **kwargs) if not pickup_locations: return error return {'pickup_locations': pickup_locations} except UserError as e: return {'error': str(e)} def action_open_delivery_wizard(self): view_id = self.env.ref('delivery.choose_delivery_carrier_view_form').id if self.env.context.get('carrier_recompute'): name = _('Update shipping cost') carrier = self.carrier_id else: name = _('Add a shipping method') carrier = ( self.with_company(self.company_id).partner_shipping_id.property_delivery_carrier_id or self.with_company(self.company_id).partner_shipping_id.commercial_partner_id.property_delivery_carrier_id ) return { 'name': name, 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'choose.delivery.carrier', 'view_id': view_id, 'views': [(view_id, 'form')], 'target': 'new', 'context': { 'default_order_id': self.id, 'default_carrier_id': carrier.id, 'default_total_weight': self._get_estimated_weight() } } def _action_confirm(self): for order in self: order_location = order.pickup_location_data if not order_location: continue # Retrieve all the data : name, street, city, state, zip, country. name = order_location.get('name') or order.partner_shipping_id.name street = order_location['street'] city = order_location['city'] zip_code = order_location['zip_code'] country_code = order_location['country_code'] country = order.env['res.country'].search([('code', '=', country_code)]).id state = order.env['res.country.state'].search([ ('code', '=', order_location['state']), ('country_id', '=', country), ]).id if (order_location.get('state') and country) else None parent_id = order.partner_shipping_id.id email = order.partner_shipping_id.email phone = order.partner_shipping_id.phone # Check if the current partner has a partner of type 'delivery' with the same address. existing_partner = order.env['res.partner'].search([ ('street', '=', street), ('city', '=', city), ('state_id', '=', state), ('country_id', '=', country), ('parent_id', '=', parent_id), ('type', '=', 'delivery'), ], limit=1) shipping_partner = existing_partner or order.env['res.partner'].create({ 'parent_id': parent_id, 'type': 'delivery', 'name': name, 'street': street, 'city': city, 'state_id': state, 'zip': zip_code, 'country_id': country, 'email': email, 'phone': phone, }) order.with_context(update_delivery_shipping_partner=True).write({'partner_shipping_id': shipping_partner}) return super()._action_confirm() def _prepare_delivery_line_vals(self, carrier, price_unit): context = {} if self.partner_id: # set delivery detail in the customer language context['lang'] = self.partner_id.lang carrier = carrier.with_context(lang=self.partner_id.lang) # Apply fiscal position taxes = carrier.product_id.taxes_id._filter_taxes_by_company(self.company_id) taxes_ids = taxes.ids if self.partner_id and self.fiscal_position_id: taxes_ids = self.fiscal_position_id.map_tax(taxes).ids # Create the sales order line if carrier.product_id.description_sale: so_description = '%s: %s' % (carrier.name, carrier.product_id.description_sale) else: so_description = carrier.name values = { 'order_id': self.id, 'name': so_description, 'price_unit': price_unit, 'product_uom_qty': 1, 'product_uom': carrier.product_id.uom_id.id, 'product_id': carrier.product_id.id, 'tax_id': [(6, 0, taxes_ids)], 'is_delivery': True, } if carrier.free_over and self.currency_id.is_zero(price_unit) : values['name'] = _('%s\nFree Shipping', values['name']) if self.order_line: values['sequence'] = self.order_line[-1].sequence + 1 del context return values def _create_delivery_line(self, carrier, price_unit): values = self._prepare_delivery_line_vals(carrier, price_unit) return self.env['sale.order.line'].sudo().create(values) @api.depends('order_line.product_uom_qty', 'order_line.product_uom') def _compute_shipping_weight(self): for order in self: order.shipping_weight = order._get_estimated_weight() def _get_estimated_weight(self): self.ensure_one() weight = 0.0 for order_line in self.order_line.filtered(lambda l: l.product_id.type == 'consu' and not l.is_delivery and not l.display_type and l.product_uom_qty > 0): weight += order_line.product_qty * order_line.product_id.weight return weight def _update_order_line_info(self, product_id, quantity, **kwargs): """ Override of `sale` to recompute the delivery prices. :param int product_id: The product, as a `product.product` id. :return: The unit price price of the product, based on the pricelist of the sale order and the quantity selected. :rtype: float """ price_unit = super()._update_order_line_info(product_id, quantity, **kwargs) if self: self.onchange_order_line() return price_unit