+
Optional Products
@@ -10,7 +11,7 @@
| Description |
- |
+ |
Unit Price |
@@ -19,8 +20,8 @@
|
-
-
+ |
+
% discount
|
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 3e612a6dc0c..ab749b59927 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -319,6 +319,13 @@ class SaleOrderLine(models.Model):
})
return values
+ def _get_qty_procurement(self):
+ self.ensure_one()
+ qty = 0.0
+ for move in self.move_ids.filtered(lambda r: r.state != 'cancel'):
+ qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
+ return qty
+
@api.multi
def _action_launch_stock_rule(self):
"""
@@ -331,9 +338,7 @@ class SaleOrderLine(models.Model):
for line in self:
if line.state != 'sale' or not line.product_id.type in ('consu','product'):
continue
- qty = 0.0
- for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
- qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom, rounding_method='HALF-UP')
+ qty = line._get_qty_procurement()
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
continue
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index f1bca879dec..a798926bfdb 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -454,8 +454,9 @@ class Picking(models.Model):
else:
location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations()
- self.location_id = location_id
- self.location_dest_id = location_dest_id
+ if self.state == 'draft':
+ self.location_id = location_id
+ self.location_dest_id = location_dest_id
# TDE CLEANME move into onchange_partner_id
if self.partner_id and self.partner_id.picking_warn:
if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
diff --git a/addons/stock/tests/test_packing_neg.py b/addons/stock/tests/test_packing_neg.py
index 197711cf053..325f9012f9f 100644
--- a/addons/stock/tests/test_packing_neg.py
+++ b/addons/stock/tests/test_packing_neg.py
@@ -24,23 +24,24 @@ class TestPackingNeg(TransactionCase):
})
# Create an incoming picking for this product of 300 PCE from suppliers to stock
- default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
- default_get_vals.update({
+ vals = {
'name': 'Incoming picking (negative product)',
'partner_id': self.ref('base.res_partner_2'),
'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
'move_lines': [(0, 0, {
+ 'name': 'NEG',
'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
'product_uom_qty': 300.00,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': self.ref('stock.stock_location_stock'),
})],
- })
- pick_neg = self.env['stock.picking'].new(default_get_vals)
+ }
+ pick_neg = self.env['stock.picking'].create(vals)
pick_neg.onchange_picking_type()
pick_neg.move_lines.onchange_product_id()
- vals = pick_neg._convert_to_write(pick_neg._cache)
- pick_neg = self.env['stock.picking'].create(vals)
# Confirm and assign picking
pick_neg.action_confirm()
@@ -80,23 +81,24 @@ class TestPackingNeg(TransactionCase):
pick_neg.action_done()
# Make a delivery order of 300 pieces to the customer
- default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
- default_get_vals.update({
+ vals = {
'name': 'outgoing picking (negative product)',
'partner_id': self.ref('base.res_partner_4'),
'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
'move_lines': [(0, 0, {
+ 'name': 'NEG',
'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
'product_uom_qty': 300.00,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_customers'),
})],
- })
- delivery_order_neg = self.env['stock.picking'].new(default_get_vals)
+ }
+ delivery_order_neg = self.env['stock.picking'].create(vals)
delivery_order_neg.onchange_picking_type()
delivery_order_neg.move_lines.onchange_product_id()
- vals = delivery_order_neg._convert_to_write(delivery_order_neg._cache)
- delivery_order_neg = self.env['stock.picking'].create(vals)
# Assign and confirm
delivery_order_neg.action_confirm()
@@ -136,23 +138,24 @@ class TestPackingNeg(TransactionCase):
self.assertEqual(pallet_3_stock_qty, 50, "Should have 50 pieces in stock on pallet 3")
# Create a picking for reconciling the negative quant
- default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
- default_get_vals.update({
+ vals = {
'name': 'reconciling_delivery',
'partner_id': self.ref('base.res_partner_4'),
'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
'move_lines': [(0, 0, {
+ 'name': 'NEG',
'product_id': product_neg.id,
+ 'product_uom': product_neg.uom_id.id,
'product_uom_qty': 20.0,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': self.ref('stock.stock_location_stock'),
})],
- })
- delivery_reconcile = self.env['stock.picking'].new(default_get_vals)
+ }
+ delivery_reconcile = self.env['stock.picking'].create(vals)
delivery_reconcile.onchange_picking_type()
delivery_reconcile.move_lines.onchange_product_id()
- vals = delivery_reconcile._convert_to_write(delivery_reconcile._cache)
- delivery_reconcile = self.env['stock.picking'].create(vals)
# Receive 20 products with lot neg in stock with a new incoming shipment that should be on pallet 2
delivery_reconcile.action_confirm()
diff --git a/addons/stock/tests/test_proc_rule.py b/addons/stock/tests/test_proc_rule.py
index 535af8a1830..1679d40637f 100644
--- a/addons/stock/tests/test_proc_rule.py
+++ b/addons/stock/tests/test_proc_rule.py
@@ -27,26 +27,23 @@ class TestProcRule(TransactionCase):
'route_ids': [(4, product_route.id)]})
# Create Delivery Order of 10 `product.product_product_3` from Output -> Customer
- default_get_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
- default_get_vals.update({
+ product = self.env.ref('product.product_product_3')
+ vals = {
'name': 'Delivery order for procurement',
'partner_id': self.ref('base.res_partner_2'),
'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_output'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
'move_lines': [(0, 0, {
- 'product_id': self.ref('product.product_product_3'),
+ 'name': '/',
+ 'product_id': product.id,
+ 'product_uom': product.uom_id.id,
'product_uom_qty': 10.00,
'procure_method': 'make_to_order',
})],
- })
- pick_output = self.env['stock.picking'].new(default_get_vals)
- pick_output.onchange_picking_type()
- pick_output.move_lines.onchange_product_id()
- pick_output.update({
- 'location_id': self.ref('stock.stock_location_output'),
- 'location_dest_id': self.ref('stock.stock_location_customers'),
- })
- vals = pick_output._convert_to_write(pick_output._cache)
+ }
pick_output = self.env['stock.picking'].create(vals)
+ pick_output.move_lines.onchange_product_id()
# Confirm delivery order.
pick_output.action_confirm()
diff --git a/addons/stock/tests/test_wise_operator.py b/addons/stock/tests/test_wise_operator.py
index ec06ab01fc2..2bfd8c08785 100644
--- a/addons/stock/tests/test_wise_operator.py
+++ b/addons/stock/tests/test_wise_operator.py
@@ -17,25 +17,25 @@ class TestWiseOperator(TransactionCase):
'uom_po_id': self.ref('uom.product_uom_unit'),
})
- picking_default_vals = self.env['stock.picking'].default_get(list(self.env['stock.picking'].fields_get()))
-
# Create an incoming picking for this product of 10 PCE from suppliers to stock
- vals = dict(picking_default_vals, **{
+ vals = {
'name': 'Incoming picking (wise unit)',
'partner_id': self.ref('base.res_partner_2'),
'picking_type_id': self.ref('stock.picking_type_in'),
+ 'location_id': self.ref('stock.stock_location_suppliers'),
+ 'location_dest_id': self.ref('stock.stock_location_stock'),
'move_lines': [(0, 0, {
+ 'name': '/',
'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
'product_uom_qty': 10.00,
'location_id': self.ref('stock.stock_location_suppliers'),
'location_dest_id': self.ref('stock.stock_location_stock'),
})],
- })
- pick1_wise = self.env['stock.picking'].new(vals)
+ }
+ pick1_wise = self.env['stock.picking'].create(vals)
pick1_wise.onchange_picking_type()
pick1_wise.move_lines.onchange_product_id()
- vals = pick1_wise._convert_to_write(pick1_wise._cache)
- pick1_wise = self.env['stock.picking'].create(vals)
# Confirm and assign picking and prepare partial
pick1_wise.action_confirm()
@@ -65,22 +65,24 @@ class TestWiseOperator(TransactionCase):
self.assertEqual(len(records.ids), 3, "The number of quants created is not correct")
# Make a delivery order of 5 pieces to the customer
- vals = dict(picking_default_vals, **{
+ vals = {
'name': 'outgoing picking 1 (wise unit)',
'partner_id': self.ref('base.res_partner_4'),
'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
'move_lines': [(0, 0, {
+ 'name': '/',
'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
'product_uom_qty': 5.0,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_customers'),
})],
- })
- delivery_order_wise1 = self.env['stock.picking'].new(vals)
+ }
+ delivery_order_wise1 = self.env['stock.picking'].create(vals)
delivery_order_wise1.onchange_picking_type()
delivery_order_wise1.move_lines.onchange_product_id()
- vals = delivery_order_wise1._convert_to_write(delivery_order_wise1._cache)
- delivery_order_wise1 = self.env['stock.picking'].create(vals)
# Assign and confirm
delivery_order_wise1.action_confirm()
@@ -88,22 +90,24 @@ class TestWiseOperator(TransactionCase):
self.assertEqual(delivery_order_wise1.state, 'assigned')
# Make a delivery order of 5 pieces to the customer
- vals = dict(picking_default_vals, **{
+ vals = {
'name': 'outgoing picking 2 (wise unit)',
'partner_id': self.ref('base.res_partner_4'),
'picking_type_id': self.ref('stock.picking_type_out'),
+ 'location_id': self.ref('stock.stock_location_stock'),
+ 'location_dest_id': self.ref('stock.stock_location_customers'),
'move_lines': [(0, 0, {
+ 'name': '/',
'product_id': product_wise.id,
+ 'product_uom': product_wise.uom_id.id,
'product_uom_qty': 5.0,
'location_id': self.ref('stock.stock_location_stock'),
'location_dest_id': self.ref('stock.stock_location_customers'),
})],
- })
- delivery_order_wise2 = self.env['stock.picking'].new(vals)
+ }
+ delivery_order_wise2 = self.env['stock.picking'].create(vals)
delivery_order_wise2.onchange_picking_type()
delivery_order_wise2.move_lines.onchange_product_id()
- vals = delivery_order_wise2._convert_to_write(delivery_order_wise2._cache)
- delivery_order_wise2 = self.env['stock.picking'].create(vals)
# Assign and confirm
delivery_order_wise2.action_confirm()
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index 1d852bc3be2..79141bf0b06 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -265,8 +265,8 @@
-
-
+
+
diff --git a/addons/stock_dropshipping/models/__init__.py b/addons/stock_dropshipping/models/__init__.py
index c617a28246d..7d6f1be055f 100644
--- a/addons/stock_dropshipping/models/__init__.py
+++ b/addons/stock_dropshipping/models/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from . import purchase
\ No newline at end of file
+from . import purchase
+from . import sale
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
new file mode 100644
index 00000000000..81f975cbce6
--- /dev/null
+++ b/addons/stock_dropshipping/models/sale.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, models, fields
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ purchase_line_ids = fields.One2many('purchase.order.line', 'sale_line_id')
+
+ @api.multi
+ def _get_qty_procurement(self):
+ if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ qty = 0.0
+ for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
+ return qty
+ else:
+ return super(SaleOrderLine, self)._get_qty_procurement()
+
diff --git a/addons/stock_dropshipping/tests/__init__.py b/addons/stock_dropshipping/tests/__init__.py
index c060ad7247b..990bd0591e3 100644
--- a/addons/stock_dropshipping/tests/__init__.py
+++ b/addons/stock_dropshipping/tests/__init__.py
@@ -6,4 +6,3 @@ from . import test_dropship
from . import test_lifo_price
from . import test_procurement_exception
from . import test_stockvaluation
-
diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py
index 7f06c437fa3..9837d93f0af 100644
--- a/addons/stock_dropshipping/tests/test_dropship.py
+++ b/addons/stock_dropshipping/tests/test_dropship.py
@@ -5,6 +5,50 @@ from odoo.tests import common, Form
from odoo.tools import mute_logger
+class TestDropship(common.TransactionCase):
+ def test_change_qty(self):
+ # enable the dropship and MTO route on the product
+ prod = self.env.ref('product.product_product_8')
+ dropshipping_route = self.env.ref('stock_dropshipping.route_drop_shipping')
+ mto_route = self.env.ref('stock.route_warehouse0_mto')
+ prod.write({'route_ids': [(6, 0, [dropshipping_route.id, mto_route.id])]})
+
+ # add a vendor
+ vendor1 = self.env['res.partner'].create({'name': 'vendor1'})
+ seller1 = self.env['product.supplierinfo'].create({
+ 'name': vendor1.id,
+ 'price': 8,
+ })
+ prod.write({'seller_ids': [(6, 0, [seller1.id])]})
+
+ # sell one unit of this product
+ cust = self.env['res.partner'].create({'name': 'customer1'})
+ so = self.env['sale.order'].create({
+ 'partner_id': cust.id,
+ 'partner_invoice_id': cust.id,
+ 'partner_shipping_id': cust.id,
+ 'order_line': [(0, 0, {
+ 'name': prod.name,
+ 'product_id': prod.id,
+ 'product_uom_qty': 1.00,
+ 'product_uom': prod.uom_id.id,
+ 'price_unit': 12,
+ })],
+ 'pricelist_id': self.env.ref('product.list0').id,
+ 'picking_policy': 'direct',
+ })
+ so.action_confirm()
+ po = self.env['purchase.order'].search([('group_id', '=', so.procurement_group_id.id)])
+ po_line = po.order_line
+
+ # Check the qty on the P0
+ self.assertAlmostEqual(po_line.product_qty, 1.00)
+
+ # Update qty on SO and check PO
+ so.order_line.product_uom_qty = 2.00
+ self.assertAlmostEqual(po_line.product_qty, 2.00)
+
+
class TestDropship(common.TransactionCase):
def test_00_dropship(self):
diff --git a/addons/web/static/src/js/chrome/abstract_web_client.js b/addons/web/static/src/js/chrome/abstract_web_client.js
index 7a8b3ce8b01..68f35f272da 100644
--- a/addons/web/static/src/js/chrome/abstract_web_client.js
+++ b/addons/web/static/src/js/chrome/abstract_web_client.js
@@ -187,7 +187,9 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
// If it is not handled, we should display something clearer than the common crash_manager error dialog
// since it won't show anything except "Script error."
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
- if (message === "Script error." && !file && !line && !col && !error) {
+ if (!file && !line && !col) {
+ // Chrome and Opera set "Script error." on the `message` and hide the `error`
+ // Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
if (window.onOriginError) {
window.onOriginError();
delete window.onOriginError;
@@ -195,7 +197,7 @@ var AbstractWebClient = Widget.extend(ServiceProviderMixin, KeyboardNavigationMi
crash_manager.show_error({
type: _t("Odoo Client Error"),
message: _t("Unknown CORS error"),
- data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin.")},
+ data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
});
}
} else {
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index 45313b99428..f844cbdaed4 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -46,7 +46,7 @@ function genericJsonRpc (fct_name, params, settings, fct) {
});
// FIXME: jsonp?
deferred.abort = function () {
- deferred.reject('communication', $.Event(), 'abort', 'abort');
+ deferred.reject({message: "XmlHttpRequestError abort"}, $.Event('abort'));
if (xhr.abort) {
xhr.abort();
}
diff --git a/addons/web/static/src/js/core/session.js b/addons/web/static/src/js/core/session.js
index fcbc5872be0..694285cccdc 100644
--- a/addons/web/static/src/js/core/session.js
+++ b/addons/web/static/src/js/core/session.js
@@ -344,9 +344,9 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
}
};
- return deferred.then(function () {
+ var promise = deferred.then(function () {
if (aborted) {
- var def = $.Deferred().reject('communication', $.Event(), 'abort', 'abort');
+ var def = $.Deferred().reject({message: "XmlHttpRequestError abort"}, $.Event('abort'));
def.abort = function () {};
return def;
}
@@ -376,6 +376,9 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
xhrDef = fct(url, "call", params, options);
return xhrDef;
});
+
+ promise.abort = deferred.abort;
+ return promise;
},
url: function (path, params) {
params = _.extend(params || {});
diff --git a/addons/web/static/src/js/services/ajax_service.js b/addons/web/static/src/js/services/ajax_service.js
index bf754792932..0cecaae9da3 100644
--- a/addons/web/static/src/js/services/ajax_service.js
+++ b/addons/web/static/src/js/services/ajax_service.js
@@ -7,17 +7,20 @@ var session = require('web.session');
var AjaxService = AbstractService.extend({
rpc: function (route, args, options, target) {
- return $.Deferred(function (def) {
- session.rpc(route, args, options).then(function () {
- if (!target.isDestroyed()) {
- def.resolve.apply(def, arguments);
- }
- }, function () {
- if (!target.isDestroyed()) {
- def.reject.apply(def, arguments);
- }
- });
- }).promise();
+ var def = $.Deferred();
+ var promise = def.promise();
+ var xhrDef = session.rpc(route, args, options);
+ promise.abort = xhrDef.abort.bind(xhrDef);
+ xhrDef.then(function () {
+ if (!target.isDestroyed()) {
+ def.resolve.apply(def, arguments);
+ }
+ }).fail(function () {
+ if (!target.isDestroyed()) {
+ def.reject.apply(def, arguments);
+ }
+ });
+ return promise;
},
});
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 9c0bcdf37fe..905ea05ac1e 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -61,15 +61,11 @@ var KanbanModel = BasicModel.extend({
if (!groupByField || groupByField.type !== 'many2one') {
return $.Deferred().reject(); // only supported when grouped on m2o
}
- // ignore action default which may be for another model
- var context = _.pick(parent.context, function (value, key) {
- return key.slice(0,8) !== 'default_';
- });
return this._rpc({
model: groupByField.relation,
method: 'name_create',
args: [name],
- context: context, // todo: combine with view context
+ context: parent.context, // todo: combine with view context
})
.then(function (result) {
var newGroup = self._makeDataPoint({
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index b343f044bc2..15c3e24116f 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -200,6 +200,7 @@ var ListRenderer = BasicRenderer.extend({
reject = columnInvisibleFields[c.attrs.name];
}
if (!reject && c.attrs.widget === 'handle') {
+ self.hasHandle = true;
self.handleField = c.attrs.name;
}
return reject;
@@ -663,8 +664,6 @@ var ListRenderer = BasicRenderer.extend({
this._computeAggregates();
$table.toggleClass('o_list_view_grouped', this.isGrouped);
$table.toggleClass('o_list_view_ungrouped', !this.isGrouped);
- this.hasHandle = this.state.orderedBy.length === 0 ||
- this.state.orderedBy[0].name === this.handleField;
if (this.isGrouped) {
$table
.append(this._renderHeader(true))
diff --git a/addons/web/static/tests/chrome/action_manager_tests.js b/addons/web/static/tests/chrome/action_manager_tests.js
index 491d7d0f382..5cd9a216abf 100644
--- a/addons/web/static/tests/chrome/action_manager_tests.js
+++ b/addons/web/static/tests/chrome/action_manager_tests.js
@@ -1980,7 +1980,7 @@ QUnit.module('ActionManager', {
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
- self.iframe.src = 'test: ' + self.iframe.getAttribute('src');
+ self.iframe.src = 'test ' + self.iframe.getAttribute('src');
});
}
});
@@ -2002,7 +2002,7 @@ QUnit.module('ActionManager', {
if (route === '/report/check_wkhtmltopdf') {
return $.when('broken');
}
- if (route === 'test: /report/html/some_report') {
+ if (route === 'test /report/html/some_report') {
return $.when();
}
return this._super.apply(this, arguments);
@@ -2023,7 +2023,7 @@ QUnit.module('ActionManager', {
'/web/action/load',
'/report/check_wkhtmltopdf',
'notification',
- 'test: /report/html/some_report', // report client action's iframe
+ 'test /report/html/some_report', // report client action's iframe
]);
actionManager.destroy();
diff --git a/addons/web/static/tests/core/widget_tests.js b/addons/web/static/tests/core/widget_tests.js
index 19e72cb97b6..9510079e380 100644
--- a/addons/web/static/tests/core/widget_tests.js
+++ b/addons/web/static/tests/core/widget_tests.js
@@ -1,10 +1,12 @@
odoo.define('web.widget_tests', function (require) {
"use strict";
+var AjaxService = require('web.AjaxService');
var concurrency = require('web.concurrency');
var core = require('web.core');
var QWeb = require('web.QWeb');
var Widget = require('web.Widget');
+var testUtils = require('web.test_utils');
QUnit.module('core', {}, function () {
@@ -377,16 +379,47 @@ QUnit.module('core', {}, function () {
});
QUnit.test("calling _rpc on destroyed widgets", function (assert) {
- assert.expect(1);
+ assert.expect(3);
+
+ var def;
+ var parent = new Widget();
+ testUtils.addMockEnvironment(parent, {
+ session: {
+ rpc: function () {
+ def = $.Deferred();
+ def.abort = def.reject;
+ return def;
+ },
+ },
+ services: [AjaxService],
+ });
+ var widget = new Widget(parent);
+
+ widget._rpc({route: '/a/route'}).then(function () {
+ assert.ok(true, "The ajax call should be resolve");
+ });
+ def.resolve();
+ def = null;
- var widget = new Widget();
- widget.destroy();
widget._rpc({route: '/a/route'}).always(function () {
throw Error("Calling _rpc on a destroyed widget should return a " +
"deferred that is never resolved nor rejected");
});
+ widget.destroy();
+ def.resolve();
+ def = null;
+
+ widget._rpc({route: '/a/route'}).always(function () {
+ throw Error("Calling _rpc on a destroyed widget should return a " +
+ "deferred that is never resolved nor rejected");
+ });
+ assert.ok(!def,
+ "The trigger_up is not performed and the call returns a deferred "+
+ "never resolved nor rejected");
+
assert.ok(true,
"there should be no crash when calling _rpc on a destroyed widget");
+ parent.destroy();
});
QUnit.test('start is not called when widget is destroyed', function (assert) {
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index f5ddc0982d0..83bd275d9e0 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -111,7 +111,16 @@ var MockServer = Class.extend({
args = JSON.parse(JSON.stringify(args));
}
var def = this._performRpc(route, args);
- var abort = def.abort;
+
+ var abort = def.abort || def.reject;
+ if (abort) {
+ abort = abort.bind(def);
+ } else {
+ abort = function () {
+ throw new Error("Can't abort this request");
+ };
+ }
+
def = def.then(function (result) {
var resultString = JSON.stringify(result || false);
if (debug) {
@@ -125,8 +134,10 @@ var MockServer = Class.extend({
}
return $.Deferred().reject(errorString, event || $.Event());
});
- def.abort = abort;
- return def;
+
+ var promise = def.promise();
+ promise.abort = abort;
+ return promise;
},
//--------------------------------------------------------------------------
diff --git a/addons/web/static/tests/helpers/test_utils.js b/addons/web/static/tests/helpers/test_utils.js
index b1a0467254a..7c47bf5574a 100644
--- a/addons/web/static/tests/helpers/test_utils.js
+++ b/addons/web/static/tests/helpers/test_utils.js
@@ -497,15 +497,15 @@ function addMockEnvironment(widget, params) {
var services = {ajax: null}; // mocked ajax service already loaded
intercept(widget, 'call_service', function (ev) {
var args, result;
- if (ev.data.service === 'ajax') {
+ if (services[ev.data.service]) {
+ var service = services[ev.data.service];
+ args = (ev.data.args || []);
+ result = service[ev.data.method].apply(service, args);
+ } else if (ev.data.service === 'ajax') {
// ajax service is already mocked by the server
var route = ev.data.args[0];
args = ev.data.args[1];
result = mockServer.performRpc(route, args);
- } else if (services[ev.data.service]) {
- var service = services[ev.data.service];
- args = (ev.data.args || []);
- result = service[ev.data.method].apply(service, args);
}
ev.data.callback(result);
});
diff --git a/addons/web/static/tests/views/kanban_tests.js b/addons/web/static/tests/views/kanban_tests.js
index 399095f9427..c967b42a332 100644
--- a/addons/web/static/tests/views/kanban_tests.js
+++ b/addons/web/static/tests/views/kanban_tests.js
@@ -1490,43 +1490,6 @@ QUnit.module('Views', {
kanban.destroy();
});
- QUnit.test('m2o group record created does not use action defaults', function (assert) {
- assert.expect(1);
-
- var kanban = createView({
- View: KanbanView,
- model: 'partner',
- data: this.data,
- arch: '' +
- '' +
- '' +
- '' +
- ' ' +
- '' +
- '',
- groupBy: ['product_id'],
- mockRPC: function (route, args) {
- if (args.method === 'name_create') {
- assert.notOk(_.has(args.kwargs.context, 'default_name'),
- "default_* should be removed from context");
- }
- return this._super.apply(this, arguments);
- },
- viewOptions: {
- context: {
- default_name: 'default name partner',
- },
- },
- });
- kanban.renderButtons();
-
- kanban.$('.o_column_quick_create input').val('new product');
- kanban.$('.o_column_quick_create button.o_kanban_add').click();
-
- kanban.destroy();
- });
-
-
QUnit.test('quick create record fail in grouped', function (assert) {
assert.expect(8);
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index cafd529b47c..567e722c135 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -3312,76 +3312,6 @@ QUnit.module('Views', {
list.destroy();
});
- QUnit.test('editable list, handle widget locks and unlocks on sort', function (assert) {
- assert.expect(6);
-
- // we need another sortable field to lock/unlock the handle
- this.data.foo.fields.amount.sortable = true;
- // resequence makes sense on a sequence field, not on arbitrary fields
- this.data.foo.records[0].int_field = 0;
- this.data.foo.records[1].int_field = 1;
- this.data.foo.records[2].int_field = 2;
- this.data.foo.records[3].int_field = 3;
-
- var list = createView({
- View: ListView,
- model: 'foo',
- data: this.data,
- arch: '' +
- '' +
- '' +
- '',
- });
-
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.00500.00300.000.00',
- "default should be sorted by int_field");
-
- // Drag and drop the fourth line in second position
- testUtils.dragAndDrop(
- list.$('.ui-sortable-handle').eq(3),
- list.$('tbody tr').first(),
- {position: 'bottom'}
- );
-
- // Handle should be unlocked at this point
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.000.00500.00300.00',
- "drag and drop should have succeeded, as the handle is unlocked");
-
- // Sorting by a field different for int_field should lock the handle
- list.$('.o_column_sortable').eq(1).click();
-
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '0.00300.00500.001200.00',
- "should have been sorted by amount");
-
- // Drag and drop the fourth line in second position (not)
- testUtils.dragAndDrop(
- list.$('.ui-sortable-handle').eq(3),
- list.$('tbody tr').first(),
- {position: 'bottom'}
- );
-
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '0.00300.00500.001200.00',
- "drag and drop should have failed as the handle is locked");
-
- // Sorting by int_field should unlock the handle
- list.$('.o_column_sortable').eq(0).click();
-
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.000.00500.00300.00',
- "records should be ordered as per the previous resequence");
-
- // Drag and drop the fourth line in second position
- testUtils.dragAndDrop(
- list.$('.ui-sortable-handle').eq(3),
- list.$('tbody tr').first(),
- {position: 'bottom'}
- );
-
- assert.strictEqual(list.$('tbody span[name="amount"]').text(), '1200.00300.000.00500.00',
- "drag and drop should have worked as the handle is unlocked");
-
- list.destroy();
- });
-
QUnit.test('editable list with handle widget with slow network', function (assert) {
assert.expect(15);
diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js
index 4d58da38041..4eaf10aab53 100644
--- a/addons/web_settings_dashboard/static/src/js/dashboard.js
+++ b/addons/web_settings_dashboard/static/src/js/dashboard.js
@@ -5,6 +5,7 @@ var AbstractAction = require('web.AbstractAction');
var config = require('web.config');
var core = require('web.core');
var framework = require('web.framework');
+var session = require('web.session');
var Widget = require('web.Widget');
var QWeb = core.qweb;
diff --git a/addons/web_settings_dashboard/static/tests/dashboard_tests.js b/addons/web_settings_dashboard/static/tests/dashboard_tests.js
index ed66ac3ecb2..7b593969967 100644
--- a/addons/web_settings_dashboard/static/tests/dashboard_tests.js
+++ b/addons/web_settings_dashboard/static/tests/dashboard_tests.js
@@ -11,12 +11,12 @@ var Dashboard = webSettingsDashboard.Dashboard;
function createDashboard(params) {
var widget = new Widget();
var dashboard = new Dashboard(widget);
- dashboard.all_dashboards = ['invitations']; // test only user invitations
+ dashboard.all_dashboards = params.dashboards || ['invitations']; // test only user invitations
testUtils.addMockEnvironment(widget, params);
var originalDestroy = Dashboard.prototype.destroy;
- Dashboard.destroy = function () {
+ dashboard.destroy = function () {
dashboard.destroy = originalDestroy;
widget.destroy();
};
diff --git a/addons/website_crm_partner_assign/models/crm_lead.py b/addons/website_crm_partner_assign/models/crm_lead.py
index 14bfb2b002d..215b96e9457 100644
--- a/addons/website_crm_partner_assign/models/crm_lead.py
+++ b/addons/website_crm_partner_assign/models/crm_lead.py
@@ -86,19 +86,10 @@ class CrmLead(models.Model):
if lead.partner_latitude and lead.partner_longitude:
continue
if lead.country_id:
- result = geo_find(geo_query_address(street=lead.street,
- zip=lead.zip,
- city=lead.city,
- state=lead.state_id.name,
- country=lead.country_id.name))
-
- if result is None:
- result = geo_find(geo_query_address(
- city=lead.city,
- state=lead.state_id.name,
- country=lead.country_id.name
- ))
-
+ apikey = self.env['ir.config_parameter'].sudo().get_param('google.api_key_geocode')
+ result = self.env['res.partner']._geo_localize(apikey,
+ lead.street, lead.zip, lead.city,
+ lead.state_id.name, lead.country_id.name)
if result:
lead.write({
'partner_latitude': result[0],
diff --git a/addons/website_crm_partner_assign/tests/test_partner_assign.py b/addons/website_crm_partner_assign/tests/test_partner_assign.py
index 61374979bfc..1945e6bd2e4 100644
--- a/addons/website_crm_partner_assign/tests/test_partner_assign.py
+++ b/addons/website_crm_partner_assign/tests/test_partner_assign.py
@@ -16,7 +16,7 @@ class TestPartnerAssign(TransactionCase):
def setUp(self):
super(TestPartnerAssign, self).setUp()
- def geo_find(addr):
+ def geo_find(addr, apikey):
return {
'Wavre, Belgium': (50.7158956, 4.6128075),
'Cannon Hill Park, B46 3AG Birmingham, United Kingdom': (52.45216, -1.898578),
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index 8fb7e3c1435..4c586b404ac 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -98,7 +98,6 @@ class SaleOrder(models.Model):
'order_id': order_id,
'product_uom': product.uom_id.id,
'price_unit': pu,
- 'customer_lead': product.sale_delay,
}
@api.multi
@@ -257,4 +256,4 @@ class SaleOrderLine(models.Model):
record.name_short = record.product_id.with_context(display_default_code=False).display_name
def get_description_following_lines(self):
- return self.name.splitlines()[1:]
+ return self.name.splitlines()[1:]
\ No newline at end of file
diff --git a/addons/website_sale_stock/models/sale_order.py b/addons/website_sale_stock/models/sale_order.py
index 4ac8a67a871..2896163c673 100644
--- a/addons/website_sale_stock/models/sale_order.py
+++ b/addons/website_sale_stock/models/sale_order.py
@@ -31,6 +31,13 @@ class SaleOrder(models.Model):
values['warning'] = self.warning_stock
return values
+ @api.multi
+ def _website_product_id_change(self, order_id, product_id, qty=0):
+ res = super(SaleOrder, self)._website_product_id_change(order_id, product_id, qty=qty)
+ product = self.env['product.product'].browse(product_id)
+ res['customer_lead'] = product.sale_delay
+ return res
+
@api.multi
def _get_stock_warning(self, clear=True):
self.ensure_one()
diff --git a/doc/cla/corporate/camptocamp.md b/doc/cla/corporate/camptocamp.md
index 777bb974dc5..206fad26646 100644
--- a/doc/cla/corporate/camptocamp.md
+++ b/doc/cla/corporate/camptocamp.md
@@ -33,3 +33,4 @@ Akim Juillerat akim.juillerat@camptocamp.com https://github.com/grindtildeath
Damien Crier damien.crier@camptocamp.com https://github.com/damdam-s
Simone Orsi simahawk@gmail.com https://github.com/simahawk
Patrick Tombez patrick.tombez@camptocamp.com https://github.com/p-tombez
+Julien Coux julien.coux@camptocamp.com https://github.com/jcoux
diff --git a/doc/cla/corporate/initos.md b/doc/cla/corporate/initos.md
index 3c95fe2d07f..dd7fb8ea0e5 100644
--- a/doc/cla/corporate/initos.md
+++ b/doc/cla/corporate/initos.md
@@ -21,3 +21,4 @@ Peter Hahn peter.hahn@initos.com https://github.com/codingforfun
Claudia Haida claudia.haida@initos.com
Andreas Zöllner andreas.zoellner@initos.com https://github.com/azoellner
Rami Alwafaie rami.alwafaie@initos.com https://github.com/rami-wafaie
+Florian Kantelberg florian.kantelberg@initos.com https://github.com/fkantelberg
diff --git a/doc/cla/individual/ivantodorovich.md b/doc/cla/individual/ivantodorovich.md
new file mode 100644
index 00000000000..d3352e4a1f7
--- /dev/null
+++ b/doc/cla/individual/ivantodorovich.md
@@ -0,0 +1,11 @@
+Argentina, 2018-09-04
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Ivan Todorovich https://github.com/ivantodorovich
diff --git a/doc/cla/individual/rocketgithub.md b/doc/cla/individual/rocketgithub.md
new file mode 100644
index 00000000000..9b30fbb4d5c
--- /dev/null
+++ b/doc/cla/individual/rocketgithub.md
@@ -0,0 +1,11 @@
+Guatemala, 2018-09-04
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Rodrigo Fernandez rodrigo@solucionesprisma.com https://github.com/rocketgithub
diff --git a/doc/reference/views.rst b/doc/reference/views.rst
index 5dcac794ec2..052c63a97f5 100644
--- a/doc/reference/views.rst
+++ b/doc/reference/views.rst
@@ -957,6 +957,18 @@ sorted on the string of the field.
which means that non-stored function fields can not be used in pivot views
+In Pivot view a ``field`` can have a ``widget`` attribute to dictate its format.
+The widget should be a field formatter, of which the most interesting are
+``date``, ``datetime``, ``float_time``, and ``monetary``.
+
+For instance a timesheet pivot view could be defined as::
+
+
+
+
+
+
+
.. _reference/views/kanban:
Kanban
diff --git a/odoo/addons/base/data/res_country_data.xml b/odoo/addons/base/data/res_country_data.xml
index 54497ad8f97..2eaa0d0abf3 100644
--- a/odoo/addons/base/data/res_country_data.xml
+++ b/odoo/addons/base/data/res_country_data.xml
@@ -651,7 +651,7 @@
Guatemala
gt
-
+
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index f0cb0ef04df..aeaad1f3968 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -451,7 +451,7 @@ class Partner(models.Model):
""" Sync commercial fields and address fields from company and to children after create/update,
just as if those were all modeled as fields.related to the parent """
# 1. From UPSTREAM: sync from parent
- if values.get('parent_id') or values.get('type', 'contact'):
+ if values.get('parent_id') or values.get('type') == 'contact':
# 1a. Commercial fields: sync if parent changed
if values.get('parent_id'):
self._commercial_sync_from_company()
diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py
index 1f88db8d519..2d96b8b9877 100644
--- a/odoo/addons/base/models/res_users.py
+++ b/odoo/addons/base/models/res_users.py
@@ -1005,7 +1005,10 @@ class GroupsView(models.Model):
xml3.append(E.field(name=field_name, **attrs))
xml3.append({'class': "o_label_nowrap"})
- user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]}
+ if user_type_field_name:
+ user_type_attrs = {'invisible': [(user_type_field_name, '!=', group_employee.id)]}
+ else:
+ user_type_attrs = {}
xml = E.field(
E.group(*(xml1), col="2"),
diff --git a/odoo/addons/test_main_flows/__init__.py b/odoo/addons/test_main_flows/__init__.py
index ac45264fad4..ba375620a33 100644
--- a/odoo/addons/test_main_flows/__init__.py
+++ b/odoo/addons/test_main_flows/__init__.py
@@ -1,4 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-#from . import models
+from odoo import api, SUPERUSER_ID
+
+#
+# Conditional installation of enterprise modules.
+#
+# This module is defined in community but some steps (defined with 'edition: "enterprise"')
+# are only used to test enterprise. As it's not possible to direcly add enterprise
+# modules dependencies, this post install hook will install account_accountant if exists.
+#
+def _auto_install_enterprise_dependencies(cr, registry):
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ module_list = ['account_accountant']
+ module_ids = env['ir.module.module'].search([('name', 'in', module_list), ('state', '=', 'uninstalled')])
+ module_ids.sudo().button_install()
diff --git a/odoo/addons/test_main_flows/__manifest__.py b/odoo/addons/test_main_flows/__manifest__.py
index cc88c62cf50..cc8feb4fa59 100644
--- a/odoo/addons/test_main_flows/__manifest__.py
+++ b/odoo/addons/test_main_flows/__manifest__.py
@@ -13,4 +13,5 @@ It will install some main apps and will try to execute the most important action
'views/templates.xml',
],
'installable': True,
+ 'post_init_hook': '_auto_install_enterprise_dependencies',
}
diff --git a/odoo/addons/test_translation_import/i18n/fr.po b/odoo/addons/test_translation_import/i18n/fr.po
index 478682cd0a8..3e3abcfe2ab 100644
--- a/odoo/addons/test_translation_import/i18n/fr.po
+++ b/odoo/addons/test_translation_import/i18n/fr.po
@@ -35,7 +35,7 @@ msgid "test.translation.import"
msgstr "test.translation.import in french"
#. module: test_translation_import
-#: help:test.translation.import,name:0
+#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name
msgid "Efgh"
msgstr "Efgh in french"
diff --git a/odoo/addons/test_translation_import/i18n/test_translation_import.pot b/odoo/addons/test_translation_import/i18n/test_translation_import.pot
index 7d197cd620c..39333d2b363 100644
--- a/odoo/addons/test_translation_import/i18n/test_translation_import.pot
+++ b/odoo/addons/test_translation_import/i18n/test_translation_import.pot
@@ -41,7 +41,7 @@ msgid "test.translation.import"
msgstr ""
#. module: test_translation_import
-#: help:test.translation.import,name:0
+#: model:ir.model.fields,help:test_translation_import.field_test_translation_import__name
msgid "Efgh"
msgstr ""
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index 5d1404af5c0..a930e562687 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -1104,10 +1104,15 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
# discard the target from the POT targets.
src = dic['src']
- if src in pot_targets:
- target = pot_targets[src]
- target.value = dic['value']
- target.targets.discard((dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0))
+ target_key = (dic['type'], dic['name'], dic['type'] != 'code' and dic['res_id'] or 0)
+ target = pot_targets.get(src)
+ if not target or target_key not in target.targets:
+ _logger.info("Translation '%s' (%s, %s, %s) not found in reference pot, skipping",
+ src[:60], dic['type'], dic['name'], dic['res_id'])
+ return
+
+ target.value = dic['value']
+ target.targets.discard(target_key)
# This would skip terms that fail to specify a res_id
res_id = dic['res_id']
@@ -1142,7 +1147,6 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
if target.value:
for type, name, res_id in target.targets:
pot_rows.append((type, name, res_id, src, target.value, target.comments))
- pot_targets.clear()
for row in pot_rows:
process_row(row)
|