diff options
| author | Mahendra barad <mba@openerp.com> | 2015-06-19 15:26:26 +0530 |
|---|---|---|
| committer | David Monjoie <dmo@odoo.com> | 2015-09-01 08:14:16 +0200 |
| commit | a223111b922d3c3d8957f894bf20a156eb96d324 (patch) | |
| tree | 00589dd65e96dbc4172364c101ebdf920b5bad45 | |
| parent | bb1200adc781efc1318e502a84cc1e77315aabc4 (diff) | |
[IMP] delivery, website_sale_delivery: refactoring and usability
* migration to new API
* removed delivery.grid object, has been merged with delivery.carrier
* change view of delivery.carrier (price rules, etc.)
* tests from yml to py
* website : restrict the list of countries for delivery address in the shop
33 files changed, 776 insertions, 1009 deletions
diff --git a/addons/delivery/__init__.py b/addons/delivery/__init__.py index 02d8c7c70af..81fc81d29c9 100644 --- a/addons/delivery/__init__.py +++ b/addons/delivery/__init__.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import delivery -import partner -import sale -import stock import models diff --git a/addons/delivery/__openerp__.py b/addons/delivery/__openerp__.py index 85823d5449d..efa69dbfdf5 100644 --- a/addons/delivery/__openerp__.py +++ b/addons/delivery/__openerp__.py @@ -10,25 +10,22 @@ Allows you to add delivery methods in sale orders and picking. ============================================================== -You can define your own carrier and delivery grids for prices. When creating +You can define your own carrier for prices. When creating invoices from picking, the system is able to add and compute the shipping line. """, 'depends': ['sale_stock'], 'data': [ 'security/ir.model.access.csv', - 'delivery_view.xml', - 'partner_view.xml', - 'delivery_data.xml', + 'views/delivery_view.xml', + 'views/partner_view.xml', + 'data/delivery_data.xml', 'views/report_shipping.xml', 'views/report_deliveryslip.xml' ], - 'demo': ['delivery_demo.xml'], + 'demo': ['data/delivery_demo.xml'], 'test': [ '../account/test/account_minimal_test.xml', - 'test/delivery_cost.yml', - 'test/stock_move_values_with_invoice_before_delivery.yml', ], 'installable': True, - 'auto_install': False, } diff --git a/addons/delivery/delivery_data.xml b/addons/delivery/data/delivery_data.xml index d567a8aff86..a26475af993 100644 --- a/addons/delivery/delivery_data.xml +++ b/addons/delivery/data/delivery_data.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> +<odoo> <data noupdate="1"> - <!-- Create a transporter --> <record id="res_partner_delivery" model="res.partner"> <field name="name">Transporter (Delivery provider)</field> @@ -10,31 +9,21 @@ <field name="customer" eval="False"/> </record> - <!-- Create a service product --> - <record id="product_product_delivery" model="product.product"> - <field name="name">Standard delivery</field> - <field name="default_code">Delivery</field> - <field name="type">service</field> - <field name="categ_id" ref="product.product_category_all"/> - <field name="standard_price">10.0</field> - <field name="list_price">10.0</field> - <field name="image" type="base64" file="delivery/static/img/product_product_delivery-image.jpg"/> - </record> - <record id="free_delivery_carrier" model="delivery.carrier"> <field name="name">Free delivery charges</field> - <field name="normal_price">0</field> + <field name="fixed_price">0</field> <field name="free_if_more_than" eval="True"/> <field name="amount">1000</field> <field name="sequence">1</field> + <field name="delivery_type">fixed</field> + <field name="product_type">service</field> + <field name="product_sale_ok" eval="False"/> <field name="partner_id" ref="res_partner_delivery"/> - <field name="product_id" ref="product_product_delivery"/> </record> <record forcecreate="True" id="property_delivery_carrier" model="ir.property"> - <field name="name">property_delivery_carrier</field> - <field name="fields_id" search="[('model','=','res.partner'),('name','=','property_delivery_carrier')]"/> + <field name="name">property_delivery_carrier_id</field> + <field name="fields_id" search="[('model','=','res.partner'),('name','=','property_delivery_carrier_id')]"/> </record> - </data> -</openerp> +</odoo> diff --git a/addons/delivery/delivery_demo.xml b/addons/delivery/data/delivery_demo.xml index 93a1957170c..44b693baada 100644 --- a/addons/delivery/delivery_demo.xml +++ b/addons/delivery/data/delivery_demo.xml @@ -1,7 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> -<openerp> +<odoo> <data noupdate="1"> - <!-- Update a transporter --> <record id="res_partner_delivery" model="res.partner"> <field name="city">Manchester</field> @@ -14,39 +13,33 @@ <record id="delivery_carrier" model="delivery.carrier"> <field name="name">The Poste</field> - <field name="normal_price">20</field> + <field name="fixed_price">20</field> <field name="sequence">2</field> <field name="partner_id" ref="res_partner_delivery"/> - <field name="product_id" ref="product_product_delivery"/> - <field name="use_detailed_pricelist" eval="True"/> + <field name="delivery_type">base_on_rule</field> + <field name="product_type">service</field> + <field name="product_sale_ok" eval="False"/> </record> <record id="normal_delivery_carrier" model="delivery.carrier"> <field name="name">Normal Delivery Charges</field> - <field name="normal_price">10</field> + <field name="fixed_price">10</field> <field name="sequence">3</field> <field name="partner_id" ref="res_partner_delivery"/> - <field name="product_id" ref="product_product_delivery"/> + <field name="delivery_type">fixed</field> + <field name="product_type">service</field> + <field name="product_sale_ok" eval="False"/> </record> - <!-- Carrier Grids --> - - <record id="delivery_grid" model="delivery.grid"> - <field name="name">The Poste - Pricelist</field> + <record id="delivery_price_rule1" model="delivery.price.rule"> <field name="carrier_id" ref="delivery_carrier"/> - </record> - <!-- delivery charge of product if weight less than and equal 5kg--> - <record id="delivery_grid_line1" model="delivery.grid.line"> - <field name="name">Weight <= 5kg</field> - <field name="grid_id" ref="delivery_grid"/> <field eval="5" name="max_value"/> <field eval="20" name="list_base_price"/> <field eval="10" name="standard_price"/> </record> <!-- delivery charge of product if weight more than 5kg--> - <record id="delivery_grid_line2" model="delivery.grid.line"> - <field name="name">Weight > 5kg</field> - <field name="grid_id" ref="delivery_grid"/> + <record id="delivery_price_rule2" model="delivery.price.rule"> + <field name="carrier_id" ref="delivery_carrier"/> <field name="operator">>=</field> <field eval="5" name="max_value"/> <field eval="50" name="list_base_price"/> @@ -54,20 +47,19 @@ </record> <!-- free delivery charge if price more than 300--> - <record id="delivery_grid_line3" model="delivery.grid.line"> - <field name="name">Free if price >= 300</field> - <field name="grid_id" ref="delivery_grid"/> + <record id="delivery_price_rule3" model="delivery.price.rule"> + <field name="carrier_id" ref="delivery_carrier"/> <field eval="300" name="max_value"/> <field name="operator">>=</field> - <field name="type">price</field> + <field name="variable">price</field> <field eval="0" name="list_base_price"/> <field eval="0" name="standard_price"/> </record> <record forcecreate="True" id="property_delivery_carrier" model="ir.property"> - <field name="name">property_delivery_carrier</field> - <field name="fields_id" search="[('model','=','res.partner'),('name','=','property_delivery_carrier')]"/> + <field name="name">property_delivery_carrier_id</field> + <field name="fields_id" search="[('model','=','res.partner'),('name','=','property_delivery_carrier_id')]"/> <field name="value" eval="'delivery.carrier,'+str(ref('normal_delivery_carrier'))"/> </record> </data> -</openerp> +</odoo> diff --git a/addons/delivery/delivery.py b/addons/delivery/delivery.py deleted file mode 100644 index e508db3e282..00000000000 --- a/addons/delivery/delivery.py +++ /dev/null @@ -1,244 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import logging -import time -from openerp.osv import fields,osv -from openerp.tools.translate import _ -import openerp.addons.decimal_precision as dp -from openerp.tools.safe_eval import safe_eval as eval -from openerp.exceptions import UserError - -_logger = logging.getLogger(__name__) - -class delivery_carrier(osv.osv): - _name = "delivery.carrier" - _description = "Carrier" - _order = 'sequence, id' - - def get_price(self, cr, uid, ids, field_name, arg=None, context=None): - res={} - if context is None: - context = {} - sale_obj=self.pool.get('sale.order') - grid_obj=self.pool.get('delivery.grid') - for carrier in self.browse(cr, uid, ids, context=context): - order_id=context.get('order_id',False) - price=False - available = False - if order_id: - order = sale_obj.browse(cr, uid, order_id, context=context) - carrier_grid=self.grid_get(cr,uid,[carrier.id],order.partner_shipping_id.id,context) - if carrier_grid: - try: - price=grid_obj.get_price(cr, uid, carrier_grid, order, time.strftime('%Y-%m-%d'), context) - available = True - except UserError, e: - # no suitable delivery method found, probably configuration error - _logger.info("Carrier %s: %s", carrier.name, e.name) - price = 0.0 - else: - price = 0.0 - res[carrier.id] = { - 'price': price, - 'available': available - } - return res - - _columns = { - 'name': fields.char('Delivery Method', required=True, translate=True), - 'sequence': fields.integer('Sequence', help="Determine the display order"), - 'partner_id': fields.many2one('res.partner', 'Transport Company', required=True, help="The partner that is doing the delivery service."), - 'product_id': fields.many2one('product.product', 'Delivery Product', required=True), - 'grids_id': fields.one2many('delivery.grid', 'carrier_id', 'Delivery Grids'), - 'available' : fields.function(get_price, string='Available',type='boolean', multi='price', - help="Is the carrier method possible with the current order."), - 'price' : fields.function(get_price, string='Price', multi='price'), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the delivery carrier without removing it."), - 'normal_price': fields.float('Normal Price', help="Keep empty if the pricing depends on the advanced pricing per destination"), - 'free_if_more_than': fields.boolean('Free If Order Total Amount Is More Than', help="If the order is more expensive than a certain amount, the customer can benefit from a free shipping"), - 'amount': fields.float('Amount', help="Amount of the order to benefit from a free shipping, expressed in the company currency"), - 'use_detailed_pricelist': fields.boolean('Advanced Pricing per Destination', help="Check this box if you want to manage delivery prices that depends on the destination, the weight, the total of the order, etc."), - 'pricelist_ids': fields.one2many('delivery.grid', 'carrier_id', 'Advanced Pricing'), - } - - _defaults = { - 'active': 1, - 'free_if_more_than': False, - 'sequence': 10, - } - - def grid_get(self, cr, uid, ids, contact_id, context=None): - contact = self.pool.get('res.partner').browse(cr, uid, contact_id, context=context) - for carrier in self.browse(cr, uid, ids, context=context): - for grid in carrier.grids_id: - get_id = lambda x: x.id - country_ids = map(get_id, grid.country_ids) - state_ids = map(get_id, grid.state_ids) - if country_ids and not contact.country_id.id in country_ids: - continue - if state_ids and not contact.state_id.id in state_ids: - continue - if grid.zip_from and (contact.zip or '')< grid.zip_from: - continue - if grid.zip_to and (contact.zip or '')> grid.zip_to: - continue - return grid.id - return False - - def create_grid_lines(self, cr, uid, ids, vals, context=None): - if context is None: - context = {} - grid_line_pool = self.pool.get('delivery.grid.line') - grid_pool = self.pool.get('delivery.grid') - for record in self.browse(cr, uid, ids, context=context): - # if using advanced pricing per destination: do not change - if record.use_detailed_pricelist: - continue - - # not using advanced pricing per destination: override grid - grid_id = grid_pool.search(cr, uid, [('carrier_id', '=', record.id)], context=context) - if grid_id and not (record.normal_price is not False or record.free_if_more_than): - grid_pool.unlink(cr, uid, grid_id, context=context) - grid_id = None - - if not (record.normal_price is not False or record.free_if_more_than): - continue - - if not grid_id: - grid_data = { - 'name': record.name, - 'carrier_id': record.id, - 'sequence': 10, - } - grid_id = [grid_pool.create(cr, uid, grid_data, context=context)] - - lines = grid_line_pool.search(cr, uid, [('grid_id','in',grid_id)], context=context) - if lines: - grid_line_pool.unlink(cr, uid, lines, context=context) - - #create the grid lines - if record.free_if_more_than: - line_data = { - 'grid_id': grid_id and grid_id[0], - 'name': _('Free if more than %.2f') % record.amount, - 'type': 'price', - 'operator': '>=', - 'max_value': record.amount, - 'standard_price': 0.0, - 'list_base_price': 0.0, - } - grid_line_pool.create(cr, uid, line_data, context=context) - if record.normal_price is not False: - line_data = { - 'grid_id': grid_id and grid_id[0], - 'name': _('Default price'), - 'type': 'price', - 'operator': '>=', - 'max_value': 0.0, - 'standard_price': record.normal_price, - 'list_base_price': record.normal_price, - } - grid_line_pool.create(cr, uid, line_data, context=context) - return True - - def write(self, cr, uid, ids, vals, context=None): - if isinstance(ids, (int,long)): - ids = [ids] - res = super(delivery_carrier, self).write(cr, uid, ids, vals, context=context) - self.create_grid_lines(cr, uid, ids, vals, context=context) - return res - - def create(self, cr, uid, vals, context=None): - res_id = super(delivery_carrier, self).create(cr, uid, vals, context=context) - self.create_grid_lines(cr, uid, [res_id], vals, context=context) - return res_id - - -class delivery_grid(osv.osv): - _name = "delivery.grid" - _description = "Delivery Grid" - _columns = { - 'name': fields.char('Grid Name', required=True), - 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when displaying a list of delivery grid."), - 'carrier_id': fields.many2one('delivery.carrier', 'Carrier', required=True, ondelete='cascade'), - 'country_ids': fields.many2many('res.country', 'delivery_grid_country_rel', 'grid_id', 'country_id', 'Countries'), - 'state_ids': fields.many2many('res.country.state', 'delivery_grid_state_rel', 'grid_id', 'state_id', 'States'), - 'zip_from': fields.char('Start Zip', size=12), - 'zip_to': fields.char('To Zip', size=12), - 'line_ids': fields.one2many('delivery.grid.line', 'grid_id', 'Grid Line', copy=True), - 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the delivery grid without removing it."), - } - _defaults = { - 'active': lambda *a: 1, - 'sequence': lambda *a: 1, - } - _order = 'sequence' - - def get_price(self, cr, uid, id, order, dt, context=None): - total = 0 - weight = 0 - volume = 0 - quantity = 0 - total_delivery = 0.0 - product_uom_obj = self.pool.get('product.uom') - for line in order.order_line: - if line.state == 'cancel': - continue - if line.is_delivery: - total_delivery += line.price_total - if not line.product_id or line.is_delivery: - continue - q = product_uom_obj._compute_qty(cr, uid, line.product_uom.id, line.product_uom_qty, line.product_id.uom_id.id) - weight += (line.product_id.weight or 0.0) * q - volume += (line.product_id.volume or 0.0) * q - quantity += q - total = (order.amount_total or 0.0) - total_delivery - - return self.get_price_from_picking(cr, uid, id, total,weight, volume, quantity, context=context) - - def get_price_from_picking(self, cr, uid, id, total, weight, volume, quantity, context=None): - grid = self.browse(cr, uid, id, context=context) - price = 0.0 - ok = False - price_dict = {'price': total, 'volume':volume, 'weight': weight, 'wv':volume*weight, 'quantity': quantity} - for line in grid.line_ids: - test = eval(line.type+line.operator+str(line.max_value), price_dict) - if test: - price = line.list_base_price + line.list_price * price_dict[line.variable_factor] - ok = True - break - if not ok: - raise UserError(_("Selected product in the delivery method doesn't fulfill any of the delivery grid(s) criteria.")) - - return price - - -class delivery_grid_line(osv.osv): - _name = "delivery.grid.line" - _description = "Delivery Grid Line" - _columns = { - 'name': fields.char('Name', required=True), - 'sequence': fields.integer('Sequence', required=True, help="Gives the sequence order when calculating delivery grid."), - 'grid_id': fields.many2one('delivery.grid', 'Grid',required=True, ondelete='cascade'), - 'type': fields.selection([('weight','Weight'),('volume','Volume'),\ - ('wv','Weight * Volume'), ('price','Price'), ('quantity','Quantity')],\ - 'Variable', required=True), - 'operator': fields.selection([('==','='),('<=','<='),('<','<'),('>=','>='),('>','>')], 'Operator', required=True), - 'max_value': fields.float('Maximum Value', required=True), - 'variable_factor': fields.selection([('weight','Weight'),('volume','Volume'),('wv','Weight * Volume'), ('price','Price'), ('quantity','Quantity')], 'Variable Factor', required=True), - 'list_base_price': fields.float('Sale Base Price', digits_compute=dp.get_precision('Product Price'), required=True), - 'list_price': fields.float('Sale Price', digits_compute= dp.get_precision('Product Price'), required=True), - 'standard_price': fields.float('Shipper Price', digits_compute= dp.get_precision('Product Price'), required=True), - } - - _defaults = { - 'sequence': lambda *args: 10, - 'type': lambda *args: 'weight', - 'operator': lambda *args: '<=', - 'variable_factor': lambda *args: 'weight', - 'list_price': 0.0, - 'list_base_price': 0.0, - 'standard_price': 0.0, - } - _order = 'sequence, list_price' diff --git a/addons/delivery/models/__init__.py b/addons/delivery/models/__init__.py index a023a5ca67b..d593c2c3957 100644 --- a/addons/delivery/models/__init__.py +++ b/addons/delivery/models/__init__.py @@ -1,24 +1,10 @@ # -*- 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/>. -# -############################################################################## +# Part of Odoo. See LICENSE file for full copyright and licensing details. import delivery_carrier +import delivery_price_rule +import product_template import sale_order +import partner import stock_picking +import stock_move diff --git a/addons/delivery/models/delivery_carrier.py b/addons/delivery/models/delivery_carrier.py index 1a59f182fcb..e9031a01390 100644 --- a/addons/delivery/models/delivery_carrier.py +++ b/addons/delivery/models/delivery_carrier.py @@ -1,33 +1,19 @@ # -*- 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/>. -# -############################################################################## +# Part of Odoo. See LICENSE file for full copyright and licensing details. import logging -from openerp import models, fields, api +from openerp import api, fields, models, _ from openerp.exceptions import UserError _logger = logging.getLogger(__name__) class DeliveryCarrier(models.Model): - _inherit = 'delivery.carrier' + _name = 'delivery.carrier' + _inherits = {'product.product': 'product_id'} + _description = "Carrier" + _order = 'sequence, id' + ''' A Shipping Provider In order to add your own external provider, follow these steps: @@ -47,10 +33,40 @@ class DeliveryCarrier(models.Model): # Internals for shipping providers # # -------------------------------- # - delivery_type = fields.Selection([('grid', 'Default Delivery')], string="Delivery Type", default='grid', required='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'), ('base_on_rule', 'Based on Rules')], string='Price Computation', default='fixed', required=True) + product_type = fields.Selection(related='product_id.type', default='service') + product_sale_ok = fields.Boolean(related='product_id.sale_ok', default=False) + partner_id = fields.Many2one('res.partner', string='Transporter Company', required=True, help="The partner that is doing the delivery service.") + product_id = fields.Many2one('product.product', string='Delivery Product', required=True, ondelete="cascade") price = fields.Float(compute='get_price') available = fields.Boolean(compute='get_price') + free_if_more_than = fields.Boolean('Free if Order total is more than', help="If the order is more expensive than a certain amount, the customer can benefit 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") + 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') + price_rule_ids = fields.One2many('delivery.price.rule', 'carrier_id', 'Pricing Rules', copy=True) + state = fields.Selection([('test', 'Testing'), ('production', 'Production')], 'State') + fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price',help="Keep empty if the pricing depends on the advanced pricing per destination") + + @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 + + @api.onchange('delivery_type') + def onchange_delivery_type(self): + if self.delivery_type in ["fixed", "base_on_rule"]: + self.state = 'production' + else: + self.state = 'test' @api.one def get_price(self): @@ -63,19 +79,27 @@ class DeliveryCarrier(models.Model): if order_id: # FIXME: temporary hack until we refactor the delivery API in master - if self.delivery_type != 'grid': + order = SaleOrder.browse(order_id) + if self.delivery_type not in ['fixed', 'base_on_rule']: 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 + # 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'] + carrier = self.verify_carrier(order.partner_shipping_id) + if carrier: + try: + self.price = carrier.get_price_available(order) + self.available = True + except UserError, e: + # No suitable delivery method found, probably configuration error + _logger.info("Carrier %s: %s", carrier.name, e.name) + self.price = 0.0 + else: + self.price = 0.0 # -------------------------- # # API for external providers # @@ -123,3 +147,113 @@ class DeliveryCarrier(models.Model): self.ensure_one() if hasattr(self, '%s_cancel_shipment' % self.delivery_type): return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) + + @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.multi + def verify_carrier(self, contact): + self.ensure_one() + if self.country_ids and contact.country_id not in self.country_ids: + return False + if self.state_ids and contact.state_id not in self.state_ids: + return False + if self.zip_from and (contact.zip or '') < self.zip_from: + return False + if self.zip_to and (contact.zip or '') > self.zip_to: + return False + return self + + @api.multi + def create_price_rules(self): + PriceRule = self.env['delivery.price.rule'] + for record in self: + # If using advanced pricing per destination: do not change + if record.delivery_type == 'base_on_rule': + continue + + # Not using advanced pricing per destination: override lines + if record.delivery_type == 'base_on_rule' and not (record.fixed_price is not False or record.free_if_more_than): + record.price_rule_ids.unlink() + + # Check that float, else 0.0 is False + if not (record.fixed_price is not False or record.free_if_more_than): + continue + + if record.delivery_type == 'fixed': + PriceRule.search([('carrier_id', '=', record.id)]).unlink() + + line_data = { + 'carrier_id': record.id, + 'variable': 'price', + 'operator': '>=', + } + # Create the delivery price rules + if record.free_if_more_than: + line_data.update({ + 'max_value': record.amount, + 'standard_price': 0.0, + 'list_base_price': 0.0, + }) + PriceRule.create(line_data) + if record.fixed_price is not False: + line_data.update({ + 'max_value': 0.0, + 'standard_price': record.fixed_price, + 'list_base_price': record.fixed_price, + }) + PriceRule.create(line_data) + return True + + @api.model + def create(self, vals): + res = super(DeliveryCarrier, self).create(vals) + res.create_price_rules() + return res + + @api.multi + def write(self, vals): + res = super(DeliveryCarrier, self).write(vals) + self.create_price_rules() + return res + + @api.multi + def get_price_available(self, order): + self.ensure_one() + total = weight = volume = quantity = 0 + total_delivery = 0.0 + ProductUom = self.env['product.uom'] + for line in order.order_line: + if line.state == 'cancel': + continue + if line.is_delivery: + total_delivery += line.price_total + if not line.product_id or line.is_delivery: + continue + qty = ProductUom._compute_qty(line.product_uom.id, line.product_uom_qty, line.product_id.uom_id.id) + weight += (line.product_id.weight or 0.0) * qty + volume += (line.product_id.volume or 0.0) * qty + quantity += qty + total = (order.amount_total or 0.0) - total_delivery + + return self.get_price_from_picking(total, weight, volume, quantity) + + def get_price_from_picking(self, total, weight, volume, quantity): + price = 0.0 + criteria_found = False + price_dict = {'price': total, 'volume': volume, 'weight': weight, 'wv': volume * weight, 'quantity': quantity} + for line in self.price_rule_ids: + test = eval(line.variable + line.operator + str(line.max_value), price_dict) + if test: + price = line.list_base_price + line.list_price * price_dict[line.variable_factor] + criteria_found = True + break + if not criteria_found: + raise UserError(_("Selected product in the delivery method doesn't fulfill any of the delivery carrier(s) criteria.")) + + return price diff --git a/addons/delivery/models/delivery_price_rule.py b/addons/delivery/models/delivery_price_rule.py new file mode 100644 index 00000000000..1d0dc49f031 --- /dev/null +++ b/addons/delivery/models/delivery_price_rule.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import models, fields, api +import openerp.addons.decimal_precision as dp + + +class PriceRule(models.Model): + _name = "delivery.price.rule" + _description = "Delivery Price Rules" + _order = 'sequence, list_price' + + @api.depends('variable', 'operator', 'max_value', 'list_base_price', 'list_price', 'variable_factor') + def _get_name(self): + for rule in self: + name = 'if %s %s %s then' % (rule.variable, rule.operator, rule.max_value) + if rule.list_base_price and not rule.list_price: + name = '%s fixed price %s' % (name, rule.list_base_price) + elif rule.list_price and not rule.list_base_price: + name = '%s %s times %s' % (name, rule.list_price, rule.variable_factor) + else: + name = '%s fixed price %s and %s times %s Extra' % (name, rule.list_base_price, rule.list_price, rule.variable_factor) + rule.name = name + + name = fields.Char(compute='_get_name') + sequence = fields.Integer(required=True, help="Gives the sequence order when calculating delivery carrier.", default=10) + carrier_id = fields.Many2one('delivery.carrier', 'Carrier', required=True, ondelete='cascade') + variable = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable', required=True, default='weight') + operator = fields.Selection([('==', '='), ('<=', '<='), ('<', '<'), ('>=', '>='), ('>', '>')], 'Operator', required=True, default='<=') + max_value = fields.Float('Maximum Value', required=True) + variable_factor = fields.Selection([('weight', 'Weight'), ('volume', 'Volume'), ('wv', 'Weight * Volume'), ('price', 'Price'), ('quantity', 'Quantity')], 'Variable Factor', required=True, default='weight') + list_base_price = fields.Float(string='Sale Base Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0) + list_price = fields.Float('Sale Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0) + standard_price = fields.Float('Cost Price', digits_compute=dp.get_precision('Product Price'), required=True, default=0.0) diff --git a/addons/delivery/models/partner.py b/addons/delivery/models/partner.py new file mode 100644 index 00000000000..ca558b98216 --- /dev/null +++ b/addons/delivery/models/partner.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import fields, models + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + property_delivery_carrier_id = fields.Many2one('delivery.carrier', company_dependent=True, string="Delivery Method", help="This delivery method will be used when invoicing from picking.") diff --git a/addons/delivery/models/product_template.py b/addons/delivery/models/product_template.py new file mode 100644 index 00000000000..228dea5f149 --- /dev/null +++ b/addons/delivery/models/product_template.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + @api.multi + def write(self, vals): + res = super(ProductTemplate, self).write(vals) + if vals.get('list_price'): + DeliveryCarrier = self.env['delivery.carrier'] + for template in self: + carrier = DeliveryCarrier.search([('product_id', 'in', template.product_variant_ids.ids)]) + if carrier: + carrier.create_price_rules() + return res diff --git a/addons/delivery/models/sale_order.py b/addons/delivery/models/sale_order.py index 2fbf68d86e7..36f5b24c5b8 100644 --- a/addons/delivery/models/sale_order.py +++ b/addons/delivery/models/sale_order.py @@ -1,23 +1,5 @@ # -*- 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/>. -# -############################################################################## +# Part of Odoo. See LICENSE file for full copyright and licensing details. from openerp import models, fields, api, _ from openerp.exceptions import UserError @@ -27,19 +9,29 @@ class SaleOrder(models.Model): _inherit = 'sale.order' delivery_price = fields.Float(string='Estimated Delivery Price', compute='_compute_delivery_price', store=True) + carrier_id = fields.Many2one("delivery.carrier", string="Delivery Method", help="Fill this field if you plan to invoice the shipping based on picking.") @api.depends('carrier_id', 'partner_id', 'order_line') def _compute_delivery_price(self): for order in self: if order.state != 'draft': - # we do not want to recompute the shipping price of an already validated/done SO + # We do not want to recompute the shipping price of an already validated/done SO continue elif order.carrier_id.delivery_type != 'grid' and not order.order_line: - # prevent SOAP call to external shipping provider when SO has no lines yet + # Prevent SOAP call to external shipping provider when SO has no lines yet continue else: order.delivery_price = order.carrier_id.with_context(order_id=order.id).price + @api.onchange('partner_id') + def onchange_partner_id_dtype(self): + if self.partner_id: + self.carrier_id = self.partner_id.property_delivery_carrier_id + + @api.multi + def _delivery_unset(self): + self.env['sale.order.line'].search([('order_id', 'in', self.ids), ('is_delivery', '=', True)]).unlink() + @api.multi def delivery_set(self): @@ -49,16 +41,26 @@ class SaleOrder(models.Model): 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' - + carrier = order.carrier_id + if carrier: 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 carrier.delivery_type not in ['fixed', 'base_on_rule']: + # Shipping providers are used when delivery_type is other than 'fixed' or 'base_on_rule' + price_unit = order.carrier_id.get_shipping_price_from_so(order)[0] + else: + # Classic grid-based carriers + carrier = order.carrier_id.verify_carrier(order.partner_shipping_id) + if not carrier: + raise UserError(_('No carrier matching.')) + price_unit = carrier.get_price_available(order) + if order.company_id.currency_id.id != order.pricelist_id.currency_id.id: + price_unit = order.company_id.currency_id.with_context(date=order.date_order).compute(order.pricelist_id.currency_id.id, price_unit) + + account_id = carrier.product_id.property_account_income_id.id if not account_id: - account_id = carrier.product_id.categ_id.property_account_income_categ.id + account_id = carrier.product_id.categ_id.property_account_income_categ_id.id # Apply fiscal position taxes = carrier.product_id.taxes_id @@ -67,17 +69,23 @@ class SaleOrder(models.Model): account_id = order.fiscal_position_id.map_account(account_id) taxes_ids = order.fiscal_position_id.map_tax(taxes).ids + # Create the sale order line SaleOrderLine.create({ 'order_id': order.id, - 'name': order.carrier_id.name, + 'name': carrier.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], + 'product_uom': carrier.product_id.uom_id.id, + 'product_id': carrier.product_id.id, + 'price_unit': price_unit, 'tax_id': [(6, 0, taxes_ids)], 'is_delivery': True }) else: - # Classic grid-based carriers - super(SaleOrder, self).delivery_set() + raise UserError(_('No carrier set for this order.')) + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_delivery = fields.Boolean(string="Is a Delivery", default=False) diff --git a/addons/delivery/models/stock_move.py b/addons/delivery/models/stock_move.py new file mode 100644 index 00000000000..06590501419 --- /dev/null +++ b/addons/delivery/models/stock_move.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import api, fields, models + +import openerp.addons.decimal_precision as dp + + +class StockMove(models.Model): + _inherit = 'stock.move' + + def _default_uom(self): + uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id + return self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1) + + weight = fields.Float(compute='_cal_move_weight', digits_compute=dp.get_precision('Stock Weight'), store=True) + weight_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly=True, help="Unit of Measure (Unit of Measure) is the unit of measurement for Weight", default=_default_uom) + + @api.depends('product_id', 'product_uom_qty', 'product_uom') + def _cal_move_weight(self): + for move in self.filtered(lambda moves: moves.product_id.weight > 0.00): + move.weight = (move.product_qty * move.product_id.weight) + + @api.multi + def action_confirm(self): + """ + Pass the carrier to the picking from the sales order + (Should also work in case of Phantom BoMs when on explosion the original move is deleted) + """ + procs_to_check = [] + for move in self: + if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.carrier_id: + procs_to_check += [move.procurement_id] + res = super(StockMove, self).action_confirm() + for proc in procs_to_check: + pickings = (proc.move_ids.mapped('picking_id')).filtered(lambda record: not record.carrier_id) + if pickings: + pickings.write({'carrier_id': proc.sale_line_id.order_id.carrier_id.id}) + return res diff --git a/addons/delivery/models/stock_picking.py b/addons/delivery/models/stock_picking.py index c8a34b490a9..b6b9f291d71 100644 --- a/addons/delivery/models/stock_picking.py +++ b/addons/delivery/models/stock_picking.py @@ -1,47 +1,48 @@ # -*- 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 +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from openerp import models, fields, api, _ +from openerp.exceptions import UserError + +import openerp.addons.decimal_precision as dp + class StockPicking(models.Model): _inherit = 'stock.picking' + def _default_uom(self): + uom_categ_id = self.env.ref('product.product_uom_categ_kgm').id + return self.env['product.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1) + carrier_price = fields.Float(string="Shipping Cost", readonly=True) delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True) + carrier_id = fields.Many2one("delivery.carrier", string="Carrier") + volume = fields.Float(copy=False) + weight = fields.Float(compute='_cal_weight', digits_compute=dp.get_precision('Stock Weight'), store=True) + carrier_tracking_ref = fields.Char(string='Carrier Tracking Ref', copy=False) + number_of_packages = fields.Integer(string='Number of Packages', copy=False) + weight_uom_id = fields.Many2one('product.uom', string='Unit of Measure', required=True, readonly="1", help="Unit of measurement for Weight", default=_default_uom) + + @api.depends('product_id', 'move_lines') + def _cal_weight(self): + for picking in self: + picking.weight = sum(move.weight for move in picking.move_lines if move.state != 'cancel') @api.multi def do_transfer(self): + self.ensure_one() res = super(StockPicking, self).do_transfer() - if self.carrier_id and self.carrier_id.delivery_type != 'grid': + if self.carrier_id and self.carrier_id.delivery_type not in ['fixed', 'base_on_rule']: 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() + @api.multi + def _prepare_shipping_invoice_line(self, invoice): + self.ensure_one() invoice.ensure_one() - carrier = picking.carrier_id + carrier = self.carrier_id # No carrier if not carrier: @@ -51,12 +52,18 @@ class StockPicking(models.Model): 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 - + if carrier.delivery_type in ['fixed', 'base_on_rule']: + carrier = carrier.verify_carrier(self.partner_id) + if not carrier: + raise UserError(_('The carrier %s (id: %d) has no delivery method!') % (self.carrier_id.name, self.carrier_id.id)) + quantity = sum([line.product_uom_qty for line in self.move_lines]) + price = carrier.get_price_from_picking(invoice.amount_untaxed, self.weight, self.volume, quantity) + else: + # Shipping provider + price = self.carrier_price + + if invoice.company_id.currency_id.id != invoice.currency_id.id: + price = invoice.company_id.currency_id.with_context(date=invoice.date_invoice).compute(invoice.currency_id.id, 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 @@ -65,8 +72,8 @@ class StockPicking(models.Model): taxes_ids = taxes.ids # Apply original SO fiscal position - if picking.sale_id.fiscal_position_id: - fpos = picking.sale_id.fiscal_position_id + if self.sale_id.fiscal_position_id: + fpos = self.sale_id.fiscal_position_id account_id = fpos.map_account(account_id) taxes_ids = fpos.map_tax(taxes).ids @@ -83,12 +90,24 @@ class StockPicking(models.Model): return res - @api.one + @api.model + def _invoice_create_line(self, moves, journal_id, inv_type='out_invoice'): + InvoiceLine = self.env['account.invoice.line'] + invoice_ids = super(StockPicking, self)._invoice_create_line(moves, journal_id, inv_type=inv_type) + for move in moves: + for invoice in move.picking_id.sale_id.invoice_ids.filtered(lambda invoice: invoice.id in invoice_ids): + invoice_line = move.picking_id._prepare_shipping_invoice_line(invoice) + if invoice_line: + InvoiceLine.create(invoice_line) + return invoice_ids + + @api.multi def send_to_shipper(self): + self.ensure_one() 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) + 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 diff --git a/addons/delivery/partner.py b/addons/delivery/partner.py deleted file mode 100644 index 2efe058527f..00000000000 --- a/addons/delivery/partner.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv - -class res_partner(osv.osv): - _inherit = 'res.partner' - _columns = { - 'property_delivery_carrier': fields.property( - type='many2one', - relation='delivery.carrier', - string="Delivery Method", - help="This delivery method will be used when invoicing from picking."), - } diff --git a/addons/delivery/sale.py b/addons/delivery/sale.py deleted file mode 100644 index 629670990c7..00000000000 --- a/addons/delivery/sale.py +++ /dev/null @@ -1,79 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import time -from openerp import api -from openerp.osv import fields, osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError - - -class sale_order_line(osv.Model): - _inherit = 'sale.order.line' - - _columns = { - 'is_delivery': fields.boolean("Is a Delivery"), - } - - _defaults = { - 'is_delivery': False - } - - -class sale_order(osv.Model): - _inherit = 'sale.order' - _columns = { - 'carrier_id': fields.many2one( - "delivery.carrier", string="Delivery Method", - help="Complete this field if you plan to invoice the shipping based on picking."), - } - - @api.onchange('partner_id') - def onchange_partner_id_dtype(self): - if self.partner_id: - self.carrier_id = self.partner_id.property_delivery_carrier - - - def _delivery_unset(self, cr, uid, ids, context=None): - sale_obj = self.pool['sale.order.line'] - line_ids = sale_obj.search(cr, uid, [('order_id', 'in', ids), ('is_delivery', '=', True)],context=context) - sale_obj.unlink(cr, uid, line_ids, context=context) - - def delivery_set(self, cr, uid, ids, context=None): - line_obj = self.pool.get('sale.order.line') - grid_obj = self.pool.get('delivery.grid') - carrier_obj = self.pool.get('delivery.carrier') - acc_fp_obj = self.pool.get('account.fiscal.position') - self._delivery_unset(cr, uid, ids, context=context) - currency_obj = self.pool.get('res.currency') - line_ids = [] - for order in self.browse(cr, uid, ids, context=context): - grid_id = carrier_obj.grid_get(cr, uid, [order.carrier_id.id], order.partner_shipping_id.id) - if not grid_id: - raise UserError(_('No grid matching for this carrier!')) - - if order.state not in ('draft', 'sent'): - raise UserError(_('The order state have to be draft to add delivery lines.')) - - grid = grid_obj.browse(cr, uid, grid_id, context=context) - - taxes = grid.carrier_id.product_id.taxes_id - fpos = order.fiscal_position_id or False - taxes_ids = acc_fp_obj.map_tax(cr, uid, fpos, taxes) - price_unit = grid_obj.get_price(cr, uid, grid.id, order, time.strftime('%Y-%m-%d'), context) - if order.company_id.currency_id.id != order.pricelist_id.currency_id.id: - price_unit = currency_obj.compute(cr, uid, order.company_id.currency_id.id, order.pricelist_id.currency_id.id, - price_unit, context=dict(context or {}, date=order.date_order)) - #create the sale order line - line_id = line_obj.create(cr, uid, { - 'order_id': order.id, - 'name': grid.carrier_id.name, - 'product_uom_qty': 1, - 'product_uom': grid.carrier_id.product_id.uom_id.id, - 'product_id': grid.carrier_id.product_id.id, - 'price_unit': price_unit, - 'tax_id': [(6, 0, taxes_ids)], - 'is_delivery': True - }, context=context) - line_ids.append(line_id) - return line_ids diff --git a/addons/delivery/security/ir.model.access.csv b/addons/delivery/security/ir.model.access.csv index 8a8bc9c2ea8..00b2d70a432 100644 --- a/addons/delivery/security/ir.model.access.csv +++ b/addons/delivery/security/ir.model.access.csv @@ -1,12 +1,9 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_delivery_carrier,delivery.carrier,model_delivery_carrier,base.group_sale_salesman,1,0,0,0
-access_delivery_grid,delivery.grid,model_delivery_grid,base.group_sale_salesman,1,0,0,0
-access_delivery_grid_line,delivery.grid.line,model_delivery_grid_line,base.group_sale_salesman,1,0,0,0
+access_delivery_price_rule,delivery.price.rule,model_delivery_price_rule,base.group_sale_salesman,1,0,0,0
access_delivery_carrier_manager,delivery.carrier,model_delivery_carrier,base.group_sale_manager,1,1,1,1
-access_delivery_grid_manager,delivery.grid,model_delivery_grid,base.group_sale_manager,1,1,1,1
-access_delivery_grid_line_manager,delivery.grid.line,model_delivery_grid_line,base.group_sale_manager,1,1,1,1
+access_delivery_price_rule_manager,delivery.price.rule,model_delivery_price_rule,base.group_sale_manager,1,1,1,1
access_delivery_carrier_partner_manager,delivery.carrier partner_manager,model_delivery_carrier,base.group_partner_manager,1,0,0,0
access_delivery_carrier_stock_user,delivery.carrier stock_user,model_delivery_carrier,stock.group_stock_user,1,0,0,0
access_delivery_carrier_stock_manager,delivery.carrier,model_delivery_carrier,stock.group_stock_manager,1,1,1,1
-access_delivery_grid_stock_manager,delivery.grid,model_delivery_grid,stock.group_stock_manager,1,1,1,1
-access_delivery_grid_line_stock_manager,delivery.grid.line,model_delivery_grid_line,stock.group_stock_manager,1,1,1,1
+access_delivery_price_rule_stock_manager,delivery.price.rule,model_delivery_price_rule,stock.group_stock_manager,1,1,1,1
diff --git a/addons/delivery/stock.py b/addons/delivery/stock.py deleted file mode 100644 index 3967733ea9e..00000000000 --- a/addons/delivery/stock.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields,osv -from openerp.tools.translate import _ -from openerp.exceptions import UserError - -import openerp.addons.decimal_precision as dp - -# Overloaded stock_picking to manage carriers : -class stock_picking(osv.osv): - _inherit = 'stock.picking' - - def _cal_weight(self, cr, uid, ids, name, args, context=None): - res = {} - for picking in self.browse(cr, uid, ids, context=context): - res[picking.id] = sum(move.weight for move in picking.move_lines if move.state != 'cancel') - return res - - def _get_picking_line(self, cr, uid, ids, context=None): - result = {} - for line in self.pool.get('stock.move').browse(cr, uid, ids, context=context): - result[line.picking_id.id] = True - return result.keys() - - - _columns = { - 'carrier_id':fields.many2one("delivery.carrier","Carrier"), - 'volume': fields.float('Volume', copy=False), - 'weight': fields.function(_cal_weight, type='float', string='Weight', digits_compute= dp.get_precision('Stock Weight'), - store={ - 'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['move_lines'], 40), - 'stock.move': (_get_picking_line, ['state', 'picking_id', 'product_id','product_uom_qty','product_uom'], 40), - }), - 'carrier_tracking_ref': fields.char('Carrier Tracking Ref', copy=False), - 'number_of_packages': fields.integer('Number of Packages', copy=False), - 'weight_uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True,readonly="1",help="Unit of measurement for Weight",), - } - - def _prepare_shipping_invoice_line(self, cr, uid, picking, invoice, context=None): - """Prepare the invoice line to add to the shipping costs to the shipping's - invoice. - - :param browse_record picking: the stock picking being invoiced - :param browse_record invoice: the stock picking's invoice - :return: dict containing the values to create the invoice line, - or None to create nothing - """ - carrier_obj = self.pool.get('delivery.carrier') - grid_obj = self.pool.get('delivery.grid') - fpos_obj = self.pool['account.fiscal.position'] - currency_obj = self.pool.get('res.currency') - 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_ids): - return None - grid_id = carrier_obj.grid_get(cr, uid, [picking.carrier_id.id], - picking.partner_id.id, context=context) - if not grid_id: - raise UserError(_('The carrier %s (id: %d) has no delivery grid!') % (picking.carrier_id.name,picking.carrier_id.id)) - quantity = sum([line.product_uom_qty for line in picking.move_lines]) - price = grid_obj.get_price_from_picking(cr, uid, grid_id, - invoice.amount_untaxed, picking.weight, picking.volume, - quantity, context=context) - if invoice.company_id.currency_id.id != invoice.currency_id.id: - price = currency_obj.compute(cr, uid, invoice.company_id.currency_id.id, invoice.currency_id.id, - price, context=dict(context or {}, date=invoice.date_invoice)) - account_id = picking.carrier_id.product_id.property_account_income_id.id - if not account_id: - account_id = picking.carrier_id.product_id.categ_id\ - .property_account_income_categ_id.id - - taxes = picking.carrier_id.product_id.taxes_id - taxes_ids = [x.id for x in taxes] - partner = picking.partner_id or False - fpos = None - if picking.sale_id and picking.sale_id.fiscal_position_id: - fpos = picking.sale_id.fiscal_position_id - elif picking.partner_id: - fpos = partner.property_account_position_id - # TDE FIXME: spotted undefined variable - 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, - 'invoice_id': invoice.id, - 'uom_id': picking.carrier_id.product_id.uom_id.id, - 'product_id': picking.carrier_id.product_id.id, - 'account_id': account_id, - 'price_unit': price, - 'quantity': 1, - 'invoice_line_tax_ids': [(6, 0, taxes_ids)], - } - - def _invoice_create_line(self, cr, uid, moves, journal_id, inv_type='out_invoice', context=None): - invoice_line_obj = self.pool.get('account.invoice.line') - invoice_ids = super(stock_picking, self)._invoice_create_line(cr, uid, moves, journal_id, inv_type=inv_type, context=context) - delivey_invoices = {} - for move in moves: - for invoice in move.picking_id.sale_id.invoice_ids: - if invoice.id in invoice_ids: - delivey_invoices[invoice] = move.picking_id - if delivey_invoices: - for invoice, picking in delivey_invoices.items(): - invoice_line = self._prepare_shipping_invoice_line(cr, uid, picking, invoice, context=context) - if invoice_line: - invoice_line_obj.create(cr, uid, invoice_line) - return invoice_ids - - def _get_default_uom(self, cr, uid, context=None): - uom_categ_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'product.product_uom_categ_kgm') - return self.pool.get('product.uom').search(cr, uid, [('category_id', '=', uom_categ_id), ('factor', '=', 1)])[0] - - _defaults = { - 'weight_uom_id': lambda self, cr, uid, c: self._get_default_uom(cr, uid, c), - } - - -class stock_move(osv.osv): - _inherit = 'stock.move' - - def _cal_move_weight(self, cr, uid, ids, name, args, context=None): - res = {} - for move in self.browse(cr, uid, ids, context=context): - weight = 0.00 - if move.product_id.weight > 0.00: - weight = move.product_qty * move.product_id.weight - res[move.id] = weight - return res - - _columns = { - 'weight': fields.function(_cal_move_weight, type='float', string='Weight', digits_compute= dp.get_precision('Stock Weight'), - store={ - 'stock.move': (lambda self, cr, uid, ids, c=None: ids, ['product_id', 'product_uom_qty', 'product_uom'], 30), - }), - 'weight_uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True,readonly="1",help="Unit of Measure (Unit of Measure) is the unit of measurement for Weight",), - } - - def action_confirm(self, cr, uid, ids, context=None): - """ - Pass the carrier to the picking from the sales order - (Should also work in case of Phantom BoMs when on explosion the original move is deleted) - """ - procs_to_check = [] - for move in self.browse(cr, uid, ids, context=context): - if move.procurement_id and move.procurement_id.sale_line_id and move.procurement_id.sale_line_id.order_id.carrier_id: - procs_to_check += [move.procurement_id] - res = super(stock_move, self).action_confirm(cr, uid, ids, context=context) - pick_obj = self.pool.get("stock.picking") - for proc in procs_to_check: - pickings = list(set([x.picking_id.id for x in proc.move_ids if x.picking_id and not x.picking_id.carrier_id])) - if pickings: - pick_obj.write(cr, uid, pickings, {'carrier_id': proc.sale_line_id.order_id.carrier_id.id}, context=context) - return res - - - def _get_default_uom(self, cr, uid, context=None): - uom_categ_id = self.pool.get('ir.model.data').xmlid_to_res_id(cr, uid, 'product.product_uom_categ_kgm') - return self.pool.get('product.uom').search(cr, uid, [('category_id', '=', uom_categ_id),('factor','=',1)])[0] - - _defaults = { - 'weight_uom_id': lambda self, cr, uid, c: self._get_default_uom(cr, uid, c), - } diff --git a/addons/delivery/test/delivery_cost.yml b/addons/delivery/test/delivery_cost.yml deleted file mode 100644 index d44c1d8ab87..00000000000 --- a/addons/delivery/test/delivery_cost.yml +++ /dev/null @@ -1,80 +0,0 @@ -- - In order to test Carrier Cost, -- - Create sale order with Normal Delivery Charges -- - !record {model: sale.order, id: sale_normal_delivery_charges}: - partner_id: base.res_partner_18 - partner_invoice_id: base.res_partner_18 - partner_shipping_id: base.res_partner_18 - pricelist_id: product.list0 - order_line: - - name: 'PC Assamble + 2GB RAM' - product_id: product.product_product_4 - product_uom_qty: 1 - product_uom: product.product_uom_unit - price_unit: 750.00 - carrier_id: normal_delivery_carrier -- - I add delivery cost in Sale order. -- - !python {model: sale.order}: | - self.pool.get('product.product').write(cr, uid, [ref('product_product_delivery')], {'taxes_id': [(5, 0, 0)]}, context=context) # Removing taxes as this can cause problem (re. price_included) - self.delivery_set(cr, uid, [ref("sale_normal_delivery_charges")], context=context) -- - I check sale order after added delivery cost. -- - !python {model: sale.order.line}: | - from openerp.tools import float_compare - line_ids = self.search(cr, uid, [('order_id','=', ref('sale_normal_delivery_charges')), ('product_id','=', ref('product_product_delivery'))]) - assert len(line_ids), "Delivery cost is not Added" - line_data = self.browse(cr ,uid ,line_ids[0] ,context) - assert float_compare(line_data.price_subtotal, 10, precision_digits=2) == 0, "Delivey cost is not correspond." -- - I confirm the sale order. -- - !python {model: sale.order}: | - self.action_confirm(cr, uid, ref("sale_normal_delivery_charges"), context=context) -- - Create one more sale order with Free Delivery Charges -- - !record {model: sale.order, id: sale_free_delivery_charges}: - partner_id: base.res_partner_4 - partner_invoice_id: base.res_partner_address_13 - partner_shipping_id: base.res_partner_address_13 - pricelist_id: product.list0 - order_line: - - name: 'Service on demand' - product_id: product.product_product_1 - product_uom_qty: 24 - product_uom: product.product_uom_hour - price_unit: 75.00 - order_line: - - name: 'On Site Assistance' - product_id: product.product_product_2 - product_uom_qty: 30 - product_uom: product.product_uom_hour - price_unit: 38.25 - carrier_id: free_delivery_carrier -- - I add free delivery cost in Sale order. -- - !python {model: sale.order}: | - self.delivery_set(cr, uid, [ref("sale_free_delivery_charges")], context=context) -- - I check sale order after added delivery cost. -- - !python {model: sale.order.line}: | - from openerp.tools import float_compare - line_ids = self.search(cr, uid, [('order_id','=', ref('sale_free_delivery_charges')), ('product_id','=', ref('product_product_delivery'))]) - assert len(line_ids), "Delivery cost is not Added" - line_data = self.browse(cr ,uid ,line_ids[0] ,context) - assert float_compare(line_data.price_subtotal, 0, precision_digits=2) == 0, "Delivey cost is not correspond." -- - I set default delivery policy. -- - !record {model: sale.config.settings, id: default_delivery_policy}: - {} -- - !python {model: sale.config.settings}: | - self.execute(cr, uid, [ref('default_delivery_policy')], context=context) diff --git a/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml b/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml deleted file mode 100644 index 7a41665adf3..00000000000 --- a/addons/delivery/test/stock_move_values_with_invoice_before_delivery.yml +++ /dev/null @@ -1,73 +0,0 @@ -- - test that the store fields of stock moves are computed with invoice before delivery flow -- - set a weight on ipod 16GB -- - !python {model: product.product, id: product.product_product_11}: | - self.write({'weight': 0.25}) -- - create a SO with invoice before delivery -- - !record {model: sale.order, id: sale_prepaid}: - partner_id: base.res_partner_18 - partner_invoice_id: base.res_partner_18 - partner_shipping_id: base.res_partner_18 - pricelist_id: product.list0 - order_line: - - name: 'Ice Cream' - product_id: stock.product_icecream - product_uom_qty: 2 - product_uom: product.product_uom_kgm - price_unit: 750.00 - carrier_id: normal_delivery_carrier -- - I add delivery cost in Sale order. -- - !python {model: sale.order, id: sale_prepaid}: | - self.delivery_set() -- - I confirm the SO. -- - !python {model: sale.order}: | - self.action_confirm(cr, uid, ref("sale_prepaid"), context=context) - self.action_invoice_create(cr, uid, [ref("sale_prepaid")], context=context) -- - I check that the invoice was created -- - !python {model: sale.order, id: sale_prepaid}: | - assert len(self.invoice_ids) == 1, "Invoice not created." -- - I confirm the invoice -- - !python {model: sale.order, id: sale_prepaid}: | - invoice = self.invoice_ids[0] - invoice.signal_workflow('invoice_open') -- - I pay the invoice. -- - !python {model: sale.order, id: sale_prepaid}: | - invoice = self.invoice_ids[0] - invoice.signal_workflow('invoice_open') - journal = self.env['account.journal'].search([('type', '=', 'cash'), - ('company_id', '=', self.company_id.id)], - limit=1) - invoice.pay_and_reconcile(journal, invoice.amount_total) -- - Check the SO after paying the invoice -- - !python {model: sale.order, id: sale_prepaid}: | - assert self.invoice_count != 0, 'order not invoiced' - assert self.invoice_status == 'invoiced', 'order is not invoiced' - assert len(self.picking_ids) == 1, 'pickings not generated' -- - check the stock moves -- - !python {model: sale.order, id: sale_prepaid}: | - move = self.picking_ids.move_lines - assert move.product_qty == 2, 'wrong product_qty' - assert move.weight == 2.0, 'wrong move weight' -- - ship -- - !python {model: sale.order, id: sale_prepaid}: | - picking = self.picking_ids.action_done() diff --git a/addons/delivery/tests/__init__.py b/addons/delivery/tests/__init__.py new file mode 100644 index 00000000000..1c52d7dd4a2 --- /dev/null +++ b/addons/delivery/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing detailsself. + +from . import test_delivery_cost, test_delivery_stock_move diff --git a/addons/delivery/tests/test_delivery_cost.py b/addons/delivery/tests/test_delivery_cost.py new file mode 100644 index 00000000000..0a83bfd7946 --- /dev/null +++ b/addons/delivery/tests/test_delivery_cost.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +from openerp.tests import common +from openerp.tools import float_compare + + +class TestDeliveryCost(common.TransactionCase): + + def setUp(self): + super(TestDeliveryCost, self).setUp() + self.SaleOrder = self.env['sale.order'] + self.SaleOrderLine = self.env['sale.order.line'] + self.AccountAccount = self.env['account.account'] + self.SaleConfigSetting = self.env['sale.config.settings'] + self.Product = self.env['product.product'] + + self.partner_18 = self.env.ref('base.res_partner_18') + self.pricelist = self.env.ref('product.list0') + self.product_4 = self.env.ref('product.product_product_4') + self.product_uom_unit = self.env.ref('product.product_uom_unit') + self.normal_delivery = self.env.ref('delivery.normal_delivery_carrier') + self.partner_4 = self.env.ref('base.res_partner_4') + self.partner_address_13 = self.env.ref('base.res_partner_address_13') + self.product_uom_hour = self.env.ref('product.product_uom_hour') + self.account_data = self.env.ref('account.data_account_type_revenue') + self.account_tag_operating = self.env.ref('account.account_tag_operating') + self.product_2 = self.env.ref('product.product_product_2') + self.product_category = self.env.ref('product.product_category_all') + self.free_delivery = self.env.ref('delivery.free_delivery_carrier') + + def test_00_delivery_cost(self): + # In order to test Carrier Cost + # Create sale order with Normal Delivery Charges + + self.sale_normal_delivery_charges = self.SaleOrder.create({ + 'partner_id': self.partner_18.id, + 'partner_invoice_id': self.partner_18.id, + 'partner_shipping_id': self.partner_18.id, + 'pricelist_id': self.pricelist.id, + 'order_line': [(0, 0, { + 'name': 'PC Assamble + 2GB RAM', + 'product_id': self.product_4.id, + 'product_uom_qty': 1, + 'product_uom': self.product_uom_unit.id, + 'price_unit': 750.00, + })], + 'carrier_id': self.normal_delivery.id + }) + # I add delivery cost in Sale order + + self.a_sale = self.AccountAccount.create({ + 'code': 'X2020', + 'name': 'Product Sales - (test)', + 'user_type_id': self.account_data.id, + 'tag_ids': [(6, 0, { + self.account_tag_operating.id + })] + }) + + self.product_consultant = self.Product.create({ + 'sale_ok': True, + 'list_price': 75.0, + 'standard_price': 30.0, + 'uom_id': self.product_uom_hour.id, + 'uom_po_id': self.product_uom_hour.id, + 'name': 'Service', + 'categ_id': self.product_category.id, + 'type': 'service' + }) + + # I add delivery cost in Sale order + self.sale_normal_delivery_charges.delivery_set() + + # I check sale order after added delivery cost + + line = self.SaleOrderLine.search([('order_id', '=', self.sale_normal_delivery_charges.id), + ('product_id', '=', self.sale_normal_delivery_charges.carrier_id.product_id.id)]) + self.assertEqual(len(line), 1, "Delivery cost is not Added") + + self.assertEqual(float_compare(line.price_subtotal, 10, precision_digits=2), 0, + "Delivey cost is not correspond.") + + # I confirm the sale order + + self.sale_normal_delivery_charges.action_confirm() + + # Create one more sale order with Free Delivery Charges + + self.delivery_sale_order_cost = self.SaleOrder.create({ + 'partner_id': self.partner_4.id, + 'partner_invoice_id': self.partner_address_13.id, + 'partner_shipping_id': self.partner_address_13.id, + 'pricelist_id': self.pricelist.id, + 'order_line': [(0, 0, { + 'name': 'Service on demand', + 'product_id': self.product_consultant.id, + 'product_uom_qty': 24, + 'product_uom': self.product_uom_hour.id, + 'price_unit': 75.00, + }), (0, 0, { + 'name': 'On Site Assistance', + 'product_id': self.product_2.id, + 'product_uom_qty': 30, + 'product_uom': self.product_uom_hour.id, + 'price_unit': 38.25, + })], + 'carrier_id': self.free_delivery.id + }) + + # I add free delivery cost in Sale order + self.delivery_sale_order_cost.delivery_set() + + # I check sale order after adding delivery cost + line = self.SaleOrderLine.search([('order_id', '=', self.delivery_sale_order_cost.id), + ('product_id', '=', self.delivery_sale_order_cost.carrier_id.product_id.id)]) + + self.assertEqual(len(line), 1, "Delivery cost is not Added") + self.assertEqual(float_compare(line.price_subtotal, 0, precision_digits=2), 0, + "Delivey cost is not correspond.") + + # I set default delivery policy + + self.default_delivery_policy = self.SaleConfigSetting.create({}) + + self.default_delivery_policy.execute() diff --git a/addons/delivery/tests/test_delivery_stock_move.py b/addons/delivery/tests/test_delivery_stock_move.py new file mode 100644 index 00000000000..fb57a2ec1e5 --- /dev/null +++ b/addons/delivery/tests/test_delivery_stock_move.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +from openerp.tests import common + + +class StockMoveInvoice(common.TransactionCase): + + def setUp(self): + super(StockMoveInvoice, self).setUp() + self.ProductProduct = self.env['product.product'] + self.SaleOrder = self.env['sale.order'] + self.AccountJournal = self.env['account.journal'] + + self.partner_18 = self.env.ref('base.res_partner_18') + self.pricelist_id = self.env.ref('product.list0') + self.product_11 = self.env.ref('product.product_product_11') + self.product_icecream = self.env.ref('stock.product_icecream') + self.product_uom_kgm = self.env.ref('product.product_uom_kgm') + self.normal_delivery = self.env.ref('delivery.normal_delivery_carrier') + + def test_01_delivery_stock_move(self): + # Test if the stored fields of stock moves are computed with invoice before delivery flow + # Set a weight on ipod 16GB + self.product_11.write({ + 'weight': 0.25, + }) + + self.sale_prepaid = self.SaleOrder.create({ + 'partner_id': self.partner_18.id, + 'partner_invoice_id': self.partner_18.id, + 'partner_shipping_id': self.partner_18.id, + 'pricelist_id': self.pricelist_id.id, + 'order_line': [(0, 0, { + 'name': 'Ice Cream', + 'product_id': self.product_icecream.id, + 'product_uom_qty': 2, + 'product_uom': self.product_uom_kgm.id, + 'price_unit': 750.00, + })], + 'carrier_id': self.normal_delivery.id + }) + + # I add delivery cost in Sale order + self.sale_prepaid.delivery_set() + + # I confirm the SO. + self.sale_prepaid.action_confirm() + self.sale_prepaid.action_invoice_create() + + # I check that the invoice was created + self.assertEqual(len(self.sale_prepaid.invoice_ids), 1, "Invoice not created.") + + # I confirm the invoice + + self.invoice = self.sale_prepaid.invoice_ids + self.invoice.signal_workflow('invoice_open') + + # I pay the invoice. + self.invoice = self.sale_prepaid.invoice_ids + self.invoice.signal_workflow('invoice_open') + self.journal = self.AccountJournal.search([('type', '=', 'cash'), ('company_id', '=', self.sale_prepaid.company_id.id)], limit=1) + self.invoice.pay_and_reconcile(self.journal, self.invoice.amount_total) + + # Check the SO after paying the invoice + self.assertNotEqual(self.sale_prepaid.invoice_count, 0, 'order not invoiced') + self.assertTrue(self.sale_prepaid.invoice_status == 'invoiced', 'order is not invoiced') + self.assertEqual(len(self.sale_prepaid.picking_ids), 1, 'pickings not generated') + + # Check the stock moves + moves = self.sale_prepaid.picking_ids.move_lines + self.assertEqual(moves[0].product_qty, 2, 'wrong product_qty') + self.assertEqual(moves[0].weight, 2.0, 'wrong move weight') + + # Ship + self.picking = self.sale_prepaid.picking_ids.action_done() diff --git a/addons/delivery/delivery_view.xml b/addons/delivery/views/delivery_view.xml index ae20d85f7bf..6d7525be2b8 100644 --- a/addons/delivery/delivery_view.xml +++ b/addons/delivery/views/delivery_view.xml @@ -1,5 +1,5 @@ -<openerp> - <data> +<?xml version="1.0" encoding="utf-8"?> +<odoo> <!-- Delivery Carriers --> <menuitem id="menu_delivery" name="Delivery" parent="stock.menu_stock_config_settings" groups="stock.group_stock_manager" sequence="50"/> <menuitem id="sale_menu_delivery" name="Delivery" parent="base.menu_sale_config" groups="base.group_sale_manager" sequence="12"/> @@ -12,7 +12,7 @@ <field name="sequence" widget="handle"/> <field name="name"/> <field name="partner_id"/> - <field name="price" invisible="'order_id' not in context"/> + <field name="price"/> </tree> </field> </record> @@ -22,72 +22,59 @@ <field name="model">delivery.carrier</field> <field name="arch" type="xml"> <form string="Carrier"> + <header> + <field name="state" widget="statusbar" attrs="{'invisible':['|', ('delivery_type', '=', 'fixed'), ('delivery_type', '=', 'base_on_rule')]}" clickable="True"/> + </header> <sheet> - <div class="oe_title" name="title"> - <label for="name" class="oe_edit_only"/> - <h1> - <field name="name"/> - </h1> - </div> - <group> - <field name="active"/> - </group> - <group name="general"> - <group string="General Information"> - <field name="partner_id"/> - <field name="product_id"/> - <field name="delivery_type"/> - </group> - <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> - <field name="free_if_more_than" attrs="{'readonly':[('use_detailed_pricelist', '=', True)]}"/> - <field name="amount" attrs="{'required':[('free_if_more_than','<>',False)], 'invisible':[('free_if_more_than','=',False)]}"/> - </div> - <field name="use_detailed_pricelist"/> - </group> - </group> - - <field name="pricelist_ids" attrs="{'invisible':[('use_detailed_pricelist','=',False)]}" mode="tree"> - <tree string="Delivery grids"> - <field name="sequence"/> - <field name="name"/> - </tree> - <form string="Delivery grids"> - <group> + <div class="oe_title" name="title"> + <label for="name" string="Name" class="oe_edit_only"/> + <h1> + <field name="name"/> + </h1> + <group name="company"> + <field name="partner_id" class="oe_inline"/> + <field name="product_type" invisible="1"/> + <field name="product_sale_ok" invisible="1"/> + </group> + </div> + <notebook> + <page string="Pricing"> <group> - <field name="name"/> + <field name="delivery_type" widget="radio"/> </group> - <group> - <field name="active"/> - <field name="sequence"/> + <group attrs="{'invisible':[('delivery_type', '!=', 'fixed')]}"> + <group> + <field name="fixed_price"/> + </group> + <group> + <div class="o_row"> + <label for="free_if_more_than"/> + <field name="free_if_more_than"/> + <field nolabel="1" name="amount" attrs="{'required':[('free_if_more_than','!=', False)], 'invisible':[('free_if_more_than','=', False)]}"/> + </div> + </group> + </group> + <group name="general" attrs="{'invisible':[('delivery_type', '!=', 'base_on_rule')]}"> + <field name="price_rule_ids" nolabel="1"/> </group> - </group> - <notebook> - <page string="Grid definition"> - <field name="line_ids"/> - </page> - <page string="Destination"> + </page> + <page string="Destination"> + <group> <group> - <group> - <field name="country_ids" widget="many2many_tags"/> - <field name="state_ids" widget="many2many_tags"/> - </group> - <group> - <label for="zip_from" string="Zip"/> - <div> - <field name="zip_from" class="oe_inline"/> - - - <field name="zip_to" class="oe_inline"/> - </div> - </group> + <field name="country_ids" widget="many2many_tags"/> + <field name="state_ids" widget="many2many_tags"/> </group> - </page> - </notebook> - </form> - </field> - </sheet> + <group></group> + <group> + <field name="zip_from"/> + </group> + <group> + <field name="zip_to"/> + </group> + </group> + </page> + </notebook> + </sheet> </form> </field> </record> @@ -116,83 +103,20 @@ <menuitem action="action_delivery_carrier_form" id="menu_action_delivery_carrier_form" parent="menu_delivery" sequence="1"/> <menuitem action="action_delivery_carrier_form" id="sale_menu_action_delivery_carrier_form" parent="sale_menu_delivery" sequence="1"/> - <!-- Delivery Grids --> - <record id="view_delivery_grid_tree" model="ir.ui.view"> - <field name="name">delivery.grid.tree</field> - <field name="model">delivery.grid</field> - <field name="arch" type="xml"> - <tree string="Delivery grids"> - <field name="sequence"/> - <field name="carrier_id"/> - <field name="name"/> - </tree> - </field> - </record> - - <record id="view_delivery_grid_form" model="ir.ui.view"> - <field name="name">delivery.grid.form</field> - <field name="model">delivery.grid</field> - <field name="arch" type="xml"> - <form string="Delivery grids"> - <group col="4"> - <field name="name"/> - <field name="active"/> - <field name="carrier_id"/> - <field name="sequence"/> - </group> - <notebook> - <page string="Grid definition"> - <field name="line_ids"/> - </page> - <page string="Destination"> - <group string="Countries"> - <field name="country_ids" nolabel="1"/> - </group> - <group string="States"> - <field colspan="2" name="state_ids" nolabel="1"/> - <field name="zip_from"/> - <field name="zip_to"/> - </group> - </page> - </notebook> - </form> - </field> - </record> - <record id="action_delivery_grid_form" model="ir.actions.act_window"> - <field name="name">Delivery Pricelist</field> - <field name="type">ir.actions.act_window</field> - <field name="res_model">delivery.grid</field> - <field name="view_type">form</field> - <field name="view_mode">tree,form</field> - <field name="help" type="html"> - <p class="oe_view_nocontent_create"> - Click to create a delivery price list for a specific region. - </p><p> - The delivery price list allows you to compute the cost and - sales price of the delivery according to the weight of the - products and other criteria. You can define several price lists - for each delivery method: per country or a zone in a specific - country defined by a postal code range. - </p> - </field> - </record> - <menuitem action="action_delivery_grid_form" id="menu_action_delivery_grid_form" parent="menu_delivery" sequence="2"/> - <menuitem action="action_delivery_grid_form" id="sale_menu_action_delivery_grid_form" parent="sale_menu_delivery" sequence="2"/> - - <record id="view_delivery_grid_line_form" model="ir.ui.view"> - <field name="name">delivery.grid.line.form</field> - <field name="model">delivery.grid.line</field> + <record id="view_delivery_price_rule_form" model="ir.ui.view"> + <field name="name">delivery.price.rule.form</field> + <field name="model">delivery.price.rule</field> <field name="arch" type="xml"> - <form string="Grid Lines"> + <form string="Price Rules"> <group> - <field name="name"/> + <field name="name" invisible="1"/> </group> <group> - <label for="type" string="Condition"/> - <div> - <field name="type" class="oe_inline"/> - <field name="operator" class="oe_inline"/> - <field name="max_value" class="oe_inline"/> + <label for="variable" string="Condition"/> + <div class="o_row"> + <field name="variable"/> + <field name="operator"/> + <field name="max_value"/> </div> <label for="list_base_price" string="Sale Price"/> <div> @@ -202,23 +126,19 @@ * <field name="variable_factor" class="oe_inline"/> </div> - <field name="standard_price"/> + <field name="standard_price" invisible="1"/> </group> </form> </field> </record> - <record id="view_delivery_grid_line_tree" model="ir.ui.view"> - <field name="name">delivery.grid.line.tree</field> - <field name="model">delivery.grid.line</field> + <record id="view_delivery_price_rule_tree" model="ir.ui.view"> + <field name="name">delivery.price.rule.tree</field> + <field name="model">delivery.price.rule</field> <field name="arch" type="xml"> - <tree string="Grid Lines"> + <tree string="Price Rules"> <field name="sequence" widget="handle" /> <field name="name"/> - <field name="type"/> - <field name="operator"/> - <field name="max_value" nolabel="1"/> - <field name="list_price"/> - <field name="standard_price"/> + <field name="list_base_price" string="Sale Price"/> </tree> </field> </record> @@ -238,7 +158,7 @@ <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'))]}"/> + <button type='object' class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"/> </div> <field name="carrier_price"/> </group> @@ -254,11 +174,11 @@ </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')]}" /> + <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule'])]}" /> </div> </xpath> <xpath expr="/form/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'))]}"/> + <button name="send_to_shipper" string="Send to Shipper" type="object" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"/> </xpath> </data> </field> @@ -327,7 +247,7 @@ <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'))]}"/> + <button type='object' class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"/> </div> <field name="carrier_price"/> </group> @@ -335,5 +255,4 @@ </field> </record> - </data> -</openerp> +</odoo> diff --git a/addons/delivery/partner_view.xml b/addons/delivery/views/partner_view.xml index e9960fee6f8..7d708a65e70 100644 --- a/addons/delivery/partner_view.xml +++ b/addons/delivery/views/partner_view.xml @@ -1,5 +1,4 @@ -<openerp> - <data> +<odoo> <record id="view_partner_property_form" model="ir.ui.view"> <field name="name">res.partner.carrier.property.form.inherit</field> @@ -7,10 +6,9 @@ <field name="inherit_id" ref="mail.view_emails_partner_info_form" /> <field name="arch" type="xml"> <field name="opt_out" position="after"> - <field name="property_delivery_carrier"/> + <field name="property_delivery_carrier_id"/> </field> </field> </record> - </data> -</openerp> +</odoo> diff --git a/addons/delivery/views/report_shipping.xml b/addons/delivery/views/report_shipping.xml index 7259668edb7..7743d13f87e 100644 --- a/addons/delivery/views/report_shipping.xml +++ b/addons/delivery/views/report_shipping.xml @@ -1,5 +1,4 @@ -<openerp> - <data> +<odoo> <template id="report_shipping2" inherit_id="stock.report_picking"> <xpath expr="//div[@name='partner_header']" position="after"> <div name="invoice_partner" t-if="o.picking_type_id.code == 'outgoing' and o.sale_id and o.sale_id.partner_invoice_id"> @@ -21,5 +20,4 @@ </td> </xpath> </template> - </data> -</openerp> +</odoo> diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 6805beab280..52c58c09012 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -333,18 +333,18 @@ $('.oe_website_sale').each(function () { }); $(oe_website_sale).on('change', "select[name='country_id']", function () { - var $select = $("select[name='state_id']"); + var $select = $("select[name='state_id']:enabled"); $select.find("option:not(:first)").hide(); var nb = $select.find("option[data-country_id="+($(this).val() || 0)+"]").show().size(); - $select.parent().toggle(nb>1); + $select.parent().toggle(nb>=1); }); $(oe_website_sale).find("select[name='country_id']").change(); $(oe_website_sale).on('change', "select[name='shipping_country_id']", function () { - var $select = $("select[name='shipping_state_id']"); + var $select = $("select[name='shipping_state_id']:enabled"); $select.find("option:not(:first)").hide(); var nb = $select.find("option[data-country_id="+($(this).val() || 0)+"]").show().size(); - $select.parent().toggle(nb>1); + $select.parent().toggle(nb>=1); }); $(oe_website_sale).find("select[name='shipping_country_id']").change(); }); diff --git a/addons/website_sale_delivery/__openerp__.py b/addons/website_sale_delivery/__openerp__.py index f843d0deac5..c05689e506f 100644 --- a/addons/website_sale_delivery/__openerp__.py +++ b/addons/website_sale_delivery/__openerp__.py @@ -13,6 +13,7 @@ Delivery Costs 'views/website_sale_delivery.xml', 'views/website_sale_delivery_view.xml', 'security/ir.model.access.csv', + 'demo/website_sale_delivery_data.xml' ], 'demo': [ 'data/website_sale_delivery_demo.xml' diff --git a/addons/website_sale_delivery/controllers/main.py b/addons/website_sale_delivery/controllers/main.py index 15bc881c364..42b6214d128 100644 --- a/addons/website_sale_delivery/controllers/main.py +++ b/addons/website_sale_delivery/controllers/main.py @@ -26,3 +26,7 @@ class website_sale(openerp.addons.website_sale.controllers.main.website_sale): """ Transforms a list of order lines into a dict for google analytics """ order_lines_not_delivery = [line for line in order_lines if not line.is_delivery] return super(website_sale, self).order_lines_2_google_api(order_lines_not_delivery) + + def checkout_values(self, data=None): + values = super(website_sale, self).checkout_values(data) + return request.env['sale.order']._get_shipping_country(values) diff --git a/addons/website_sale_delivery/demo/website_sale_delivery_data.xml b/addons/website_sale_delivery/demo/website_sale_delivery_data.xml new file mode 100644 index 00000000000..d77522880d0 --- /dev/null +++ b/addons/website_sale_delivery/demo/website_sale_delivery_data.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="delivery.free_delivery_carrier" model="delivery.carrier"> + <field name="website_published" eval="True"/> + </record> +</odoo> diff --git a/addons/website_sale_delivery/models/sale_order.py b/addons/website_sale_delivery/models/sale_order.py index 7416bfc8007..d8fbeb38f05 100644 --- a/addons/website_sale_delivery/models/sale_order.py +++ b/addons/website_sale_delivery/models/sale_order.py @@ -12,7 +12,7 @@ class delivery_carrier(orm.Model): _inherit = ['delivery.carrier', 'website.published.mixin'] _columns = { - 'website_description': fields.text('Description for Online Quotations'), + 'website_description': fields.related('product_id', 'description_sale', type="text", string='Description for Online Quotations'), } _defaults = { 'website_published': False @@ -91,8 +91,8 @@ class SaleOrder(orm.Model): carrier_ids.insert(0, carrier_id) if force_carrier_id or not carrier_id or not carrier_id in carrier_ids: for delivery_id in carrier_ids: - grid_id = carrier_obj.grid_get(cr, SUPERUSER_ID, [delivery_id], order.partner_shipping_id.id) - if grid_id: + carrier = carrier_obj.verify_carrier(cr, SUPERUSER_ID, [delivery_id], order.partner_shipping_id) + if carrier: carrier_id = delivery_id break order.write({'carrier_id': carrier_id}) @@ -157,3 +157,25 @@ class SaleOrder(orm.Model): self._check_carrier_quotation(cr, uid, sale_order, context=context) return values + + def _get_shipping_country(self, cr, uid, values, context=None): + country_ids = set() + state_ids = set() + values['shipping_countries'] = values['countries'] + values['shipping_states'] = values['states'] + + DeliveryCarrier = self.pool['delivery.carrier'] + delivery_carrier_ids = DeliveryCarrier.search(cr, SUPERUSER_ID, [('website_published', '=', True)], context=context) + for carrier in DeliveryCarrier.browse(cr, SUPERUSER_ID, delivery_carrier_ids, context): + if not carrier.country_ids and not carrier.state_ids: + return values + # Authorized shipping countries + country_ids = country_ids|set(carrier.country_ids.ids) + # Authorized shipping countries without any state restriction + state_country_ids = [country.id for country in carrier.country_ids if country.id not in carrier.state_ids.mapped('country_id.id')] + # Authorized shipping states + all states from shipping countries without any state restriction + state_ids = state_ids|set(carrier.state_ids.ids)|set(values['states'].filtered(lambda r: r.country_id.id in state_country_ids).ids) + + values['shipping_countries'] = values['countries'].filtered(lambda r: r.id in list(country_ids)) + values['shipping_states'] = values['states'].filtered(lambda r: r.id in list(state_ids)) + return values diff --git a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js index d18b6516383..46caab18e0e 100644 --- a/addons/website_sale_delivery/static/src/js/website_sale_delivery.js +++ b/addons/website_sale_delivery/static/src/js/website_sale_delivery.js @@ -7,4 +7,19 @@ $(document).ready(function () { window.location.href = '/shop/payment?carrier_id=' + carrier_id; }); + $(".oe_website_sale select[name='shipping_id']").on('change', function () { + var value = $(this).val(); + var $provider_free = $("select[name='country_id']:not(.o_provider_restricted), select[name='state_id']:not(.o_provider_restricted)"); + var $provider_restricted = $("select[name='country_id'].o_provider_restricted, select[name='state_id'].o_provider_restricted"); + if (value == 0) { + // Ship to the same address : only show shipping countries available for billing + $provider_free.hide().attr('disabled', true); + $provider_restricted.show().attr('disabled', false).change(); + } else { + // Create a new address : show all countries available for billing + $provider_free.show().attr('disabled', false).change(); + $provider_restricted.hide().attr('disabled', true); + } + }); + }); diff --git a/addons/website_sale_delivery/views/website_sale_delivery.xml b/addons/website_sale_delivery/views/website_sale_delivery.xml index 0c33577cea3..9708cd0d335 100644 --- a/addons/website_sale_delivery/views/website_sale_delivery.xml +++ b/addons/website_sale_delivery/views/website_sale_delivery.xml @@ -51,5 +51,39 @@ </xpath> </template> + <template id="country_state_shipping" name="Shipping Country and states" inherit_id="website_sale.checkout"> + <!-- By default, with 'ship to the same address', we can only ship to restricted countries & states --> + <xpath expr="//select[@name='country_id']" position="attributes"> + <attribute name="t-att-disabled">not shipping_id and 'disabled' or ''</attribute> + <attribute name="t-att-style">not shipping_id and 'display: none' or ''</attribute> + </xpath> + <xpath expr="//select[@name='country_id']" position="after"> + <select name="country_id" class="form-control o_provider_restricted" t-att-style="shipping_id and 'display:none' or ''" t-att-disabled="shipping_id and 'disabled' or ''"> + <option value="">Country...</option> + <t t-foreach="shipping_countries or []" t-as="country"> + <option t-att-value="country.id" t-att-selected="country.id == checkout.get('country_id')"><t t-esc="country.name"/></option> + </t> + </select> + </xpath> + <xpath expr="//select[@name='state_id']" position="attributes"> + <attribute name="t-att-disabled">not shipping_id and 'disabled' or ''</attribute> + <attribute name="t-att-style">not shipping_id and 'display: none' or ''</attribute> + </xpath> + <xpath expr="//select[@name='state_id']" position="after"> + <select name="state_id" class="form-control o_provider_restricted" t-att-style="shipping_id and 'display:none' or ''" t-att-disabled="shipping_id and 'disabled' or ''"> + <option value="">Select...</option> + <t t-foreach="shipping_states or []" t-as="state"> + <option t-att-value="state.id" style="display:none;" t-att-data-country_id="state.country_id.id" t-att-selected="state.id == checkout.get('state_id')"><t t-esc="state.name"/></option> + </t> + </select> + </xpath> + <xpath expr="//select[@name='shipping_country_id']/t[@t-foreach]" position="attributes"> + <attribute name="t-foreach">shipping_countries</attribute> + </xpath> + <xpath expr="//select[@name='shipping_state_id']/t[@t-foreach]" position="attributes"> + <attribute name="t-foreach">shipping_states</attribute> + </xpath> + </template> + </data> </openerp> diff --git a/addons/website_sale_delivery/views/website_sale_delivery_view.xml b/addons/website_sale_delivery/views/website_sale_delivery_view.xml index 58f26f7d39e..12a48b96f85 100644 --- a/addons/website_sale_delivery/views/website_sale_delivery_view.xml +++ b/addons/website_sale_delivery/views/website_sale_delivery_view.xml @@ -13,11 +13,11 @@ </button> </div> </div> - <group name="general" position="after"> - <group name="website"> - <field name="website_description" attrs="{'invisible':[('website_published', '=', False)]}"/> + <div name="title" position="after"> + <group> + <field name="website_description" string="Description" placeholder="Description displayed on the eCommerce and on the Online Quotations."/> </group> - </group> + </div> </field> </record> </odoo> |
