summaryrefslogtreecommitdiff
path: root/addons/delivery/models/delivery_carrier.py
diff options
context:
space:
mode:
Diffstat (limited to 'addons/delivery/models/delivery_carrier.py')
-rw-r--r--addons/delivery/models/delivery_carrier.py279
1 files changed, 279 insertions, 0 deletions
diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py
new file mode 100644
index 00000000..f4ff63f9
--- /dev/null
+++ b/addons/delivery/models/delivery_carrier.py
@@ -0,0 +1,279 @@
+# -*- 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()