299 lines
13 KiB
Python
299 lines
13 KiB
Python
# -*- 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>', 'My Provider')
|
|
3. Add your methods:
|
|
<my_provider>_get_shipping_price_from_so
|
|
<my_provider>_send_shipping
|
|
<my_provider>_open_tracking_page
|
|
<my_provider>_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': _('''<p class="oe_view_nocontent">
|
|
Buy Odoo Enterprise now to get more providers.
|
|
</p>'''),
|
|
}
|
|
|
|
@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
|