# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) class DeliveryCarrier(models.Model): _name = 'delivery.carrier' _inherits = {'product.product': 'product_id'} _description = "Carrier" _order = 'sequence, id' ''' A Shipping Provider In order to add your own external provider, follow these steps: 1. Create your model MyProvider that _inherit 'delivery.carrier' 2. Extend the selection of the field "delivery_type" with a pair ('', 'My Provider') 3. Add your methods: _get_shipping_price_from_so _send_shipping _open_tracking_page _cancel_shipment (they are documented hereunder) ''' # -------------------------------- # # Internals for shipping providers # # -------------------------------- # sequence = fields.Integer(help="Determine the display order", default=10) # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex') delivery_type = fields.Selection([('fixed', 'Fixed Price'), ('base_on_rule', 'Based on Rules')], string='Provider', default='fixed', required=True) product_type = fields.Selection(related='product_id.type', default='service') product_sale_ok = fields.Boolean(related='product_id.sale_ok', default=False) product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete="cascade") price = fields.Float(compute='get_price') available = fields.Boolean(compute='get_price') free_if_more_than = fields.Boolean('Free if Order total is more than', help="If the order is more expensive than a certain amount, the customer can benefit from a free shipping", default=False) amount = fields.Float(string='Amount', help="Amount of the order to benefit from a free shipping, expressed in the company currency") country_ids = fields.Many2many('res.country', 'delivery_carrier_country_rel', 'carrier_id', 'country_id', 'Countries') state_ids = fields.Many2many('res.country.state', 'delivery_carrier_state_rel', 'carrier_id', 'state_id', 'States') zip_from = fields.Char('Zip From') zip_to = fields.Char('Zip To') price_rule_ids = fields.One2many('delivery.price.rule', 'carrier_id', 'Pricing Rules', copy=True) fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price',help="Keep empty if the pricing depends on the advanced pricing per destination") integration_level = fields.Selection([('rate', 'Get Rate'), ('rate_and_ship', 'Get Rate and Create Shipment')], string="Integration Level", default='rate_and_ship', help="Action while validating Delivery Orders") prod_environment = fields.Boolean("Environment", help="Set to True if your credentials are certified for production.") margin = fields.Integer(help='This percentage will be added to the shipping price.') _sql_constraints = [ ('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'), ] @api.one def toggle_prod_environment(self): self.prod_environment = not self.prod_environment @api.multi def install_more_provider(self): return { 'name': 'New Providers', 'view_mode': 'kanban', 'res_model': 'ir.module.module', 'domain': [['name', 'ilike', 'delivery_']], 'type': 'ir.actions.act_window', 'help': _('''

Buy Odoo Enterprise now to get more providers.

