summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/account/views/account_payment_view.xml2
-rw-r--r--addons/account_check_printing/models/account_payment.py2
-rw-r--r--addons/barcodes/static/src/js/barcode_form_view.js23
-rw-r--r--addons/barcodes/static/tests/barcode_tests.js132
-rw-r--r--addons/board/static/src/js/board_view.js8
-rw-r--r--addons/board/static/tests/dashboard_tests.js16
-rw-r--r--addons/calendar/models/calendar.py23
-rw-r--r--addons/fleet/models/fleet_vehicle.py2
-rw-r--r--addons/l10n_be_hr_payroll_fleet/models/fleet.py13
-rw-r--r--addons/mail/models/mail_channel.py2
-rw-r--r--addons/mass_mailing/wizard/test_mailing.py2
-rw-r--r--addons/mrp/models/mrp_workorder.py39
-rw-r--r--addons/pad/models/pad.py4
-rw-r--r--addons/payment/controllers/portal.py4
-rw-r--r--addons/sale/models/sale.py2
-rw-r--r--addons/stock/views/procurement_views.xml1
-rw-r--r--addons/stock/views/stock_location_views.xml2
-rw-r--r--addons/stock_account/views/product_views.xml2
-rw-r--r--addons/stock_account/wizard/stock_quantity_history.py2
-rw-r--r--addons/transifex/models/ir_translation.py7
-rw-r--r--addons/web/controllers/main.py19
-rw-r--r--addons/web/static/src/js/chrome/web_client.js7
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js3
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js15
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js13
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js3
-rw-r--r--addons/web/static/src/less/fields.less4
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js29
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js58
-rw-r--r--addons/web/static/tests/views/pivot_tests.js41
-rw-r--r--addons/web_editor/controllers/main.py4
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js84
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js10
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js13
-rw-r--r--doc/cla/corporate/vauxoo.md1
-rw-r--r--doc/reference/reports.rst2
-rw-r--r--doc/setup/deploy.rst10
-rw-r--r--odoo/addons/base/data/res.lang.csv2
-rw-r--r--odoo/addons/base/i18n/base.pot12
-rw-r--r--odoo/addons/base/models/ir_model.py7
40 files changed, 527 insertions, 98 deletions
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index a534992cc8f..480b1378a29 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -144,7 +144,7 @@
<group>
<group>
<field name="payment_type" widget="radio" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
- <field name="partner_type" widget="selection" attrs="{'required': [('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}"/>
+ <field name="partner_type" widget="selection" attrs="{'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}"/>
<field name="partner_id" attrs="{'required': [('state', '=', 'draft'), ('payment_type', 'in', ('inbound', 'outbound'))], 'invisible': [('payment_type', 'not in', ('inbound', 'outbound'))], 'readonly': [('state', '!=', 'draft')]}" context="{'default_is_company': True, 'default_supplier': payment_type == 'outbound', 'default_customer': payment_type == 'inbound'}"/>
<label for="amount"/>
<div name="amount_div" class="o_row">
diff --git a/addons/account_check_printing/models/account_payment.py b/addons/account_check_printing/models/account_payment.py
index 3bcbfc39d55..9bd4a429cf5 100644
--- a/addons/account_check_printing/models/account_payment.py
+++ b/addons/account_check_printing/models/account_payment.py
@@ -44,7 +44,7 @@ class AccountPayment(models.Model):
_inherit = "account.payment"
check_amount_in_words = fields.Char(string="Amount in Words")
- check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing')
+ check_manual_sequencing = fields.Boolean(related='journal_id.check_manual_sequencing', readonly=1)
check_number = fields.Integer(string="Check Number", readonly=True, copy=False,
help="The selected journal is configured to print check numbers. If your pre-printed check paper already has numbers "
"or if the current numbering is wrong, you can change it in the journal configuration page.")
diff --git a/addons/barcodes/static/src/js/barcode_form_view.js b/addons/barcodes/static/src/js/barcode_form_view.js
index 1730b42290e..015119c5830 100644
--- a/addons/barcodes/static/src/js/barcode_form_view.js
+++ b/addons/barcodes/static/src/js/barcode_form_view.js
@@ -146,10 +146,10 @@ FormController.include({
* @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Deferred}
*/
- _barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode) {
+ _barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode, quantity) {
var changes = {};
var candidateChanges = {};
- candidateChanges[activeBarcode.quantity] = candidate.data[activeBarcode.quantity] + 1;
+ candidateChanges[activeBarcode.quantity] = quantity ? quantity : candidate.data[activeBarcode.quantity] + 1;
changes[activeBarcode.fieldName] = {
operation: 'UPDATE',
id: candidate.id,
@@ -283,15 +283,14 @@ FormController.include({
function (reserved) {return barcode.indexOf(reserved) === 0;});
var hasCommand = false;
var defs = [];
+ if (! $.contains(target, self.el)) {
+ return;
+ }
for (var k in self.activeBarcode) {
var activeBarcode = self.activeBarcode[k];
// Handle the case where there are several barcode widgets on the same page. Since the
// event is global on the page, all barcode widgets will be triggered. However, we only
// want to keep the event on the target widget.
- if (! $.contains(target, self.el)) {
- continue;
- }
-
var methods = self.activeBarcode[k].commands;
var method = prefixed ? methods[barcode] : methods.barcode;
if (method) {
@@ -306,6 +305,8 @@ FormController.include({
}
return self.alive($.when.apply($, defs)).then(function () {
if (!prefixed) {
+ // remember the barcode scanned for the quantity listener
+ self.current_barcode = barcode;
// redraw the view if we scanned a real barcode (required if
// we manually apply the change in JS, e.g. incrementing the
// quantity)
@@ -321,6 +322,9 @@ FormController.include({
_quantityListener: function (event) {
var character = String.fromCharCode(event.which);
+ if (! $.contains(event.target, this.el)) {
+ return;
+ }
// only catch the event if we're not focused in
// another field and it's a number
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
@@ -355,9 +359,10 @@ FormController.include({
title: _t('Set quantity'),
buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
var new_qty = this.$content.find('.o_set_qty_input').val();
- var values = {};
- values[activeBarcode.quantity] = parseFloat(new_qty);
- return self.model.notifyChanges(activeBarcode.candidate.id, values).then(function () {
+ var record = self.model.get(self.handle);
+ return self._barcodeSelectedCandidate(activeBarcode.candidate, record,
+ self.current_barcode, activeBarcode, parseFloat(new_qty))
+ .then(function () {
self.update({}, {reload: false});
});
}}, {text: _t('Discard'), close: true}],
diff --git a/addons/barcodes/static/tests/barcode_tests.js b/addons/barcodes/static/tests/barcode_tests.js
index cf77497a2a7..b2a2bf46126 100644
--- a/addons/barcodes/static/tests/barcode_tests.js
+++ b/addons/barcodes/static/tests/barcode_tests.js
@@ -404,4 +404,136 @@ QUnit.test('specification of widget barcode_handler', function (assert) {
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
delete fieldRegistry.map.test_barcode_handler;
});
+
+QUnit.test('specification of widget barcode_handler with keypress and notifyChange', function (assert) {
+ assert.expect(6);
+ var done = assert.async();
+
+ var delay = barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms;
+ barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = 0;
+
+ this.data.order.onchanges = {
+ _barcode_scanned: function () {},
+ };
+
+ // Define a specific barcode_handler widget for this test case
+ var TestBarcodeHandler = AbstractField.extend({
+ init: function () {
+ this._super.apply(this, arguments);
+
+ this.trigger_up('activeBarcode', {
+ name: 'test',
+ fieldName: 'line_ids',
+ notifyChange: false,
+ setQuantityWithKeypress: true,
+ quantity: 'quantity',
+ commands: {
+ barcode: '_barcodeAddX2MQuantity',
+ }
+ });
+ },
+ });
+ fieldRegistry.add('test_barcode_handler', TestBarcodeHandler);
+
+ var form = createView({
+ View: FormView,
+ model: 'order',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="_barcode_scanned" widget="test_barcode_handler"/>' +
+ '<field name="line_ids">' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '<field name="product_barcode" invisible="1"/>' +
+ '<field name="quantity"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+ _.each(['1','2','3','4','5','6','7','8','9','0','Enter'], triggerKeypressEvent);
+ // Quantity listener should open a dialog.
+ triggerKeypressEvent('5');
+
+ setTimeout(function () {
+ var keycode = $.ui.keyCode.ENTER;
+
+ assert.strictEqual($('.modal .modal-body').length, 1, 'should open a modal with a quantity as input');
+ assert.strictEqual($('.modal .modal-body .o_set_qty_input').val(), '5', 'the quantity by default in the modal shoud be 5');
+
+ $('.modal .modal-body .o_set_qty_input').val('7');
+
+ $('.modal .modal-body .o_set_qty_input').trigger($.Event('keypress', {which: keycode, keyCode: keycode}));
+ assert.strictEqual(form.$('.o_data_row .o_data_cell:nth(1)').text(), '7',
+ "quantity checked should be 7");
+
+ assert.verifySteps(['read', 'read']);
+
+ form.destroy();
+ barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
+ delete fieldRegistry.map.test_barcode_handler;
+ done();
+ });
+});
+QUnit.test('barcode_scanned only trigger error for active view', function (assert) {
+ assert.expect(2);
+
+ this.data.order_line.fields._barcode_scanned = {string: 'Barcode scanned', type: 'char'};
+
+ var form = createView({
+ View: FormView,
+ model: 'order',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="_barcode_scanned" widget="barcode_handler"/>' +
+ '<field name="line_ids">' +
+ '<tree>' +
+ '<field name="product_id"/>' +
+ '<field name="product_barcode" invisible="1"/>' +
+ '<field name="quantity"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ archs: {
+ "order_line,false,form":
+ '<form string="order line">' +
+ '<field name="_barcode_scanned" widget="barcode_handler"/>' +
+ '<field name="product_id"/>' +
+ '</form>',
+ },
+ res_id: 1,
+ intercepts: {
+ warning: function (event) {
+ assert.step(event.name);
+ }
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ form.$('.o_data_row:first').click();
+
+ // We do not trigger on the body since modal and
+ // form view are both inside it.
+ function modalTriggerKeypressEvent(char) {
+ var keycode;
+ if (char === "Enter") {
+ keycode = $.ui.keyCode.ENTER;
+ } else {
+ keycode = char.charCodeAt(0);
+ }
+ return $('.modal').trigger($.Event('keypress', {which: keycode, keyCode: keycode}));
+ }
+ _.each(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], modalTriggerKeypressEvent);
+ assert.verifySteps(['warning'], "only one event should be triggered");
+ form.destroy();
+});
});
diff --git a/addons/board/static/src/js/board_view.js b/addons/board/static/src/js/board_view.js
index e5fd56ad049..f65e6bb1e85 100644
--- a/addons/board/static/src/js/board_view.js
+++ b/addons/board/static/src/js/board_view.js
@@ -28,7 +28,7 @@ var BoardController = FormController.extend({
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
- this.viewID = params.viewID;
+ this.customViewID = params.customViewID;
},
//--------------------------------------------------------------------------
@@ -58,9 +58,9 @@ var BoardController = FormController.extend({
var board = this.renderer.getBoard();
var arch = QWeb.render('DashBoard.xml', _.extend({}, board));
return this._rpc({
- route: '/web/view/add_custom',
+ route: '/web/view/edit_custom',
params: {
- view_id: this.viewID,
+ custom_id: this.customViewID,
arch: arch,
}
}).then(dataManager.invalidate.bind(dataManager));
@@ -426,7 +426,7 @@ var BoardView = FormView.extend({
*/
init: function (viewInfo) {
this._super.apply(this, arguments);
- this.controllerParams.viewID = viewInfo.view_id;
+ this.controllerParams.customViewID = viewInfo.custom_view_id;
},
});
diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js
index 1ef295e3962..1211fc5da05 100644
--- a/addons/board/static/tests/dashboard_tests.js
+++ b/addons/board/static/tests/dashboard_tests.js
@@ -126,8 +126,8 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [['foo', '!=', 'False']], "the domain should be passed");
}
- if (route === '/web/view/add_custom') {
- assert.step('add custom');
+ if (route === '/web/view/edit_custom') {
+ assert.step('edit custom');
return $.when(true);
}
return this._super.apply(this, arguments);
@@ -158,7 +158,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
form.$('.oe_fold').click();
assert.ok(form.$('.oe_content').is(':visible'), "content is visible again");
- assert.verifySteps(['load action', 'add custom', 'add custom']);
+ assert.verifySteps(['load action', 'edit custom', 'edit custom']);
assert.strictEqual($('.modal').length, 0, "should have no modal open");
@@ -186,7 +186,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
assert.strictEqual($('.modal').length, 0, "should have no modal open");
assert.strictEqual(form.$('.oe_action').length, 0, "should have no displayed action");
- assert.verifySteps(['load action', 'add custom', 'add custom', 'add custom', 'add custom']);
+ assert.verifySteps(['load action', 'edit custom', 'edit custom', 'edit custom', 'edit custom']);
form.destroy();
});
@@ -295,8 +295,8 @@ QUnit.test('can drag and drop a view', function (assert) {
views: [[4, 'list']],
});
}
- if (route === '/web/view/add_custom') {
- assert.step('add custom');
+ if (route === '/web/view/edit_custom') {
+ assert.step('edit custom');
return $.when(true);
}
return this._super.apply(this, arguments);
@@ -342,8 +342,8 @@ QUnit.test('twice the same action in a dashboard', function (assert) {
views: [[4, 'list'],[5, 'kanban']],
});
}
- if (route === '/web/view/add_custom') {
- assert.step('add custom');
+ if (route === '/web/view/edit_custom') {
+ assert.step('edit custom');
return $.when(true);
}
return this._super.apply(this, arguments);
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 5cae21147a8..793e7cf2286 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -618,13 +618,21 @@ class Meeting(models.Model):
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)])
- for meeting in recurring_meetings:
- recurring_date = fields.Datetime.from_string(meeting.recurrent_id_date)
- if use_naive_datetime:
- recurring_date = recurring_date.replace(tzinfo=None)
- else:
- recurring_date = todate(meeting.recurrent_id_date)
- rset1.exdate(recurring_date)
+
+ # We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to
+ # control the memory usage.
+ invalidate = False
+ for meetings in self.env.cr.split_for_in_conditions(recurring_meetings, size=50000):
+ if invalidate:
+ self.invalidate_cache()
+ for meeting in meetings:
+ recurring_date = fields.Datetime.from_string(meeting.recurrent_id_date)
+ if use_naive_datetime:
+ recurring_date = recurring_date.replace(tzinfo=None)
+ else:
+ recurring_date = todate(meeting.recurrent_id_date)
+ rset1.exdate(recurring_date)
+ invalidate = True
return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1]
@api.multi
@@ -1149,6 +1157,7 @@ class Meeting(models.Model):
def key(record):
# we need to deal with undefined fields, as sorted requires an homogeneous iterable
def boolean_product(x):
+ x = False if (isinstance(x, models.Model) and not x) else x
if isinstance(x, bool):
return (x, x)
return (True, x)
diff --git a/addons/fleet/models/fleet_vehicle.py b/addons/fleet/models/fleet_vehicle.py
index 3ef914c2612..d7bac0d2925 100644
--- a/addons/fleet/models/fleet_vehicle.py
+++ b/addons/fleet/models/fleet_vehicle.py
@@ -22,7 +22,7 @@ class FleetVehicle(models.Model):
license_plate = fields.Char(required=True, track_visibility="onchange",
help='License plate number of the vehicle (i = plate number for a car)')
vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False)
- driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle', copy=False)
+ driver_id = fields.Many2one('res.partner', 'Driver', track_visibility="onchange", help='Driver of the vehicle', copy=False)
model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle')
log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
diff --git a/addons/l10n_be_hr_payroll_fleet/models/fleet.py b/addons/l10n_be_hr_payroll_fleet/models/fleet.py
index 8541a255675..da0bf8600fc 100644
--- a/addons/l10n_be_hr_payroll_fleet/models/fleet.py
+++ b/addons/l10n_be_hr_payroll_fleet/models/fleet.py
@@ -139,19 +139,6 @@ class FleetVehicleModel(models.Model):
for model in self:
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated
- @api.multi
- @api.depends('name', 'brand_id', 'default_total_depreciated_cost')
- def name_get(self):
- res = super(FleetVehicleModel, self).name_get()
- new_res = []
- for res_item in res:
- model = self.browse(res_item[0])
- if model.default_total_depreciated_cost != 0.0:
- new_res.append((res_item[0], res_item[1] + u" \u2022 " + str(round(model.default_total_depreciated_cost, 2))))
- else:
- new_res.append(res_item)
- return new_res
-
@api.depends('default_co2')
def _compute_co2_fee(self):
for model in self:
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index f94eead0736..f2a2f176a6b 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -680,7 +680,7 @@ class Channel(models.Model):
msg += _(" This channel is private. People must be invited to join it.")
else:
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)])
- msg = _("You are in a private conversation with <b>@%s</b>.") % channel_partners[0].partner_id.name
+ msg = _("You are in a private conversation with <b>@%s</b>.") % (channel_partners[0].partner_id.name if channel_partners else _('Anonymous'))
msg += _("""<br><br>
You can mention someone by typing <b>@username</b>, this will grab its attention.<br>
You can mention a channel by typing <b>#channel</b>.<br>
diff --git a/addons/mass_mailing/wizard/test_mailing.py b/addons/mass_mailing/wizard/test_mailing.py
index f1231fd2b38..7fafbb920a8 100644
--- a/addons/mass_mailing/wizard/test_mailing.py
+++ b/addons/mass_mailing/wizard/test_mailing.py
@@ -26,7 +26,7 @@ class TestMassMailing(models.TransientModel):
'reply_to': mailing.reply_to,
'email_to': test_mail,
'subject': mailing.name,
- 'body_html': mailing.body_html,
+ 'body_html': tools.html_sanitize(mailing.body_html, sanitize_attributes=True, sanitize_style=True, strip_classes=True),
'notification': True,
'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index 8bac597e6a9..c6ec88533ec 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -345,28 +345,25 @@ class MrpWorkorder(models.Model):
# If last work order, then post lots used
# TODO: should be same as checking if for every workorder something has been done?
if not self.next_work_order_id:
- production_moves = self.production_id.move_finished_ids.filtered(lambda x: (x.state not in ('done', 'cancel')))
- for production_move in production_moves:
- if production_move.product_id.id == self.production_id.product_id.id and production_move.has_tracking != 'none':
- move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
- if move_line:
- move_line.product_uom_qty += self.qty_producing
- else:
- move_line.create({'move_id': production_move.id,
- 'product_id': production_move.product_id.id,
- 'lot_id': self.final_lot_id.id,
- 'product_uom_qty': self.qty_producing,
- 'product_uom_id': production_move.product_uom.id,
- 'qty_done': self.qty_producing,
- 'workorder_id': self.id,
- 'location_id': production_move.location_id.id,
- 'location_dest_id': production_move.location_dest_id.id,
- })
- elif production_move.unit_factor:
- rounding = production_move.product_uom.rounding
- production_move.quantity_done += float_round(self.qty_producing * production_move.unit_factor, precision_rounding=rounding)
+ production_move = self.production_id.move_finished_ids.filtered(
+ lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
+ if production_move.product_id.tracking != 'none':
+ move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
+ if move_line:
+ move_line.product_uom_qty += self.qty_producing
else:
- production_move.quantity_done += self.qty_producing
+ move_line.create({'move_id': production_move.id,
+ 'product_id': production_move.product_id.id,
+ 'lot_id': self.final_lot_id.id,
+ 'product_uom_qty': self.qty_producing,
+ 'product_uom_id': production_move.product_uom.id,
+ 'qty_done': self.qty_producing,
+ 'workorder_id': self.id,
+ 'location_id': production_move.location_id.id,
+ 'location_dest_id': production_move.location_dest_id.id,
+ })
+ else:
+ production_move.quantity_done += self.qty_producing
if not self.next_work_order_id:
for by_product_move in self.production_id.move_finished_ids.filtered(lambda x: (x.product_id.id != self.production_id.product_id.id) and (x.state not in ('done', 'cancel'))):
diff --git a/addons/pad/models/pad.py b/addons/pad/models/pad.py
index deb920ec69e..9cdcbec12af 100644
--- a/addons/pad/models/pad.py
+++ b/addons/pad/models/pad.py
@@ -75,7 +75,7 @@ class PadCommon(models.AbstractModel):
@api.model
def pad_get_content(self, url):
company = self.env.user.sudo().company_id
- myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
+ myPad = EtherpadLiteClient(company.pad_key, (company.pad_server or '') + '/api')
content = ''
if url:
split_url = url.split('/p/')
@@ -132,7 +132,7 @@ class PadCommon(models.AbstractModel):
for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
company = self.env.user.sudo().company_id
- myPad = EtherpadLiteClient(company.pad_key, company.pad_server + '/api')
+ myPad = EtherpadLiteClient(company.pad_key, (company.pad_server or '') + '/api')
path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
diff --git a/addons/payment/controllers/portal.py b/addons/payment/controllers/portal.py
index be70f46309e..6f13400356b 100644
--- a/addons/payment/controllers/portal.py
+++ b/addons/payment/controllers/portal.py
@@ -89,7 +89,7 @@ class WebsitePayment(http.Controller):
'amount': float(amount),
'currency_id': currency_id,
'partner_id': partner_id,
- 'type': 'form_save' if acquirer.save_token else 'form',
+ 'type': 'form_save' if acquirer.save_token != 'none' and partner_id else 'form',
}
tx = request.env['payment.transaction'].sudo().create(values)
@@ -119,7 +119,7 @@ class WebsitePayment(http.Controller):
'currency_id': int(currency_id),
'partner_id': partner_id,
'payment_token_id': pm_id,
- 'type': 'form_save' if token.acquirer_id.save_token else 'form',
+ 'type': 'form_save' if token.acquirer_id.save_token != 'none' and partner_id else 'form',
}
tx = request.env['payment.transaction'].sudo().create(values)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index bf88e322a9d..843b091c4d2 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -450,7 +450,7 @@ class SaleOrder(models.Model):
if references.get(invoices.get(group_key)):
if order not in references[invoices[group_key]]:
- references[invoice] = references[invoice] | order
+ references[invoices[group_key]] |= order
if not invoices:
raise UserError(_('There is no invoiceable line.'))
diff --git a/addons/stock/views/procurement_views.xml b/addons/stock/views/procurement_views.xml
index e7f846a69b8..b22a7ff849d 100644
--- a/addons/stock/views/procurement_views.xml
+++ b/addons/stock/views/procurement_views.xml
@@ -78,6 +78,7 @@
<group string="Applied On">
<field name="location_id"/>
<field name="warehouse_id" groups="base.group_no_one"/>
+ <field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group>
<group string="Creates">
<field name="location_src_id" attrs="{'required': [('action', '=', 'move')], 'invisible':[('action', '!=', 'move')]}" domain="[('usage','!=','view')]"/>
diff --git a/addons/stock/views/stock_location_views.xml b/addons/stock/views/stock_location_views.xml
index 780bb5891b4..810fc4ba078 100644
--- a/addons/stock/views/stock_location_views.xml
+++ b/addons/stock/views/stock_location_views.xml
@@ -271,7 +271,7 @@
<field name="push_ids" colspan="4" nolabel="1"/>
</group>
<group string="Procurement Rules" colspan="4" >
- <field name="pull_ids" colspan="4" nolabel="1"/>
+ <field name="pull_ids" colspan="4" nolabel="1" context="{'default_company_id': company_id}"/>
</group>
</sheet>
</form>
diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml
index e0b925cc2b3..69ce7f4910f 100644
--- a/addons/stock_account/views/product_views.xml
+++ b/addons/stock_account/views/product_views.xml
@@ -98,7 +98,7 @@
<field name="view_type">form</field>
<field name="view_id" ref="view_stock_product_tree2"/>
<field name="domain">[('type', '=', 'product'), ('qty_available', '!=', 0)]</field>
- <field name="context">{}</field>
+ <field name="context">{'company_owned': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new product valuation
diff --git a/addons/stock_account/wizard/stock_quantity_history.py b/addons/stock_account/wizard/stock_quantity_history.py
index 6b244d13ac2..fe920a457bb 100644
--- a/addons/stock_account/wizard/stock_quantity_history.py
+++ b/addons/stock_account/wizard/stock_quantity_history.py
@@ -24,7 +24,7 @@ class StockQuantityHistory(models.TransientModel):
'name': _('Products'),
'res_model': 'product.product',
'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]",
- 'context': dict(self.env.context, to_date=self.date),
+ 'context': dict(self.env.context, to_date=self.date, company_owned=True),
}
return action
else:
diff --git a/addons/transifex/models/ir_translation.py b/addons/transifex/models/ir_translation.py
index 46ceecdb91f..316fbba47b1 100644
--- a/addons/transifex/models/ir_translation.py
+++ b/addons/transifex/models/ir_translation.py
@@ -78,11 +78,14 @@ class IrTranslation(models.Model):
translation.transifex_url = False
continue
- # e.g. 'https://www.transifex.com/odoo/odoo-10/translate/#fr/sale/42?q=Sale+Order'
+ # e.g. https://www.transifex.com/odoo/odoo-10/translate/#fr/sale/42?q=text'Sale+Order'
translation.transifex_url = "%(url)s/%(project)s/translate/#%(lang)s/%(module)s/42?q=%(src)s" % {
'url': base_url,
'project': project,
'lang': lang_code,
'module': translation.module,
- 'src': werkzeug.url_quote_plus(translation.source[:50]),
+ 'src': werkzeug.url_quote_plus(
+ "text:'" +
+ translation.source[:50].replace("'", "\'") +
+ "'"),
}
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index ccf7b932669..c1eb41ac13e 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -961,14 +961,17 @@ class DataSet(http.Controller):
class View(http.Controller):
- @http.route('/web/view/add_custom', type='json', auth="user")
- def add_custom(self, view_id, arch):
- CustomView = request.env['ir.ui.view.custom']
- CustomView.create({
- 'user_id': request.session.uid,
- 'ref_id': view_id,
- 'arch': arch
- })
+ @http.route('/web/view/edit_custom', type='json', auth="user")
+ def edit_custom(self, custom_id, arch):
+ """
+ Edit a custom view
+
+ :param int custom_id: the id of the edited custom view
+ :param str arch: the edited arch of the custom view
+ :returns: dict with acknowledged operation (result set to True)
+ """
+ custom_view = request.env['ir.ui.view.custom'].browse(custom_id)
+ custom_view.write({ 'arch': arch })
return {'result': True}
class Binary(http.Controller):
diff --git a/addons/web/static/src/js/chrome/web_client.js b/addons/web/static/src/js/chrome/web_client.js
index 225599e0f7e..e2b3a23d2a6 100644
--- a/addons/web/static/src/js/chrome/web_client.js
+++ b/addons/web/static/src/js/chrome/web_client.js
@@ -91,6 +91,10 @@ return AbstractWebClient.extend({
bind_hashchange: function() {
var self = this;
$(window).bind('hashchange', this.on_hashchange);
+ var didHashChanged = false;
+ $(window).one('hashchange', function () {
+ didHashChanged = true;
+ });
var state = $.bbq.getState(true);
if (_.isEmpty(state) || state.action === "login") {
@@ -101,6 +105,9 @@ return AbstractWebClient.extend({
args: [[session.uid], ['action_id']],
})
.done(function(result) {
+ if (didHashChanged) {
+ return;
+ }
var data = result[0];
if(data.action_id) {
self.action_manager.doAction(data.action_id[0]);
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 0f426dedd3b..b14d71c0b26 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1197,9 +1197,6 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
self.do_warn(_t("Image"), _t("Could not display the selected image."));
});
},
- isSet: function () {
- return true;
- },
});
var FieldBinaryFile = AbstractFieldBinary.extend({
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index c3df03fcff9..423dbf40235 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2335,6 +2335,21 @@ var FieldReference = FieldMany2One.extend({
},
//--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @returns {jQuery}
+ */
+ getFocusableElement: function () {
+ if (this.mode === 'edit' && !this.field.relation) {
+ return this.$('select');
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 962bb2899e1..e86d7dba8f7 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -3646,8 +3646,9 @@ var BasicModel = AbstractModel.extend({
if (isFieldInView) {
var field = r.fields[fieldName];
var fieldType = field.type;
+ var rec;
if (fieldType === 'many2one') {
- var rec = self._makeDataPoint({
+ rec = self._makeDataPoint({
context: r.context,
modelName: field.relation,
data: {id: r._changes[fieldName]},
@@ -3655,6 +3656,16 @@ var BasicModel = AbstractModel.extend({
});
r._changes[fieldName] = rec.id;
many2ones[fieldName] = true;
+ } else if (fieldType === 'reference') {
+ var reference = r._changes[fieldName].split(',');
+ rec = self._makeDataPoint({
+ context: r.context,
+ modelName: reference[0],
+ data: {id: parseInt(reference[1])},
+ parentID: r.id,
+ });
+ r._changes[fieldName] = rec.id;
+ many2ones[fieldName] = true;
} else if (_.contains(['one2many', 'many2many'], fieldType)) {
var x2mCommands = commands[0][2][fieldName];
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 93201843633..4a5d9b45b4a 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -267,6 +267,7 @@ var PivotModel = AbstractModel.extend({
this.data.colGroupBys = params.context.pivot_column_groupby || this.data.colGroupBys;
this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy;
this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures;
+ this.defaultGroupedBy = this.data.groupedBy.length ? this.data.groupedBy : this.defaultGroupedBy;
}
if ('domain' in params) {
this.data.domain = params.domain;
@@ -295,7 +296,7 @@ var PivotModel = AbstractModel.extend({
self._updateTree(old_col_root, self.data.main_col.root);
new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1;
- self.data.main_row.groupbys = old_col_root.groupbys.slice(0, new_groupby_length);
+ self.data.main_row.groupbys = old_row_root.groupbys.slice(0, new_groupby_length);
});
},
/**
diff --git a/addons/web/static/src/less/fields.less b/addons/web/static/src/less/fields.less
index ff6fe9eb20b..761e8577a29 100644
--- a/addons/web/static/src/less/fields.less
+++ b/addons/web/static/src/less/fields.less
@@ -361,6 +361,10 @@
&:hover .o_form_image_controls {
opacity: 0.8;
}
+
+ &.o_field_invalid > img {
+ border: 1px solid @brand-danger;
+ }
}
// Input loading/file
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 6aa28fd9a2e..f2a49cbc387 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1796,7 +1796,6 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('div[name="document"] > img').css('max-width'), "90px",
"the image should correctly set its attributes");
form.destroy();
-
});
QUnit.test('image fields in subviews are loaded correctly', function (assert) {
@@ -1853,6 +1852,34 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('image fields with required attribute', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<field name="document" required="1" widget="image"/>' +
+ '</form>',
+ mockRPC: function (route, args) {
+ if (args.method === 'create') {
+ throw new Error("Should not do a create RPC with unset required image field");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.ok(form.$('.o_form_view').hasClass('o_form_editable'),
+ "form view should still be editable");
+ assert.ok(form.$('.o_field_widget').hasClass('o_field_invalid'),
+ "image field should be displayed as invalid");
+
+ form.destroy();
+ });
+
QUnit.module('JournalDashboardGraph', {
beforeEach: function () {
_.extend(this.data.partner.fields, {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index a046e79de6c..01739c7888c 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -11392,6 +11392,38 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('default_get a reference field in a x2m', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.fields.turtles.default = [
+ [0, false, {turtle_ref: 'product,37'}]
+ ];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_ref"/>' +
+ '</tree>' +
+ '</field>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ archs: {
+ 'turtle,false,form': '<form><field name="display_name"/><field name="turtle_ref"/></form>',
+ },
+ });
+ assert.strictEqual(form.$('.o_field_one2many[name="turtles"] .o_data_row:first').text(), "xphone",
+ "the default value should be correctly handled");
+ form.destroy();
+ });
+
QUnit.test('widget reference on char field, reset by onchange', function (assert) {
assert.expect(4);
@@ -11442,6 +11474,32 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('reference and list navigation', function (assert) {
+ assert.expect(2);
+
+ var list = createView({
+ View: ListView,
+ model: 'partner',
+ data: this.data,
+ arch: '<tree editable="bottom"><field name="reference"/></tree>',
+ });
+
+ // edit first row
+ list.$('.o_data_row .o_data_cell').first().click();
+ assert.strictEqual(list.$('.o_data_row:eq(0) .o_field_widget[name="reference"] input')[0], document.activeElement,
+ 'input of first data row should be selected');
+
+ // press TAB to go to next line
+ list.$('.o_data_row:eq(0) input').trigger($.Event('keydown', {
+ which: $.ui.keyCode.TAB,
+ keyCode: $.ui.keyCode.TAB,
+ }));
+ assert.strictEqual(list.$('.o_data_row:eq(1) .o_field_widget[name="reference"] select')[0], document.activeElement,
+ 'select of second data row should be selected');
+
+ list.destroy();
+ });
+
QUnit.test('one2many with extra field from server not in form', function (assert) {
assert.expect(6);
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index 77c1c66fae8..26733a5edca 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -1083,4 +1083,45 @@ QUnit.module('Views', {
'should have loaded the proper data');
pivot.destroy();
});
+
+ QUnit.test('Row and column groupbys plus a domain', function (assert) {
+ assert.expect(3);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ });
+
+ // Set a column groupby
+ pivot.$('thead .o_pivot_header_cell_closed').click();
+ pivot.$('.o_field_selection li[data-field=customer] a').click();
+
+ // Set a Row groupby
+ pivot.$('tbody .o_pivot_header_cell_closed').click();
+ pivot.$('.o_pivot_field_menu li[data-field=product_id] a').click();
+
+ // Set a domain
+ pivot.update({domain: [['product_id', '=', 41]]});
+
+ var expectedContext = {pivot_column_groupby: ['customer'],
+ pivot_measures: ['foo'],
+ pivot_row_groupby: ['product_id']};
+
+ // Mock 'save as favorite'
+ assert.deepEqual(pivot.getContext(), expectedContext,
+ 'The pivot view should have the right context');
+
+ var $xpadHeader = pivot.$('tbody .o_pivot_header_cell_closed[data-original-title=Product]');
+ assert.equal($xpadHeader.length, 1,
+ 'There should be only one product line because of the domain');
+
+ assert.equal($xpadHeader.text(), 'xpad',
+ 'The product should be the right one');
+
+ pivot.destroy();
+ });
});});
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 68051f8c07c..2174dae0b90 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -164,7 +164,7 @@ class Web_Editor(http.Controller):
'datas_fname': datas_fname,
'type': 'url',
'url': url,
- 'public': True,
+ 'public': res_model == 'ir.ui.view',
'res_id': res_id,
'res_model': res_model,
})
@@ -195,7 +195,7 @@ class Web_Editor(http.Controller):
'name': name,
'datas': base64.b64encode(data),
'datas_fname': datas_fname,
- 'public': True,
+ 'public': res_model == 'ir.ui.view',
'res_id': res_id,
'res_model': res_model,
})
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index bbb43fc2f82..479a26469a3 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -90,11 +90,49 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
//--------------------------------------------------------------------------
/**
+ * Returns the domain for attachments used in media dialog.
+ * We look for attachments related to the current document. If there is a value for the model
+ * field, it is used to search attachments, and the attachments from the current document are
+ * filtered to display only user-created documents.
+ * In the case of a wizard such as mail, we have the documents uploaded and those of the model
+ *
+ * @private
+ * @returns {Array} "ir.attachment" odoo domain.
+ */
+ _getAttachmentsDomain: function () {
+ var domain = ['|', ['id', 'in', _.pluck(this.attachments, 'id')]];
+ var attachedDocumentDomain = [
+ '&',
+ ['res_model', '=', this.model],
+ ['res_id', '=', this.res_id|0]
+ ];
+ // if the document is not yet created, do not see the documents of other users
+ if (!this.res_id) {
+ attachedDocumentDomain.unshift('&');
+ attachedDocumentDomain.push(['create_uid', '=', session.uid]);
+ }
+ if (this.recordData.model) {
+ var relatedDomain = ['&',
+ ['res_model', '=', this.recordData.model],
+ ['res_id', '=', this.recordData.res_id|0]];
+ if (!this.recordData.res_id) {
+ relatedDomain.unshift('&');
+ relatedDomain.push(['create_uid', '=', session.uid]);
+ }
+ domain = domain.concat(['|'], attachedDocumentDomain, relatedDomain);
+ } else {
+ domain = domain.concat(attachedDocumentDomain);
+ }
+ return domain;
+ },
+ /**
* @private
* @returns {Object} the summernote configuration
*/
_getSummernoteConfig: function () {
var summernoteConfig = {
+ model: this.model,
+ id: this.res_id,
focus: false,
height: 180,
toolbar: [
@@ -113,6 +151,22 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
lang: "odoo",
onChange: this._doDebouncedAction.bind(this),
};
+
+ var fieldNameAttachment =_.chain(this.recordData)
+ .pairs()
+ .find(function (value) {
+ return _.isObject(value[1]) && value[1].model === "ir.attachment";
+ })
+ .first()
+ .value();
+
+ if (fieldNameAttachment) {
+ this.fieldNameAttachment = fieldNameAttachment;
+ this.attachments = [];
+ summernoteConfig.onImageUpload = this._onImageUpload.bind(this);
+ summernoteConfig.getMediaDomain = this._getAttachmentsDomain.bind(this);
+ }
+
if (config.debug) {
summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]);
}
@@ -131,6 +185,34 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
return this.$content.html();
},
/**
+ * trigger_up 'field_changed' add record into the "ir.attachment" field found in the view.
+ * This method is called when an image is uploaded by the media dialog.
+ *
+ * For e.g. when sending email, this allows people to add attachments with the content
+ * editor interface and that they appear in the attachment list.
+ * The new documents being attached to the email, they will not be erased by the CRON
+ * when closing the wizard.
+ *
+ * @private
+ */
+ _onImageUpload: function (attachments) {
+ var self = this;
+ attachments = _.filter(attachments, function (attachment) {
+ return !_.findWhere(self.attachments, {id: attachment.id});
+ });
+ if (!attachments.length) {
+ return;
+ }
+ this.attachments = this.attachments.concat(attachments);
+ this.trigger_up('field_changed', {
+ dataPointID: this.dataPointID,
+ changes: _.object([this.fieldNameAttachment], [{
+ operation: 'ADD_M2M',
+ ids: attachments
+ }])
+ });
+ },
+ /**
* @override
* @private
*/
@@ -140,8 +222,6 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
this.$textarea.summernote(this._getSummernoteConfig());
this.$content = this.$('.note-editable:first');
this.$content.html(this._textToHtml(this.value));
- this.$content.data('oe-id', this.recordData.res_id || this.res_id);
- this.$content.data('oe-model', this.recordData.model || this.model);
// trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup');
if (this.nodeOptions['style-inline']) {
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index fb4d6fc7206..f6e2ac63aa9 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -397,6 +397,7 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
media: media,
options: {
lastFilters: ['background'],
+ onUpload: $editable.data('callbacks').onImageUpload,
},
});
return new $.Deferred().reject();
@@ -788,6 +789,14 @@ eventHandler.attach = function (oLayoutInfo, options) {
$(document).on("keyup", reRangeSelectKey);
var clone_data = false;
+
+ if (options.model) {
+ oLayoutInfo.editable().data({'oe-model': options.model, 'oe-id': options.id});
+ }
+ if (options.getMediaDomain) {
+ oLayoutInfo.editable().data('oe-media-domain', options.getMediaDomain);
+ }
+
var $node = oLayoutInfo.editor();
if ($node.data('oe-model') || $node.data('oe-translation-id')) {
$node.on('content_changed', function () {
@@ -1089,6 +1098,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
_.extend({
res_model: data.$editable.data('oe-model'),
res_id: data.$editable.data('oe-id'),
+ domain: data.$editable.data('oe-media-domain'),
}, data.options),
data.$editable,
data.media
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index 53df66e9ba7..f7c1c808016 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -209,7 +209,9 @@ var ImageWidget = MediaWidget.extend({
this.options = options;
this.accept = options.accept || (options.document ? '*/*' : 'image/*');
- if (options.res_id) {
+ if (options.domain) {
+ this.domain = typeof options.domain === 'function' ? options.domain() : options.domain;
+ } else if (options.res_id) {
this.domain = ['|',
'&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id],
['res_model', '=', 'ir.ui.view']];
@@ -330,6 +332,11 @@ var ImageWidget = MediaWidget.extend({
self.$media.css(style);
}
+ if (self.options.onUpload) {
+ // We consider that when selecting an image it is as if we upload it in the html content.
+ self.options.onUpload([img]);
+ }
+
return self.media;
});
},
@@ -504,6 +511,10 @@ var ImageWidget = MediaWidget.extend({
_processFile(attachments[i], error);
}
+ if (self.options.onUpload) {
+ self.options.onUpload(attachments);
+ }
+
function _processFile(attachment, error) {
var $button = self.$('.o_upload_image_button');
if (!error) {
diff --git a/doc/cla/corporate/vauxoo.md b/doc/cla/corporate/vauxoo.md
index 0d8c8b623e9..5bc8dca8882 100644
--- a/doc/cla/corporate/vauxoo.md
+++ b/doc/cla/corporate/vauxoo.md
@@ -39,6 +39,7 @@ Jesus Zapata jesus@vauxoo.com https://github.com/JesusZapata
Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
Edgar Rivero edgar@vauxoo.com https://github.com/egrivero
+Edilianny Sánchez esanchez@vauxoo.com https://github.com/edy1192
Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
Erick Birbe erick@vauxoo.com https://github.com/ebirbe
diff --git a/doc/reference/reports.rst b/doc/reference/reports.rst
index 6ec6e6a157c..ed408a0d817 100644
--- a/doc/reference/reports.rst
+++ b/doc/reference/reports.rst
@@ -190,7 +190,7 @@ Barcodes
--------
Barcodes are images returned by a controller and can easily be embedded in
-reports thanks to the QWeb syntax:
+reports thanks to the QWeb syntax (e.g. see :ref:`reference/qweb/attributes`):
.. code-block:: html
diff --git a/doc/setup/deploy.rst b/doc/setup/deploy.rst
index 5aea431caf4..13d4198cceb 100644
--- a/doc/setup/deploy.rst
+++ b/doc/setup/deploy.rst
@@ -442,6 +442,10 @@ security-related topics:
only for controlling/managing the installation.
*Never* use any default passwords like admin/admin, even for test/staging databases.
+- Do **not** install demo data on internet-facing servers. Databases with demo data contain
+ default logins and passwords that can be used to get into your systems and cause significant
+ trouble, even on staging/dev systems.
+
- Use appropriate database filters ( :option:`--db-filter <odoo-bin --db-filter>`)
to restrict the visibility of your databases according to the hostname.
See :ref:`db_filter`.
@@ -474,6 +478,12 @@ security-related topics:
and then enable the :option:`proxy mode <odoo-bin --proxy-mode>` option.
See also :ref:`https_proxy`.
+- If you need to allow remote SSH access to your servers, make sure to set a strong password
+ for **all** accounts, not just `root`. It is strongly recommended to entirely disable
+ password-based authentication, and only allow public key authentication. Also consider
+ restricting access via a VPN, allowing only trusted IPs in the firewall, and/or
+ running a brute-force detection system such as `fail2ban` or equivalent.
+
- Whenever possible, host your public-facing demo/test/staging instances on different
machines than the production ones. And apply the same security precautions as for
production.
diff --git a/odoo/addons/base/data/res.lang.csv b/odoo/addons/base/data/res.lang.csv
index 7a15b0a7147..bea539d9325 100644
--- a/odoo/addons/base/data/res.lang.csv
+++ b/odoo/addons/base/data/res.lang.csv
@@ -1,7 +1,7 @@
"id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","translatable","week_start"
"base.lang_en","English","en_US","en","Left-to-Right","[3,0]",".",",","%m/%d/%Y","%H:%M:%S","True","7"
"base.lang_am_ET","Amharic / አምሃርኛ","am_ET","am_ET","Left-to-Right","[3,0]",".",",","%d/%m/%Y","%I:%M:%S","True","7"
-"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar_SY","Left-to-Right","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6"
+"base.lang_ar_SY","Arabic / الْعَرَبيّة","ar_SY","ar","Left-to-Right","[3,0]",".",",","%d %b, %Y","%I:%M:%S","True","6"
"base.lang_eu_ES","Basque / Euskara","eu_ES","eu_ES","Left-to-Right","[]",",",,"%a, %Y.eko %bren %da","%H:%M:%S","True","1"
"base.lang_bs_BA","Bosnian / bosanski jezik","bs_BA","bs","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","7"
"base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","True","1"
diff --git a/odoo/addons/base/i18n/base.pot b/odoo/addons/base/i18n/base.pot
index 31d78fc8e04..63cd8c07736 100644
--- a/odoo/addons/base/i18n/base.pot
+++ b/odoo/addons/base/i18n/base.pot
@@ -12045,6 +12045,18 @@ msgid "Memos pad"
msgstr ""
#. module: base
+#: code:addons/base/ir/ir_model.py:382
+#, python-format
+msgid "Model %s does not exist"
+msgstr ""
+
+#. module: base
+#: code:addons/base/ir/ir_model.py:383
+#, python-format
+msgid "Please specify a valid model for the object relation"
+msgstr ""
+
+#. module: base
#: model:ir.model.fields,field_description:base.field_ir_ui_menu__name
#: model:ir.ui.view,arch_db:base.edit_menu
#: model:ir.ui.view,arch_db:base.edit_menu_access
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index 0176b2a8ed2..8981618590e 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -483,6 +483,13 @@ class IrModelFields(models.Model):
def _onchange_ttype(self):
self.copy = (self.ttype != 'one2many')
if self.ttype == 'many2many' and self.model_id and self.relation:
+ if self.relation not in self.env:
+ return {
+ 'warning': {
+ 'title': _('Model %s does not exist') % self.relation,
+ 'message': _('Please specify a valid model for the object relation'),
+ }
+ }
names = self._custom_many2many_names(self.model_id.model, self.relation)
self.relation_table, self.column1, self.column2 = names
else: