@@ -322,6 +323,7 @@
name="product_uom_qty" class="oe_inline"/>
+
@@ -480,6 +484,7 @@
+
diff --git a/addons/sale_expense/models/analytic.py b/addons/sale_expense/models/analytic.py
index 9f037bfcae1..d387091adc1 100644
--- a/addons/sale_expense/models/analytic.py
+++ b/addons/sale_expense/models/analytic.py
@@ -10,7 +10,7 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _sale_determine_order(self):
mapping = super(AccountAnalyticLine, self)._sale_determine_order()
- for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy != 'no'):
+ for analytic_line in self.sudo().filtered(lambda aal: not aal.so_line and aal.product_id and aal.product_id.expense_policy not in [False, 'no']):
if analytic_line.move_id.expense_id.sale_order_id:
mapping[analytic_line.id] = analytic_line.move_id.expense_id.sale_order_id
return mapping
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 50444266362..78666bc2669 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -387,7 +387,7 @@ class StockMove(models.Model):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
rounding = move.product_id.uom_id.rounding
- qty_done = 0.0
+ qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
if float_is_zero(product_tot_qty_available, precision_rounding=rounding):
new_std_price = move._get_price_unit()
elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
@@ -396,7 +396,6 @@ class StockMove(models.Model):
else:
# Get the standard price
amount_unit = std_price_update.get((move.company_id.id, move.product_id.id)) or move.product_id.standard_price
- qty_done = move.product_uom._compute_quantity(move.quantity_done, move.product_id.uom_id)
qty = forced_qty or qty_done
new_std_price = ((amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / (product_tot_qty_available + qty_done)
diff --git a/addons/stock_account/tests/test_stockvaluation.py b/addons/stock_account/tests/test_stockvaluation.py
index 39ced676a1a..4afd3706d5e 100644
--- a/addons/stock_account/tests/test_stockvaluation.py
+++ b/addons/stock_account/tests/test_stockvaluation.py
@@ -2587,6 +2587,40 @@ class TestStockValuation(TransactionCase):
self.assertAlmostEqual(self.product1.qty_at_date, 0.0)
self.assertAlmostEqual(self.product1.stock_value, 0.0)
+ def test_average_perpetual_6(self):
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ move1 = self.env['stock.move'].create({
+ 'name': 'Receive 1 unit at 10',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 10,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move1.move_line_ids.qty_done = 1.0
+
+ move2 = self.env['stock.move'].create({
+ 'name': 'Receive 1 units at 5',
+ 'location_id': self.supplier_location.id,
+ 'location_dest_id': self.stock_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ 'price_unit': 5,
+ })
+ move2._action_confirm()
+ move2._action_assign()
+ move2.move_line_ids.qty_done = 1.0
+
+ # Receive both at the same time
+ (move1 | move2)._action_done()
+
+ self.assertAlmostEqual(self.product1.standard_price, 7.5)
+
def test_average_negative_1(self):
""" Test edit in the past. Receive 10, send 20, edit the second move to only send 10.
"""
diff --git a/addons/stock_landed_costs/models/stock_landed_cost.py b/addons/stock_landed_costs/models/stock_landed_cost.py
index 07aa7be9a51..ca1b544f868 100644
--- a/addons/stock_landed_costs/models/stock_landed_cost.py
+++ b/addons/stock_landed_costs/models/stock_landed_cost.py
@@ -307,6 +307,9 @@ class AdjustmentLines(models.Model):
return False
accounts = self.product_id.product_tmpl_id.get_product_accounts()
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
+ # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
+ if self.move_id._is_dropshipped():
+ debit_account_id = accounts.get('expense') and accounts['expense'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 60b0275c8c8..ff9d6359dd1 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -240,6 +240,12 @@ var FieldMany2One = AbstractField.extend({
*/
_bindAutoComplete: function () {
var self = this;
+ // avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
+ if (this.$input.attr('placeholder')) {
+ this.$input.attr('placeholder', function (index, val) {
+ return val.split('').join('\ufeff');
+ });
+ }
this.$input.autocomplete({
source: function (req, resp) {
_.each(self._autocompleteSources, function (source) {
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index b7c8e8a6214..45b68ce6bea 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -228,17 +228,16 @@ var FormController = BasicController.extend({
// are displayed with an alert
var fields = self.renderer.state.fields;
var data = self.renderer.state.data;
- var alertFields = [];
+ var alertFields = {};
for (var k = 0; k < changedFields.length; k++) {
var field = fields[changedFields[k]];
var fieldData = data[changedFields[k]];
if (field.translate && fieldData) {
- alertFields.push(field);
+ alertFields[changedFields[k]] = field;
}
}
- if (alertFields.length) {
- self.renderer.alertFields = alertFields;
- self.renderer.displayTranslationAlert();
+ if (!_.isEmpty(alertFields)) {
+ self.renderer.updateAlertFields(alertFields);
}
}
return changedFields;
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index c7a32b72ba9..91146ccd504 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -13,6 +13,7 @@ var FormRenderer = BasicRenderer.extend({
className: "o_form_view",
events: _.extend({}, BasicRenderer.prototype.events, {
'click .o_notification_box .oe_field_translate': '_onTranslate',
+ 'click .o_notification_box .close': '_onTranslateNotificationClose',
'click .oe_title, .o_inner_group': '_onClick',
}),
custom_events: _.extend({}, BasicRenderer.prototype.custom_events, {
@@ -30,6 +31,7 @@ var FormRenderer = BasicRenderer.extend({
this._super.apply(this, arguments);
this.idsForLabels = {};
this.lastActivatedFieldIndex = -1;
+ this.alertFields = {};
},
/**
* @override
@@ -103,23 +105,32 @@ var FormRenderer = BasicRenderer.extend({
});
return fieldNames;
},
+ /*
+ * Updates translation alert fields for the current state and display updated fields
+ *
+ * @param {Object} alertFields
+ */
+ updateAlertFields: function (alertFields) {
+ this.alertFields[this.state.res_id] = _.extend(this.alertFields[this.state.res_id] || {}, alertFields);
+ this.displayTranslationAlert();
+ },
/**
* Show a warning message if the user modified a translated field. For each
* field, the notification provides a link to edit the field's translations.
- *
- * @param {Object[]} alertFields field list
*/
displayTranslationAlert: function () {
this.$('.o_notification_box').remove();
- var $notification = $(qweb.render('notification-box', {type: 'info'}))
- .append(qweb.render('translation-alert', {
- fields: this.alertFields,
- lang: _t.database.parameters.name
- }));
- if (this.$('.o_form_statusbar').length) {
- this.$('.o_form_statusbar').after($notification);
- } else {
- this.$el.prepend($notification);
+ if (this.alertFields[this.state.res_id]) {
+ var $notification = $(qweb.render('notification-box', {type: 'info'}))
+ .append(qweb.render('translation-alert', {
+ fields: this.alertFields[this.state.res_id],
+ lang: _t.database.parameters.name
+ }));
+ if (this.$('.o_form_statusbar').length) {
+ this.$('.o_form_statusbar').after($notification);
+ } else {
+ this.$el.prepend($notification);
+ }
}
},
/**
@@ -905,6 +916,9 @@ var FormRenderer = BasicRenderer.extend({
return $.when.apply($, defs).then(function () {
self._updateView($form.contents());
+ if (self.state.res_id in self.alertFields) {
+ self.displayTranslationAlert();
+ }
}, function () {
$form.remove();
}).then(function(){
@@ -1015,6 +1029,15 @@ var FormRenderer = BasicRenderer.extend({
event.preventDefault();
this.trigger_up('translate', {fieldName: event.target.name, id: this.state.id});
},
+ /**
+ * remove alert fields of record from alertFields object
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onTranslateNotificationClose: function(ev) {
+ delete this.alertFields[this.state.res_id];
+ },
});
return FormRenderer;
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 30f160460cd..5a416938362 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1525,7 +1525,7 @@
Please update translations of :
- ,
+ ,
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index e5b5f0cf36f..47db6dbe2f4 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -16,6 +16,7 @@ var Widget = require('web.Widget');
var _t = core._t;
var createView = testUtils.createView;
+var createActionManager = testUtils.createActionManager;
var createAsyncView = testUtils.createAsyncView;
var createActionManager = testUtils.createActionManager;
@@ -5647,9 +5648,10 @@ QUnit.module('Views', {
});
QUnit.test('display translation alert', function (assert) {
- assert.expect(1);
+ assert.expect(2);
this.data.partner.fields.foo.translate = true;
+ this.data.partner.fields.display_name.translate = true;
var multi_lang = _t.database.multi_lang;
_t.database.multi_lang = true;
@@ -5662,6 +5664,7 @@ QUnit.module('Views', {
'' +
'' +
'' +
+ '' +
'' +
'' +
'',
@@ -5671,14 +5674,154 @@ QUnit.module('Views', {
form.$buttons.find('.o_form_button_edit').click();
form.$('input[name="foo"]').val("test").trigger("input");
form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 1,
+ "should have single translation alert");
- assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,"should have a translation alert");
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input[name="display_name"]').val("test2").trigger("input");
+ form.$buttons.find('.o_form_button_save').click();
+ assert.strictEqual(form.$('.o_form_view > .alert > div .oe_field_translate').length, 2,
+ "should have two translate fields in translation alert");
form.destroy();
_t.database.multi_lang = multi_lang;
});
+ QUnit.test('translation alerts are preserved on pager change', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.fields.foo.translate = true;
+
+ var multi_lang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ viewOptions: {
+ ids: [1, 2],
+ index: 0,
+ },
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_edit').click();
+ form.$('input[name="foo"]').val("test").trigger("input");
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
+ "should have a translation alert");
+
+ // click on the pager to switch to the next record
+ form.pager.$('.o_pager_next').click();
+ assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
+ "should not have a translation alert");
+
+ // click on the pager to switch back to the previous record
+ form.pager.$('.o_pager_previous').click();
+ assert.strictEqual(form.$('.o_form_view > .alert > div').length, 1,
+ "should have a translation alert");
+
+ // remove translation alert by click X and check alert even after form reload
+ form.$('.o_form_view > .alert > .close').click();
+ assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
+ "should not have a translation alert");
+ form.reload();
+ assert.strictEqual(form.$('.o_form_view > .alert > div').length, 0,
+ "should not have a translation alert after reload");
+
+ form.destroy();
+ _t.database.multi_lang = multi_lang;
+ });
+
+ QUnit.test('translation alerts preseved on reverse breadcrumb', function (assert) {
+ assert.expect(2);
+
+ this.data['ir.translation'] = {
+ fields: {
+ name: { string: "name", type: "char" },
+ source: {string: "Source", type: "char"},
+ value: {string: "Value", type: "char"},
+ },
+ records: [],
+ };
+
+ this.data.partner.fields.foo.translate = true;
+
+ var multi_lang = _t.database.multi_lang;
+ _t.database.multi_lang = true;
+
+ var archs = {
+ 'partner,false,form': '',
+ 'partner,false,search': '',
+ 'ir.translation,false,list': '' +
+ '' +
+ '' +
+ '' +
+ '',
+ 'ir.translation,false,search': '',
+ };
+
+ var actions = [{
+ id: 1,
+ name: 'Partner',
+ res_model: 'partner',
+ type: 'ir.actions.act_window',
+ views: [[false, 'form']],
+ }, {
+ id: 2,
+ name: 'Translate',
+ res_model: 'ir.translation',
+ type: 'ir.actions.act_window',
+ views: [[false, 'list']],
+ target: 'current',
+ flags: {'search_view': true, 'action_buttons': true},
+ }];
+
+ var actionManager = createActionManager({
+ actions: actions,
+ archs: archs,
+ data: this.data,
+ });
+
+ actionManager.doAction(1);
+
+ actionManager.controlPanel.$el.find('.o_form_button_edit').click();
+ actionManager.$('input[name="foo"]').val("test").trigger("input");
+ actionManager.controlPanel.$el.find('.o_form_button_save').click();
+
+ assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
+ "should have a translation alert");
+
+ var currentController = actionManager.getCurrentController().widget;
+ actionManager.doAction(2, {
+ on_reverse_breadcrumb: function () {
+ if (!_.isEmpty(currentController.renderer.alertFields)) {
+ currentController.renderer.displayTranslationAlert();
+ }
+ return false;
+ },
+ });
+
+ $('.o_control_panel .breadcrumb a:first').click();
+ assert.strictEqual(actionManager.$('.o_form_view > .alert > div').length, 1,
+ "should have a translation alert");
+
+ actionManager.destroy();
+ _t.database.multi_lang = multi_lang;
+ });
+
QUnit.test('translate event correctly handled with multiple controllers', function (assert) {
assert.expect(3);
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 27e4419afe9..7bb6f77a649 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -424,7 +424,7 @@ class Web_Editor(http.Controller):
(note: searching for the URL string is not enough as it could appear in a comment or an xpath expression.)
"""
- return bool(etree.XML(view.arch).xpath("link[@href='{}']".format(url)))
+ return bool(etree.XML(view.arch).xpath("//link[@href='{}']".format(url)))
view_to_xpath = IrUiView.get_related_views(bundle_xmlid, bundles=True).filtered(views_linking_url)
diff --git a/doc/cla/corporate/levelprime.md b/doc/cla/corporate/levelprime.md
index bca6d6aa7d9..d2d579302d8 100644
--- a/doc/cla/corporate/levelprime.md
+++ b/doc/cla/corporate/levelprime.md
@@ -9,9 +9,12 @@ declaration.
Signed,
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
+Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
List of contributors:
Roberto Fichera roberto.fichera@levelprime.com https://github.com/robyf70
Roberto Fichera robyf@tekno-soft.it https://github.com/robyf70
+Filippo Iovine - filippo.iovine@levelprime.com - https://github.com/FilippoIOVINE
+Filippo Iovine - filippo.jovine@gmail.com - https://github.com/FilippoIOVINE
diff --git a/doc/cla/individual/liweijie0812.md b/doc/cla/individual/liweijie0812.md
new file mode 100644
index 00000000000..d13cbaf1dbd
--- /dev/null
+++ b/doc/cla/individual/liweijie0812.md
@@ -0,0 +1,11 @@
+china, 2019-01-16
+
+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,
+
+liweijie 674416404@qq.com https://github.com/liweijie0812
diff --git a/doc/cla/individual/mohsenwaleed.md b/doc/cla/individual/mohsenwaleed.md
new file mode 100644
index 00000000000..4470fb3bc93
--- /dev/null
+++ b/doc/cla/individual/mohsenwaleed.md
@@ -0,0 +1,11 @@
+Yemen, 2019-01-22
+
+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,
+
+Waleed Mohsen Mohsen.Waleed@gmail.com https://github.com/MohsenWaleed
diff --git a/doc/cla/individual/xyvs.md b/doc/cla/individual/xyvs.md
new file mode 100644
index 00000000000..8262dfb72a5
--- /dev/null
+++ b/doc/cla/individual/xyvs.md
@@ -0,0 +1,11 @@
+Mexico, 18-01-2018
+
+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,
+
+José Francisco MartÃnez Salgado admin@jofranmtz.com https://github.com/xyvs
diff --git a/doc/conf.py b/doc/conf.py
index 84b8b9289f4..3563433ff9d 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -180,8 +180,6 @@ todo_include_todos = False
intersphinx_mapping = {
'python': ('https://docs.python.org/3/', None),
'werkzeug': ('http://werkzeug.pocoo.org/docs/', None),
- 'sqlalchemy': ('http://docs.sqlalchemy.org/en/rel_0_9/', None),
- 'django': ('https://django.readthedocs.org/en/latest/', None),
}
github_user = 'odoo'
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index 49dda7f249b..1bbf53794a6 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -403,6 +403,11 @@ class IrModelFields(models.Model):
raise UserError(_("The Selection Options expression is not a valid Pythonic expression. "
"Please provide an expression in the [('key','Label'), ...] format."))
+ @api.constrains('domain')
+ def _check_domain(self):
+ for field in self:
+ safe_eval(field.domain or '[]')
+
@api.constrains('name', 'state')
def _check_name(self):
for field in self:
diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py
index 802711f5f5c..6c700bed92d 100644
--- a/odoo/addons/base/models/ir_ui_view.py
+++ b/odoo/addons/base/models/ir_ui_view.py
@@ -394,7 +394,10 @@ actual arch.
def _compute_defaults(self, values):
if 'inherit_id' in values:
- values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
+ # Do not automatically change the mode if the view already has an inherit_id,
+ # and the user change it to another.
+ if not values['inherit_id'] or all(not view.inherit_id for view in self):
+ values.setdefault('mode', 'extension' if values['inherit_id'] else 'primary')
return values
@api.model_create_multi
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index e1e38dd506d..461fe6277c3 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -1573,6 +1573,12 @@ class ViewModeField(ViewCase):
})
self.assertEqual(view2.mode, 'extension')
+ view2.write({'inherit_id': None})
+ self.assertEqual(view2.mode, 'primary')
+
+ view2.write({'inherit_id': view.id})
+ self.assertEqual(view2.mode, 'extension')
+
@mute_logger('odoo.sql_db')
def testModeExplicit(self):
view = self.View.create({
@@ -1585,6 +1591,7 @@ class ViewModeField(ViewCase):
'arch': ''
})
self.assertEqual(view.mode, 'primary')
+ self.assertEqual(view2.mode, 'primary')
with self.assertRaises(IntegrityError):
self.View.create({
@@ -1636,6 +1643,27 @@ class ViewModeField(ViewCase):
view.write({'mode': 'primary'})
+ def testChangeInheritOfPrimary(self):
+ """
+ A primary view with an inherit_id must remain primary when changing the inherit_id
+ """
+ base1 = self.View.create({
+ 'inherit_id': None,
+ 'arch': '',
+ })
+ base2 = self.View.create({
+ 'inherit_id': None,
+ 'arch': '',
+ })
+ view = self.View.create({
+ 'mode': 'primary',
+ 'inherit_id': base1.id,
+ 'arch': '',
+ })
+ self.assertEqual(view.mode, 'primary')
+ view.write({'inherit_id': base2.id})
+ self.assertEqual(view.mode, 'primary')
+
class TestDefaultView(ViewCase):
def testDefaultViewBase(self):