diff options
| author | Richard Mathot <rim@odoo.com> | 2015-06-24 13:44:45 +0200 |
|---|---|---|
| committer | Olivier Dony <odo@openerp.com> | 2015-06-24 13:54:31 +0200 |
| commit | 3098ab7fd101b1bc5caa7ad8fd3200c0b5e66310 (patch) | |
| tree | 7941df932e0acc7227d343430d8f29762fb008a0 | |
| parent | 704a11ddeefc794267a288800f75435216291abf (diff) | |
[IMP] delivery: basic pluggable support for shipping integration
Add a `delivery_type` selection field on delivery.carrier.
The default is `grid` i.e. a static pricing grid with rules
to compute the delivery costs.
Future module will however be able to extend the `delivery_type`
selection to add new keys, such as `fedex` or `ups` and provide
custom implementations for the various API methods, as described
below.
In order to add your own external provider, follow these steps:
1. Create your model MyProvider that _inherits `delivery.carrier`
2. Extend the selection field `delivery_type` with a pair
('<my_provider' , 'My Provider')
3. Implement API methods as required:
<my_provider>_get_shipping_price_from_so
<my_provider>_send_shipping
<my_provider>_open_tracking_page
<my_provider>_cancel_shipment
| -rw-r--r-- | addons/delivery/__init__.py | 1 | ||||
| -rw-r--r-- | addons/delivery/__openerp__.py | 4 | ||||
| -rw-r--r-- | addons/delivery/delivery.py | 14 | ||||
| -rw-r--r-- | addons/delivery/delivery_view.xml | 100 | ||||
| -rw-r--r-- | addons/delivery/models/__init__.py | 24 | ||||
| -rw-r--r-- | addons/delivery/models/delivery_carrier.py | 125 | ||||
| -rw-r--r-- | addons/delivery/models/sale_order.py | 76 | ||||
| -rw-r--r-- | addons/delivery/models/stock_picking.py | 110 | ||||
| -rw-r--r-- | addons/delivery/stock.py | 14 |
9 files changed, 415 insertions, 53 deletions
diff --git a/addons/delivery/__init__.py b/addons/delivery/__init__.py index d9f5b50644d..c33ce66418b 100644 --- a/addons/delivery/__init__.py +++ b/addons/delivery/__init__.py @@ -23,3 +23,4 @@ import delivery import partner import sale import stock +import models diff --git a/addons/delivery/__openerp__.py b/addons/delivery/__openerp__.py index 8a468af3431..34cee6bf5dc 100644 --- a/addons/delivery/__openerp__.py +++ b/addons/delivery/__openerp__.py @@ -29,9 +29,9 @@ Allows you to add delivery methods in sale orders and picking. ============================================================== You can define your own carrier and delivery grids for prices. When creating -invoices from picking, OpenERP is able to add and compute the shipping line. +invoices from picking, the system is able to add and compute the shipping line. """, - 'author': 'OpenERP SA', + 'author': 'Odoo SA', 'depends': ['sale_stock'], 'data': [ 'security/ir.model.access.csv', diff --git a/addons/delivery/delivery.py b/addons/delivery/delivery.py index 9728f408bb4..cf61228fc6a 100644 --- a/addons/delivery/delivery.py +++ b/addons/delivery/delivery.py @@ -33,20 +33,6 @@ class delivery_carrier(osv.osv): _name = "delivery.carrier" _description = "Carrier" - def name_get(self, cr, uid, ids, context=None): - if not len(ids): - return [] - if context is None: - context = {} - order_id = context.get('order_id',False) - if not order_id: - res = super(delivery_carrier, self).name_get(cr, uid, ids, context=context) - else: - order = self.pool.get('sale.order').browse(cr, uid, order_id, context=context) - currency = order.pricelist_id.currency_id.name or '' - res = [(r['id'], r['name']+' ('+(str(r['price']))+' '+currency+')') for r in self.read(cr, uid, ids, ['name', 'price'], context)] - return res - def get_price(self, cr, uid, ids, field_name, arg=None, context=None): res={} if context is None: diff --git a/addons/delivery/delivery_view.xml b/addons/delivery/delivery_view.xml index 98ea8641efa..12c03277263 100644 --- a/addons/delivery/delivery_view.xml +++ b/addons/delivery/delivery_view.xml @@ -34,8 +34,9 @@ <group string="General Information"> <field name="partner_id"/> <field name="product_id"/> + <field name="delivery_type"/> </group> - <group string="Pricing Information"> + <group string="Pricing Information" name='pricing_info' attrs="{'invisible':[('delivery_type','!=','grid')]}"> <field name="normal_price" attrs="{'readonly':[('use_detailed_pricelist', '=', True)]}"/> <label for="free_if_more_than"/> <div> @@ -96,7 +97,7 @@ <field name="view_mode">tree,form</field> <field name="help" type="html"> <p class="oe_view_nocontent_create"> - Click to define a new deliver method. + Click to define a new delivery method. </p><p> Each carrier (e.g. UPS) can have several delivery methods (e.g. UPS Express, UPS Standard) with a set of pricing rules attached @@ -228,24 +229,40 @@ <field name="model">stock.picking</field> <field name="inherit_id" ref="stock.view_picking_form"/> <field name="arch" type="xml"> - <xpath expr="//page[@string='Additional Info']/group[last()]" position="after"> - <separator string="Carrier Information"/> - <group> - <group> + <data> + <xpath expr="//page[@string='Additional Info']/group[last()]" position="after"> + <separator string="Carrier Information"/> + <group> + <group name='carrier_data'> <field name="carrier_id"/> - <field name="carrier_tracking_ref"/> - </group> - <group> - <label for="weight" string="Weight"/> + <field name="delivery_type" attrs="{'invisible':True}"/> + <label for="carrier_tracking_ref"/> <div> - <field name="weight" class="oe_inline"/> - <field name="weight_uom_id" nolabel="1" class="oe_inline"/> + <field name="carrier_tracking_ref" class="oe_inline"/> + <button type='object' class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','=','grid'),('delivery_type','=',False),('state','not in',('done'))]}"/> </div> - <field name="weight_net" groups="base.group_no_one"/> - <field name="number_of_packages"/> - </group> - </group> - </xpath> + <field name="carrier_price"/> + </group> + <group> + <label for="weight" string="Weight"/> + <div> + <field name="weight" class="oe_inline"/> + <field name="weight_uom_id" nolabel="1" class="oe_inline"/> + </div> + <field name="weight_net" groups="base.group_no_one"/> + <field name="number_of_packages"/> + </group> + </group> + </xpath> + <xpath expr="//h1" position="before"> + <div class="oe_right oe_button_box"> + <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','=','grid')]}" /> + </div> + </xpath> + <xpath expr="//form[@string='Transfer']/header/button[last()]" position="after"> + <button name="send_to_shipper" string="Send to Shipper" type="object" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','=','grid'),('delivery_type','=',False),('state','not in',('done'))]}"/> + </xpath> + </data> </field> </record> @@ -291,28 +308,47 @@ </field> </record> - <record id="view_order_withcarrier_form" model="ir.ui.view"> - <field name="name">delivery.sale.order_withcarrier.form.view</field> + <record id="view_order_form_with_carrier" model="ir.ui.view"> + <field name="name">delivery.sale.order.form.view.with_carrier</field> <field name="model">sale.order</field> <field name="inherit_id" ref="sale.view_order_form"/> <field name="arch" type="xml"> - <xpath expr="//group[@name='sale_total']" position="after"> - <group style="width: 65%%"> - <label for="carrier_id"/> - <div> - <field name="carrier_id" context="{'order_id':active_id or False}" class="oe_inline"/> - <button name="delivery_set" string="Add in Quote" type="object" - class="oe_edit_only" - attrs="{'invisible':['|',('carrier_id','=',False),('state','not in',('draft','sent'))]}"/> - <br/> - <label string="If you don't 'Add in Quote', the exact price will be computed when invoicing based on delivery order(s)." - class="oe_edit_only" - attrs="{'invisible':['|',('carrier_id','=',False),('state','not in',('draft','sent'))]}"/> - </div> + <data> + <xpath expr="//field[@name='payment_term']" position="after"> + <field name="id" invisible="1"/> + <label for="carrier_id"/> + <div name='carrier_selection'> + <field name="carrier_id" context="{'order_id': id}" class="oe_inline"/> + <button name="delivery_set" string="Set price" type="object" class="fa fa-arrow-right oe_link" attrs="{'invisible':['|',('carrier_id','=',False),('state','not in',('draft','sent'))]}"/> + </div> + </xpath> + <xpath expr="//div[@name='carrier_selection']" position="after"> + <field name='delivery_price' widget='monetary' options="{'currency_field': 'currency_id'}"/> + </xpath> + </data> + </field> + </record> + + <record id="view_picking_withcarrier_out_form_shipping" model="ir.ui.view"> + <field name="name">delivery.stock.picking_withcarrier.form.view.shipping</field> + <field name="model">stock.picking</field> + <field name="inherit_id" ref="delivery.view_picking_withcarrier_out_form"/> + <field name="arch" type="xml"> + <xpath expr='//group[@name="carrier_data"]' position='replace'> + <group name='carrier_data'> + <field name="carrier_id"/> + <field name="delivery_type" attrs="{'invisible':True}"/> + <label for="carrier_tracking_ref"/> + <div> + <field name="carrier_tracking_ref" class="oe_inline"/> + <button type='object' class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','=','grid'),('delivery_type','=',False),('state','not in',('done'))]}"/> + </div> + <field name="carrier_price"/> </group> </xpath> </field> </record> + </data> </openerp> diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py new file mode 100644 index 00000000000..a023a5ca67b --- /dev/null +++ b/addons/delivery/models/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Business Applications +# Copyright (c) 2015 Odoo S.A. <http://openerp.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +import delivery_carrier +import sale_order +import stock_picking diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py new file mode 100644 index 00000000000..1a59f182fcb --- /dev/null +++ b/addons/delivery/models/delivery_carrier.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Business Applications +# Copyright (c) 2015 Odoo S.A. <http://openerp.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +import logging +from openerp import models, fields, api +from openerp.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class DeliveryCarrier(models.Model): + _inherit = 'delivery.carrier' + ''' 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 # + # -------------------------------- # + + delivery_type = fields.Selection([('grid', 'Default Delivery')], string="Delivery Type", default='grid', required='True') + + price = fields.Float(compute='get_price') + available = fields.Boolean(compute='get_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 + + if self.delivery_type != 'grid': + try: + order = SaleOrder.browse(order_id) + self.price = self.get_shipping_price_from_so(order)[0] + self.available = True + except UserError as e: + # no suitable delivery method found, probably configuration error + _logger.info("Carrier %s: %s, not found", self.name, e.name) + self.price = 0.0 + else: + res = super(DeliveryCarrier, self).get_price('price', []) + self.available = res[self.id]['available'] + self.price = res[self.id]['price'] + + # -------------------------- # + # 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) diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py new file mode 100644 index 00000000000..b635ec7d801 --- /dev/null +++ b/addons/delivery/models/sale_order.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Business Applications +# Copyright (c) 2015 Odoo S.A. <http://openerp.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +from openerp import models, fields, api, _ +from openerp.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + delivery_price = fields.Float(string='Estimated Delivery Price', compute='_compute_delivery_price') + + @api.depends('carrier_id', 'partner_id', 'order_line') + def _compute_delivery_price(self): + for order in self: + order.delivery_price = order.carrier_id.with_context(order_id=order.id).price + + @api.multi + def delivery_set(self): + + SaleOrderLine = self.env['sale.order.line'] + + # Remove delivery products from the sale order + self._delivery_unset() + + for order in self: + if order.carrier_id and order.carrier_id.delivery_type != 'grid': + # Shipping providers are used when delivery_type is other than 'grid' + + if order.state not in ('draft', 'sent'): + raise UserError(_('The order state have to be draft to add delivery lines.')) + + carrier = order.carrier_id + account_id = carrier.product_id.property_account_income.id + if not account_id: + account_id = carrier.product_id.categ_id.property_account_income_categ.id + + # Apply fiscal position + taxes = carrier.product_id.taxes_id + taxes_ids = taxes.ids + if order.partner_id and order.fiscal_position: + account_id = order.fiscal_position.map_account(account_id) + taxes_ids = order.fiscal_position.map_tax(taxes).ids + + SaleOrderLine.create({ + 'order_id': order.id, + 'name': order.carrier_id.name, + 'product_uom_qty': 1, + 'product_uom': order.carrier_id.product_id.uom_id.id, + 'product_id': order.carrier_id.product_id.id, + 'price_unit': order.carrier_id.get_shipping_price_from_so(order)[0], + 'tax_id': [(6, 0, taxes_ids)], + 'is_delivery': True + }) + + else: + # Classic grid-based carriers + super(SaleOrder, self).delivery_set() diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py new file mode 100644 index 00000000000..ee9b2debc3c --- /dev/null +++ b/addons/delivery/models/stock_picking.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Odoo, Open Source Business Applications +# Copyright (c) 2015 Odoo S.A. <http://openerp.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +############################################################################## + +from openerp import models, fields, api + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + carrier_price = fields.Float(string="Shipping Cost", readonly=True) + delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True) + + @api.multi + def do_transfer(self): + res = super(StockPicking, self).do_transfer() + + if self.carrier_id and self.carrier_id.delivery_type != 'grid': + self.send_to_shipper() + return res + + # Signature due to strange old api methods + @api.model + def _prepare_shipping_invoice_line(self, picking, invoice): + picking.ensure_one() + invoice.ensure_one() + + carrier = picking.carrier_id + + # No carrier + if not carrier: + return None + # Carrier already invoiced on the sale order + if any(inv_line.product_id.id == carrier.product_id.id for inv_line in invoice.invoice_line): + return None + + # Classic carrier + if carrier.delivery_type == 'grid': + return super(StockPicking, self)._prepare_shipping_invoice_line(picking, invoice) + + # Shipping provider + price = picking.carrier_price + + account_id = carrier.product_id.property_account_income.id + if not account_id: + account_id = carrier.product_id.categ_id.property_account_income_categ.id + + taxes = carrier.product_id.taxes_id + taxes_ids = taxes.ids + + # Apply original SO fiscal position + if picking.sale_id.fiscal_position: + fpos = picking.sale_id.fiscal_position + account_id = fpos.map_account(account_id) + taxes_ids = fpos.map_tax(taxes).ids + + res = { + 'name': carrier.name, + 'invoice_id': invoice.id, + 'uos_id': carrier.product_id.uos_id.id, + 'product_id': carrier.product_id.id, + 'account_id': account_id, + 'price_unit': price, + 'quantity': 1, + 'invoice_line_tax_id': [(6, 0, taxes_ids)], + } + + return res + + @api.one + def send_to_shipper(self): + res = self.carrier_id.send_shipping(self)[0] + self.carrier_price = res['exact_price'] + self.carrier_tracking_ref = res['tracking_number'] + msg = "Shipment sent to carrier %s for expedition with tracking number %s" % (self.carrier_id.name, self.carrier_tracking_ref) + self.message_post(body=msg) + + @api.multi + def open_website_url(self): + self.ensure_one() + + client_action = {'type': 'ir.actions.act_url', + 'name': "Shipment Tracking Page", + 'target': 'new', + 'url': self.carrier_id.get_tracking_link(self)[0] + } + return client_action + + @api.one + def cancel_shipment(self): + self.carrier_id.cancel_shipment(self) + msg = "Shipment %s cancelled" % self.carrier_tracking_ref + self.message_post(body=msg) + self.carrier_tracking_ref = False diff --git a/addons/delivery/stock.py b/addons/delivery/stock.py index f2520965879..6f37e0ad4da 100644 --- a/addons/delivery/stock.py +++ b/addons/delivery/stock.py @@ -82,6 +82,7 @@ class stock_picking(osv.osv): """ carrier_obj = self.pool.get('delivery.carrier') grid_obj = self.pool.get('delivery.grid') + fpos_obj = self.pool['account.fiscal.position'] if not picking.carrier_id or \ any(inv_line.product_id.id == picking.carrier_id.product_id.id for inv_line in invoice.invoice_line): @@ -100,12 +101,15 @@ class stock_picking(osv.osv): .property_account_income_categ.id taxes = picking.carrier_id.product_id.taxes_id + taxes_ids = [x.id for x in taxes] partner = picking.partner_id or False - if partner: - account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, partner.property_account_position, account_id) - taxes_ids = self.pool.get('account.fiscal.position').map_tax(cr, uid, partner.property_account_position, taxes) - else: - taxes_ids = [x.id for x in taxes] + fpos = None + if picking.sale_id and picking.sale_id.fiscal_position: + fpos = picking.sale_id.fiscal_position + elif picking.partner_id: + fpos = partner.property_account_position + account_id = fpos_obj.map_account(cr, uid, fpos, account_id, context=context) + taxes_ids = fpos_obj.map_tax(cr, uid, fpos, taxes, context=context) return { 'name': picking.carrier_id.name, |
