summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Mathot <rim@odoo.com>2015-06-24 13:44:45 +0200
committerOlivier Dony <odo@openerp.com>2015-06-24 13:54:31 +0200
commit3098ab7fd101b1bc5caa7ad8fd3200c0b5e66310 (patch)
tree7941df932e0acc7227d343430d8f29762fb008a0
parent704a11ddeefc794267a288800f75435216291abf (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__.py1
-rw-r--r--addons/delivery/__openerp__.py4
-rw-r--r--addons/delivery/delivery.py14
-rw-r--r--addons/delivery/delivery_view.xml100
-rw-r--r--addons/delivery/models/__init__.py24
-rw-r--r--addons/delivery/models/delivery_carrier.py125
-rw-r--r--addons/delivery/models/sale_order.py76
-rw-r--r--addons/delivery/models/stock_picking.py110
-rw-r--r--addons/delivery/stock.py14
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,