- Set the company currency in USD - Create a pricelist in EUR - Create a SO with the EUR pricelist - Add a stockable product - Click on 'Add Shipping' - Select 'Normal Delivery Charges' which has a fixed price The price is not updated according to the USD - EUR exchange rate. This happens because no company is set on the delivery method, so no conversion is performed. We fall back on the order company, then the current environement company. opw-2159838 closes odoo/odoo#43167 X-original-commit: 6a6a7e6e64a7e3727a6de1ca8d35361112d99de8 Signed-off-by: Nicolas Martinelli (nim) <nim@odoo.com>
280 lines
12 KiB
Python
280 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
import psycopg2
|
|
|
|
from odoo import api, fields, models, registry, SUPERUSER_ID, _
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class DeliveryCarrier(models.Model):
|
|
_name = 'delivery.carrier'
|
|
_description = "Shipping Methods"
|
|
_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>_rate_shipment
|
|
<my_provider>_send_shipping
|
|
<my_provider>_get_tracking_link
|
|
<my_provider>_cancel_shipment
|
|
_<my_provider>_get_default_custom_package_code
|
|
(they are documented hereunder)
|
|
'''
|
|
|
|
# -------------------------------- #
|
|
# Internals for shipping providers #
|
|
# -------------------------------- #
|
|
|
|
name = fields.Char('Delivery Method', required=True, translate=True)
|
|
active = fields.Boolean(default=True)
|
|
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')], string='Provider', default='fixed', required=True)
|
|
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.")
|
|
debug_logging = fields.Boolean('Debug logging', help="Log requests in order to ease debugging")
|
|
company_id = fields.Many2one('res.company', string='Company', related='product_id.company_id', store=True, readonly=False)
|
|
product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete='restrict')
|
|
|
|
invoice_policy = fields.Selection([
|
|
('estimated', 'Estimated cost'),
|
|
('real', 'Real cost')
|
|
], string='Invoicing Policy', default='estimated', required=True,
|
|
help="Estimated Cost: the customer will be invoiced the estimated cost of the shipping.\nReal Cost: the customer will be invoiced the real cost of the shipping, the cost of the shipping will be updated on the SO after the delivery.")
|
|
|
|
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')
|
|
|
|
margin = fields.Float(help='This percentage will be added to the shipping price.')
|
|
free_over = fields.Boolean('Free if order amount is above', help="If the order total amount (shipping excluded) is above or equal to this value, the customer benefits 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")
|
|
|
|
can_generate_return = fields.Boolean(compute="_compute_can_generate_return")
|
|
return_label_on_delivery = fields.Boolean(string="Generate Return Label", help="The return label is automatically generated at the delivery.")
|
|
get_return_label_from_portal = fields.Boolean(string="Return Label Accessible from Customer Portal", help="The return label can be downloaded by the customer from the customer portal.")
|
|
|
|
_sql_constraints = [
|
|
('margin_not_under_100_percent', 'CHECK (margin >= -100)', 'Margin cannot be lower than -100%'),
|
|
]
|
|
|
|
@api.depends('delivery_type')
|
|
def _compute_can_generate_return(self):
|
|
for carrier in self:
|
|
carrier.can_generate_return = False
|
|
|
|
def toggle_prod_environment(self):
|
|
for c in self:
|
|
c.prod_environment = not c.prod_environment
|
|
|
|
def toggle_debug(self):
|
|
for c in self:
|
|
c.debug_logging = not c.debug_logging
|
|
|
|
def install_more_provider(self):
|
|
return {
|
|
'name': 'New Providers',
|
|
'view_mode': 'kanban,form',
|
|
'res_model': 'ir.module.module',
|
|
'domain': [['name', '=like', 'delivery_%'], ['name', '!=', 'delivery_barcode']],
|
|
'type': 'ir.actions.act_window',
|
|
'help': _('''<p class="o_view_nocontent">
|
|
Buy Odoo Enterprise now to get more providers.
|
|
</p>'''),
|
|
}
|
|
|
|
def available_carriers(self, partner):
|
|
return self.filtered(lambda c: c._match_address(partner))
|
|
|
|
def _match_address(self, partner):
|
|
self.ensure_one()
|
|
if self.country_ids and partner.country_id not in self.country_ids:
|
|
return False
|
|
if self.state_ids and partner.state_id not in self.state_ids:
|
|
return False
|
|
if self.zip_from and (partner.zip or '').upper() < self.zip_from.upper():
|
|
return False
|
|
if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper():
|
|
return False
|
|
return True
|
|
|
|
@api.onchange('integration_level')
|
|
def _onchange_integration_level(self):
|
|
if self.integration_level == 'rate':
|
|
self.invoice_policy = 'estimated'
|
|
|
|
@api.onchange('can_generate_return')
|
|
def _onchange_can_generate_return(self):
|
|
if not self.can_generate_return:
|
|
self.return_label_on_delivery = False
|
|
|
|
@api.onchange('return_label_on_delivery')
|
|
def _onchange_return_label_on_delivery(self):
|
|
if not self.return_label_on_delivery:
|
|
self.get_return_label_from_portal = False
|
|
|
|
@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 for external providers #
|
|
# -------------------------- #
|
|
|
|
def rate_shipment(self, order):
|
|
''' Compute the price of the order shipment
|
|
|
|
:param order: record of sale.order
|
|
:return dict: {'success': boolean,
|
|
'price': a float,
|
|
'error_message': a string containing an error message,
|
|
'warning_message': a string containing a warning message}
|
|
# TODO maybe the currency code?
|
|
'''
|
|
self.ensure_one()
|
|
if hasattr(self, '%s_rate_shipment' % self.delivery_type):
|
|
res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order)
|
|
# apply margin on computed price
|
|
res['price'] = float(res['price']) * (1.0 + (self.margin / 100.0))
|
|
# save the real price in case a free_over rule overide it to 0
|
|
res['carrier_price'] = res['price']
|
|
# free when order is large enough
|
|
if res['success'] and self.free_over and order._compute_amount_total_without_delivery() >= self.amount:
|
|
res['warning_message'] = _('The shipping is free since the order amount exceeds %.2f.') % (self.amount)
|
|
res['price'] = 0.0
|
|
return res
|
|
|
|
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 }
|
|
# TODO missing labels per package
|
|
# TODO missing currency
|
|
# TODO missing success, error, warnings
|
|
'''
|
|
self.ensure_one()
|
|
if hasattr(self, '%s_send_shipping' % self.delivery_type):
|
|
return getattr(self, '%s_send_shipping' % self.delivery_type)(pickings)
|
|
|
|
def get_return_label(self,pickings, tracking_number=None, origin_date=None):
|
|
self.ensure_one()
|
|
if self.can_generate_return:
|
|
return getattr(self, '%s_get_return_label' % self.delivery_type)(pickings, tracking_number, origin_date)
|
|
|
|
def get_return_label_prefix(self):
|
|
return 'ReturnLabel-%s' % self.delivery_type
|
|
|
|
def get_tracking_link(self, picking):
|
|
''' Ask the tracking link to the service provider
|
|
|
|
:param picking: record of stock.picking
|
|
:return str: an URL containing the tracking link or False
|
|
'''
|
|
self.ensure_one()
|
|
if hasattr(self, '%s_get_tracking_link' % self.delivery_type):
|
|
return getattr(self, '%s_get_tracking_link' % self.delivery_type)(picking)
|
|
|
|
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)
|
|
|
|
def log_xml(self, xml_string, func):
|
|
self.ensure_one()
|
|
|
|
if self.debug_logging:
|
|
self.flush()
|
|
db_name = self._cr.dbname
|
|
|
|
# Use a new cursor to avoid rollback that could be caused by an upper method
|
|
try:
|
|
db_registry = registry(db_name)
|
|
with db_registry.cursor() as cr:
|
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
|
IrLogging = env['ir.logging']
|
|
IrLogging.sudo().create({'name': 'delivery.carrier',
|
|
'type': 'server',
|
|
'dbname': db_name,
|
|
'level': 'DEBUG',
|
|
'message': xml_string,
|
|
'path': self.delivery_type,
|
|
'func': func,
|
|
'line': 1})
|
|
except psycopg2.Error:
|
|
pass
|
|
|
|
def _get_default_custom_package_code(self):
|
|
""" Some delivery carriers require a prefix to be sent in order to use custom
|
|
packages (ie not official ones). This optional method will return it as a string.
|
|
"""
|
|
self.ensure_one()
|
|
if hasattr(self, '_%s_get_default_custom_package_code' % self.delivery_type):
|
|
return getattr(self, '_%s_get_default_custom_package_code' % self.delivery_type)()
|
|
else:
|
|
return False
|
|
|
|
# ------------------------------------------------ #
|
|
# Fixed price shipping, aka a very simple provider #
|
|
# ------------------------------------------------ #
|
|
|
|
fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price')
|
|
|
|
@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
|
|
|
|
def fixed_rate_shipment(self, order):
|
|
carrier = self._match_address(order.partner_shipping_id)
|
|
if not carrier:
|
|
return {'success': False,
|
|
'price': 0.0,
|
|
'error_message': _('Error: this delivery method is not available for this address.'),
|
|
'warning_message': False}
|
|
price = self.fixed_price
|
|
company = self.company_id or order.company_id or self.env.company
|
|
if company.currency_id and company.currency_id != order.currency_id:
|
|
price = company.currency_id._convert(price, order.currency_id, company, fields.Date.today())
|
|
return {'success': True,
|
|
'price': price,
|
|
'error_message': False,
|
|
'warning_message': False}
|
|
|
|
def fixed_send_shipping(self, pickings):
|
|
res = []
|
|
for p in pickings:
|
|
res = res + [{'exact_price': p.carrier_id.fixed_price,
|
|
'tracking_number': False}]
|
|
return res
|
|
|
|
def fixed_get_tracking_link(self, picking):
|
|
return False
|
|
|
|
def fixed_cancel_shipment(self, pickings):
|
|
raise NotImplementedError()
|