diff options
Diffstat (limited to 'addons/delivery/models/delivery_carrier.py')
| -rw-r--r-- | addons/delivery/models/delivery_carrier.py | 279 |
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() |