'''), } @api.multi def name_get(self): display_delivery = self.env.context.get('display_delivery', False) order_id = self.env.context.get('order_id', False) if display_delivery and order_id: order = self.env['sale.order'].browse(order_id) currency = order.pricelist_id.currency_id.name or '' res = [] for carrier_id in self.ids: try: r = self.read([carrier_id], ['name', 'price'])[0] res.append((r['id'], r['name'] + ' (' + (str(r['price'])) + ' ' + currency + ')')) except ValidationError: r = self.read([carrier_id], ['name'])[0] res.append((r['id'], r['name'])) else: res = super(DeliveryCarrier, self).name_get() return res @api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price') def _compute_fixed_price(self): for carrier in self: carrier.fixed_price = carrier.product_id.list_price def _set_product_fixed_price(self): for carrier in self: carrier.product_id.list_price = carrier.fixed_price @api.one def get_price(self): SaleOrder = self.env['sale.order'] self.available = False self.price = False order_id = self.env.context.get('order_id') if order_id: # FIXME: temporary hack until we refactor the delivery API in master order = SaleOrder.browse(order_id) if self.delivery_type not in ['fixed', 'base_on_rule']: try: computed_price = self.get_shipping_price_from_so(order)[0] self.available = True except ValidationError as e: # No suitable delivery method found, probably configuration error _logger.info("Carrier %s: %s, not found", self.name, e.name) computed_price = 0.0 else: carrier = self.verify_carrier(order.partner_shipping_id) if carrier: try: computed_price = carrier.get_price_available(order) self.available = True except UserError as e: # No suitable delivery method found, probably configuration error _logger.info("Carrier %s: %s", carrier.name, e.name) computed_price = 0.0 else: computed_price = 0.0 self.price = computed_price * (1.0 + (float(self.margin) / 100.0)) # -------------------------- # # API for external providers # # -------------------------- # # TODO define and handle exceptions that could be thrown by providers def get_shipping_price_from_so(self, orders): ''' For every sale order, compute the price of the shipment :param orders: A recordset of sale orders :return list: A list of floats, containing the estimated price for the shipping of the sale order ''' self.ensure_one() if hasattr(self, '%s_get_shipping_price_from_so' % self.delivery_type): return getattr(self, '%s_get_shipping_price_from_so' % self.delivery_type)(orders) def send_shipping(self, pickings): ''' Send the package to the service provider :param pickings: A recordset of pickings :return list: A list of dictionaries (one per picking) containing of the form:: { 'exact_price': price, 'tracking_number': number } ''' self.ensure_one() if hasattr(self, '%s_send_shipping' % self.delivery_type): return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings) def get_tracking_link(self, pickings): ''' Ask the tracking link to the service provider :param pickings: A recordset of pickings :return list: A list of string URLs, containing the tracking links for every picking ''' self.ensure_one() if hasattr(self, '%s_get_tracking_link' % self.delivery_type): return getattr(self, '%s_get_tracking_link' % self.delivery_type)(pickings) def cancel_shipment(self, pickings): ''' Cancel a shipment :param pickings: A recordset of pickings ''' self.ensure_one() if hasattr(self, '%s_cancel_shipment' % self.delivery_type): return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) @api.onchange('state_ids') def onchange_states(self): self.country_ids = [(6, 0, self.country_ids.ids + self.state_ids.mapped('country_id.id'))] @api.onchange('country_ids') def onchange_countries(self): self.state_ids = [(6, 0, self.state_ids.filtered(lambda state: state.id in self.country_ids.mapped('state_ids').ids).ids)] @api.multi def verify_carrier(self, contact): self.ensure_one() if self.country_ids and contact.country_id not in self.country_ids: return False if self.state_ids and contact.state_id not in self.state_ids: return False if self.zip_from and (contact.zip or '').upper() < self.zip_from.upper(): return False if self.zip_to and (contact.zip or '').upper() > self.zip_to.upper(): return False return self @api.multi def create_price_rules(self): PriceRule = self.env['delivery.price.rule'] for record in self: # If using advanced pricing per destination: do not change if record.delivery_type == 'base_on_rule': continue # Not using advanced pricing per destination: override lines if record.delivery_type == 'base_on_rule' and not (record.fixed_price is not False or record.free_if_more_than): record.price_rule_ids.unlink() # Check that float, else 0.0 is False if not (record.fixed_price is not False or record.free_if_more_than): continue if record.delivery_type == 'fixed': PriceRule.search([('carrier_id', '=', record.id)]).unlink() line_data = { 'carrier_id': record.id, 'variable': 'price', 'operator': '>=', } # Create the delivery price rules if record.free_if_more_than: line_data.update({ 'max_value': record.amount, 'standard_price': 0.0, 'list_base_price': 0.0, }) PriceRule.create(line_data) if record.fixed_price is not False: line_data.update({ 'max_value': 0.0, 'standard_price': record.fixed_price, 'list_base_price': record.fixed_price, }) PriceRule.create(line_data) return True @api.model def create(self, vals): res = super(DeliveryCarrier, self).create(vals) res.create_price_rules() return res @api.multi def write(self, vals): res = super(DeliveryCarrier, self).write(vals) self.create_price_rules() return res @api.multi def get_price_available(self, order): self.ensure_one() total = weight = volume = quantity = 0 total_delivery = 0.0 for line in order.order_line: if line.state == 'cancel': continue if line.is_delivery: total_delivery += line.price_total if not line.product_id or line.is_delivery: continue qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id) weight += (line.product_id.weight or 0.0) * qty volume += (line.product_id.volume or 0.0) * qty quantity += qty total = (order.amount_total or 0.0) - total_delivery total = order.currency_id.with_context(date=order.date_order).compute(total, order.company_id.currency_id) return self.get_price_from_picking(total, weight, volume, quantity) def get_price_from_picking(self, total, weight, volume, quantity): price = 0.0 criteria_found = False price_dict = {'price': total, 'volume': volume, 'weight': weight, 'wv': volume * weight, 'quantity': quantity} for line in self.price_rule_ids: test = safe_eval(line.variable + line.operator + str(line.max_value), price_dict) if test: price = line.list_base_price + line.list_price * price_dict[line.variable_factor] criteria_found = True break if not criteria_found: raise UserError(_("Selected product in the delivery method doesn't fulfill any of the delivery carrier(s) criteria.")) return price