[MERGE] forward port branch 11.0 up to 8c64159b88

This commit is contained in:
Christophe Simonis
2018-04-18 18:20:40 +02:00
39 changed files with 524 additions and 102 deletions
@@ -144,7 +144,7 @@
<group> <group>
<group> <group>
<field name="payment_type" widget="radio" attrs="{'readonly': [('state', '!=', 'draft')]}"/> <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'}"/> <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"/> <label for="amount"/>
<div name="amount_div" class="o_row"> <div name="amount_div" class="o_row">
@@ -44,7 +44,7 @@ class AccountPayment(models.Model):
_inherit = "account.payment" _inherit = "account.payment"
check_amount_in_words = fields.Char(string="Amount in Words") 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, 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 " 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.") "or if the current numbering is wrong, you can change it in the journal configuration page.")
@@ -72,10 +72,10 @@ class AccountPayment(models.Model):
@api.model @api.model
def create(self, vals): def create(self, vals):
if vals['payment_method_id'] == self.env.ref('account_check_printing.account_payment_method_check').id\ if vals['payment_method_id'] == self.env.ref('account_check_printing.account_payment_method_check').id:
and vals.get('check_manual_sequencing'): journal = self.env['account.journal'].browse(vals['journal_id'])
sequence = self.env['account.journal'].browse(vals['journal_id']).check_sequence_id if journal.check_manual_sequencing:
vals.update({'check_number': sequence.next_by_id()}) vals.update({'check_number': journal.check_sequence_id.next_by_id()})
return super(AccountPayment, self).create(vals) return super(AccountPayment, self).create(vals)
@api.multi @api.multi
@@ -146,10 +146,10 @@ FormController.include({
* @param {Object} activeBarcode: options sent by the field who use barcode features * @param {Object} activeBarcode: options sent by the field who use barcode features
* @returns {Deferred} * @returns {Deferred}
*/ */
_barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode) { _barcodeSelectedCandidate: function (candidate, record, barcode, activeBarcode, quantity) {
var changes = {}; var changes = {};
var candidateChanges = {}; var candidateChanges = {};
candidateChanges[activeBarcode.quantity] = candidate.data[activeBarcode.quantity] + 1; candidateChanges[activeBarcode.quantity] = quantity ? quantity : candidate.data[activeBarcode.quantity] + 1;
changes[activeBarcode.fieldName] = { changes[activeBarcode.fieldName] = {
operation: 'UPDATE', operation: 'UPDATE',
id: candidate.id, id: candidate.id,
@@ -283,15 +283,14 @@ FormController.include({
function (reserved) {return barcode.indexOf(reserved) === 0;}); function (reserved) {return barcode.indexOf(reserved) === 0;});
var hasCommand = false; var hasCommand = false;
var defs = []; var defs = [];
if (! $.contains(target, self.el)) {
return;
}
for (var k in self.activeBarcode) { for (var k in self.activeBarcode) {
var activeBarcode = self.activeBarcode[k]; var activeBarcode = self.activeBarcode[k];
// Handle the case where there are several barcode widgets on the same page. Since the // 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 // event is global on the page, all barcode widgets will be triggered. However, we only
// want to keep the event on the target widget. // want to keep the event on the target widget.
if (! $.contains(target, self.el)) {
continue;
}
var methods = self.activeBarcode[k].commands; var methods = self.activeBarcode[k].commands;
var method = prefixed ? methods[barcode] : methods.barcode; var method = prefixed ? methods[barcode] : methods.barcode;
if (method) { if (method) {
@@ -306,6 +305,8 @@ FormController.include({
} }
return self.alive($.when.apply($, defs)).then(function () { return self.alive($.when.apply($, defs)).then(function () {
if (!prefixed) { 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 // redraw the view if we scanned a real barcode (required if
// we manually apply the change in JS, e.g. incrementing the // we manually apply the change in JS, e.g. incrementing the
// quantity) // quantity)
@@ -321,6 +322,9 @@ FormController.include({
_quantityListener: function (event) { _quantityListener: function (event) {
var character = String.fromCharCode(event.which); var character = String.fromCharCode(event.which);
if (! $.contains(event.target, this.el)) {
return;
}
// only catch the event if we're not focused in // only catch the event if we're not focused in
// another field and it's a number // another field and it's a number
if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) { if (!$(event.target).is('body, .modal') || !/[0-9]/.test(character)) {
@@ -355,9 +359,10 @@ FormController.include({
title: _t('Set quantity'), title: _t('Set quantity'),
buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () { buttons: [{text: _t('Select'), classes: 'btn-primary', close: true, click: function () {
var new_qty = this.$content.find('.o_set_qty_input').val(); var new_qty = this.$content.find('.o_set_qty_input').val();
var values = {}; var record = self.model.get(self.handle);
values[activeBarcode.quantity] = parseFloat(new_qty); return self._barcodeSelectedCandidate(activeBarcode.candidate, record,
return self.model.notifyChanges(activeBarcode.candidate.id, values).then(function () { self.current_barcode, activeBarcode, parseFloat(new_qty))
.then(function () {
self.update({}, {reload: false}); self.update({}, {reload: false});
}); });
}}, {text: _t('Discard'), close: true}], }}, {text: _t('Discard'), close: true}],
@@ -404,4 +404,136 @@ QUnit.test('specification of widget barcode_handler', function (assert) {
barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay; barcodeEvents.BarcodeEvents.max_time_between_keys_in_ms = delay;
delete fieldRegistry.map.test_barcode_handler; 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();
});
}); });
+4 -4
View File
@@ -20,7 +20,7 @@ FormView.include({
*/ */
init: function (viewInfo) { init: function (viewInfo) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.controllerParams.viewID = viewInfo.view_id; this.controllerParams.customViewID = viewInfo.custom_view_id;
}, },
}); });
@@ -33,7 +33,7 @@ FormController.include({
}), }),
init: function (parent, model, renderer, params) { init: function (parent, model, renderer, params) {
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.viewID = params.viewID; this.customViewID = params.customViewID;
}, },
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -63,9 +63,9 @@ FormController.include({
var board = this.renderer.getBoard(); var board = this.renderer.getBoard();
var arch = QWeb.render('DashBoard.xml', _.extend({}, board)); var arch = QWeb.render('DashBoard.xml', _.extend({}, board));
return this._rpc({ return this._rpc({
route: '/web/view/add_custom', route: '/web/view/edit_custom',
params: { params: {
view_id: this.viewID, custom_id: this.customViewID,
arch: arch, arch: arch,
} }
}).then(dataManager.invalidate.bind(dataManager)); }).then(dataManager.invalidate.bind(dataManager));
+8 -8
View File
@@ -124,8 +124,8 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
if (route === '/web/dataset/search_read') { if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [['foo', '!=', 'False']], "the domain should be passed"); assert.deepEqual(args.domain, [['foo', '!=', 'False']], "the domain should be passed");
} }
if (route === '/web/view/add_custom') { if (route === '/web/view/edit_custom') {
assert.step('add custom'); assert.step('edit custom');
return $.when(true); return $.when(true);
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
@@ -156,7 +156,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
form.$('.oe_fold').click(); form.$('.oe_fold').click();
assert.ok(form.$('.oe_content').is(':visible'), "content is visible again"); 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"); assert.strictEqual($('.modal').length, 0, "should have no modal open");
@@ -184,7 +184,7 @@ QUnit.test('basic functionality, with one sub action', function (assert) {
assert.strictEqual($('.modal').length, 0, "should have no modal open"); assert.strictEqual($('.modal').length, 0, "should have no modal open");
assert.strictEqual(form.$('.oe_action').length, 0, "should have no displayed action"); 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(); form.destroy();
}); });
@@ -293,8 +293,8 @@ QUnit.test('can drag and drop a view', function (assert) {
views: [[4, 'list']], views: [[4, 'list']],
}); });
} }
if (route === '/web/view/add_custom') { if (route === '/web/view/edit_custom') {
assert.step('add custom'); assert.step('edit custom');
return $.when(true); return $.when(true);
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
@@ -340,8 +340,8 @@ QUnit.test('twice the same action in a dashboard', function (assert) {
views: [[4, 'list'],[5, 'kanban']], views: [[4, 'list'],[5, 'kanban']],
}); });
} }
if (route === '/web/view/add_custom') { if (route === '/web/view/edit_custom') {
assert.step('add custom'); assert.step('edit custom');
return $.when(true); return $.when(true);
} }
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
+16 -7
View File
@@ -617,13 +617,21 @@ class Meeting(models.Model):
event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone event_date = event_date.astimezone(timezone) # transform "+hh:mm" timezone
rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={}) rset1 = rrule.rrulestr(str(self.rrule), dtstart=event_date, forceset=True, tzinfos={})
recurring_meetings = self.search([('recurrent_id', '=', self.id), '|', ('active', '=', False), ('active', '=', True)]) 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) # We handle a maximum of 50,000 meetings at a time, and clear the cache at each step to
if use_naive_datetime: # control the memory usage.
recurring_date = recurring_date.replace(tzinfo=None) invalidate = False
else: for meetings in self.env.cr.split_for_in_conditions(recurring_meetings, size=50000):
recurring_date = todate(meeting.recurrent_id_date) if invalidate:
rset1.exdate(recurring_date) 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] return [d.astimezone(pytz.UTC) if d.tzinfo else d for d in rset1]
@api.multi @api.multi
@@ -1148,6 +1156,7 @@ class Meeting(models.Model):
def key(record): def key(record):
# we need to deal with undefined fields, as sorted requires an homogeneous iterable # we need to deal with undefined fields, as sorted requires an homogeneous iterable
def boolean_product(x): def boolean_product(x):
x = False if (isinstance(x, models.Model) and not x) else x
if isinstance(x, bool): if isinstance(x, bool):
return (x, x) return (x, x)
return (True, x) return (True, x)
+1 -1
View File
@@ -22,7 +22,7 @@ class FleetVehicle(models.Model):
license_plate = fields.Char(required=True, track_visibility="onchange", license_plate = fields.Char(required=True, track_visibility="onchange",
help='License plate number of the vehicle (i = plate number for a car)') 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) 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') 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_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs')
log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs') log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs')
@@ -139,19 +139,6 @@ class FleetVehicleModel(models.Model):
for model in self: for model in self:
model.default_total_depreciated_cost = model.co2_fee + model.default_recurring_cost_amount_depreciated 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') @api.depends('default_co2')
def _compute_co2_fee(self): def _compute_co2_fee(self):
for model in self: for model in self:
+1 -1
View File
@@ -680,7 +680,7 @@ class Channel(models.Model):
msg += _(" This channel is private. People must be invited to join it.") msg += _(" This channel is private. People must be invited to join it.")
else: else:
channel_partners = self.env['mail.channel.partner'].search([('partner_id', '!=', partner.id), ('channel_id', '=', self.id)]) 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> msg += _("""<br><br>
You can mention someone by typing <b>@username</b>, this will grab its attention.<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> You can mention a channel by typing <b>#channel</b>.<br>
+1 -1
View File
@@ -26,7 +26,7 @@ class TestMassMailing(models.TransientModel):
'reply_to': mailing.reply_to, 'reply_to': mailing.reply_to,
'email_to': test_mail, 'email_to': test_mail,
'subject': mailing.name, '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, 'notification': True,
'mailing_id': mailing.id, 'mailing_id': mailing.id,
'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
+18 -21
View File
@@ -345,28 +345,25 @@ class MrpWorkorder(models.Model):
# If last work order, then post lots used # If last work order, then post lots used
# TODO: should be same as checking if for every workorder something has been done? # TODO: should be same as checking if for every workorder something has been done?
if not self.next_work_order_id: if not self.next_work_order_id:
production_moves = self.production_id.move_finished_ids.filtered(lambda x: (x.state not in ('done', 'cancel'))) production_move = self.production_id.move_finished_ids.filtered(
for production_move in production_moves: lambda x: (x.product_id.id == self.production_id.product_id.id) and (x.state not in ('done', 'cancel')))
if production_move.product_id.id == self.production_id.product_id.id and production_move.has_tracking != 'none': 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) move_line = production_move.move_line_ids.filtered(lambda x: x.lot_id.id == self.final_lot_id.id)
if move_line: if move_line:
move_line.product_uom_qty += self.qty_producing 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)
else: 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: 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'))): 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'))):
+2 -2
View File
@@ -75,7 +75,7 @@ class PadCommon(models.AbstractModel):
@api.model @api.model
def pad_get_content(self, url): def pad_get_content(self, url):
company = self.env.user.sudo().company_id 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 = '' content = ''
if url: if url:
split_url = url.split('/p/') split_url = url.split('/p/')
@@ -132,7 +132,7 @@ class PadCommon(models.AbstractModel):
for k, field in self._fields.items(): for k, field in self._fields.items():
if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]: if hasattr(field, 'pad_content_field') and vals.get(field.pad_content_field) and self[k]:
company = self.env.user.sudo().company_id 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] path = self[k].split('/p/')[1]
myPad.setHtmlFallbackText(path, vals[field.pad_content_field]) myPad.setHtmlFallbackText(path, vals[field.pad_content_field])
+2 -2
View File
@@ -89,7 +89,7 @@ class WebsitePayment(http.Controller):
'amount': float(amount), 'amount': float(amount),
'currency_id': currency_id, 'currency_id': currency_id,
'partner_id': partner_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) tx = request.env['payment.transaction'].sudo().create(values)
@@ -119,7 +119,7 @@ class WebsitePayment(http.Controller):
'currency_id': int(currency_id), 'currency_id': int(currency_id),
'partner_id': partner_id, 'partner_id': partner_id,
'payment_token_id': pm_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) tx = request.env['payment.transaction'].sudo().create(values)
+1 -1
View File
@@ -425,7 +425,7 @@ class SaleOrder(models.Model):
if references.get(invoices.get(group_key)): if references.get(invoices.get(group_key)):
if order not in references[invoices[group_key]]: if order not in references[invoices[group_key]]:
references[invoice] = references[invoice] | order references[invoices[group_key]] |= order
if not invoices: if not invoices:
raise UserError(_('There is no invoiceable line.')) raise UserError(_('There is no invoiceable line.'))
+1
View File
@@ -78,6 +78,7 @@
<group string="Applied On"> <group string="Applied On">
<field name="location_id"/> <field name="location_id"/>
<field name="warehouse_id" groups="base.group_no_one"/> <field name="warehouse_id" groups="base.group_no_one"/>
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
</group> </group>
<group string="Creates"> <group string="Creates">
<field name="location_src_id" attrs="{'required': [('action', '=', 'move')], 'invisible':[('action', '!=', 'move')]}" domain="[('usage','!=','view')]"/> <field name="location_src_id" attrs="{'required': [('action', '=', 'move')], 'invisible':[('action', '!=', 'move')]}" domain="[('usage','!=','view')]"/>
+1 -1
View File
@@ -271,7 +271,7 @@
<field name="push_ids" colspan="4" nolabel="1"/> <field name="push_ids" colspan="4" nolabel="1"/>
</group> </group>
<group string="Procurement Rules" colspan="4" > <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> </group>
</sheet> </sheet>
</form> </form>
+1 -1
View File
@@ -95,7 +95,7 @@
<field name="view_type">form</field> <field name="view_type">form</field>
<field name="view_id" ref="view_stock_product_tree2"/> <field name="view_id" ref="view_stock_product_tree2"/>
<field name="domain">[('type', '=', 'product'), ('qty_available', '!=', 0)]</field> <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"> <field name="help" type="html">
<p class="o_view_nocontent_smiling_face"> <p class="o_view_nocontent_smiling_face">
Create a new product valuation Create a new product valuation
@@ -24,7 +24,7 @@ class StockQuantityHistory(models.TransientModel):
'name': _('Products'), 'name': _('Products'),
'res_model': 'product.product', 'res_model': 'product.product',
'domain': "[('type', '=', 'product'), ('qty_available', '!=', 0)]", '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 return action
else: else:
+5 -2
View File
@@ -78,11 +78,14 @@ class IrTranslation(models.Model):
translation.transifex_url = False translation.transifex_url = False
continue 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" % { translation.transifex_url = "%(url)s/%(project)s/translate/#%(lang)s/%(module)s/42?q=%(src)s" % {
'url': base_url, 'url': base_url,
'project': project, 'project': project,
'lang': lang_code, 'lang': lang_code,
'module': translation.module, 'module': translation.module,
'src': werkzeug.url_quote_plus(translation.source[:50]), 'src': werkzeug.url_quote_plus(
"text:'" +
translation.source[:50].replace("'", "\'") +
"'"),
} }
+11 -8
View File
@@ -961,14 +961,17 @@ class DataSet(http.Controller):
class View(http.Controller): class View(http.Controller):
@http.route('/web/view/add_custom', type='json', auth="user") @http.route('/web/view/edit_custom', type='json', auth="user")
def add_custom(self, view_id, arch): def edit_custom(self, custom_id, arch):
CustomView = request.env['ir.ui.view.custom'] """
CustomView.create({ Edit a custom view
'user_id': request.session.uid,
'ref_id': view_id, :param int custom_id: the id of the edited custom view
'arch': arch :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} return {'result': True}
class Binary(http.Controller): class Binary(http.Controller):
@@ -1241,9 +1241,6 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
self.do_warn(_t("Image"), _t("Could not display the selected image.")); self.do_warn(_t("Image"), _t("Could not display the selected image."));
}); });
}, },
isSet: function () {
return true;
},
}); });
var FieldBinaryFile = AbstractFieldBinary.extend({ var FieldBinaryFile = AbstractFieldBinary.extend({
@@ -2302,6 +2302,21 @@ var FieldReference = FieldMany2One.extend({
return this._super.apply(this, arguments); return this._super.apply(this, arguments);
}, },
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
* @returns {jQuery}
*/
getFocusableElement: function () {
if (this.mode === 'edit' && !this.field.relation) {
return this.$('select');
}
return this._super.apply(this, arguments);
},
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
@@ -3614,8 +3614,9 @@ var BasicModel = AbstractModel.extend({
if (isFieldInView) { if (isFieldInView) {
var field = r.fields[fieldName]; var field = r.fields[fieldName];
var fieldType = field.type; var fieldType = field.type;
var rec;
if (fieldType === 'many2one') { if (fieldType === 'many2one') {
var rec = self._makeDataPoint({ rec = self._makeDataPoint({
context: r.context, context: r.context,
modelName: field.relation, modelName: field.relation,
data: {id: r._changes[fieldName]}, data: {id: r._changes[fieldName]},
@@ -3623,6 +3624,16 @@ var BasicModel = AbstractModel.extend({
}); });
r._changes[fieldName] = rec.id; r._changes[fieldName] = rec.id;
many2ones[fieldName] = true; 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)) { } else if (_.contains(['one2many', 'many2many'], fieldType)) {
var x2mCommands = commands[0][2][fieldName]; var x2mCommands = commands[0][2][fieldName];
defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands)); defs.push(self._processX2ManyCommands(r, fieldName, x2mCommands));
@@ -250,6 +250,7 @@ var PivotModel = AbstractModel.extend({
this.data.colGroupBys = params.context.pivot_column_groupby || this.data.colGroupBys; this.data.colGroupBys = params.context.pivot_column_groupby || this.data.colGroupBys;
this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy; this.data.groupedBy = params.context.pivot_row_groupby || this.data.groupedBy;
this.data.measures = this._processMeasures(params.context.pivot_measures) || this.data.measures; 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) { if ('domain' in params) {
this.data.domain = params.domain; this.data.domain = params.domain;
@@ -278,7 +279,7 @@ var PivotModel = AbstractModel.extend({
self._updateTree(old_col_root, self.data.main_col.root); self._updateTree(old_col_root, self.data.main_col.root);
new_groupby_length = self._getHeaderDepth(self.data.main_col.root) - 1; 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);
}); });
}, },
/** /**
+4
View File
@@ -361,6 +361,10 @@
&:hover .o_form_image_controls { &:hover .o_form_image_controls {
opacity: 0.8; opacity: 0.8;
} }
&.o_field_invalid > img {
border: 1px solid @brand-danger;
}
} }
// Input loading/file // Input loading/file
@@ -1736,7 +1736,6 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90", assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90",
"the image should correctly set its attributes"); "the image should correctly set its attributes");
form.destroy(); form.destroy();
}); });
QUnit.test('image fields in subviews are loaded correctly', function (assert) { QUnit.test('image fields in subviews are loaded correctly', function (assert) {
@@ -1788,6 +1787,34 @@ QUnit.module('basic_fields', {
form.destroy(); 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', { QUnit.module('JournalDashboardGraph', {
beforeEach: function () { beforeEach: function () {
_.extend(this.data.partner.fields, { _.extend(this.data.partner.fields, {
@@ -11326,6 +11326,38 @@ QUnit.module('relational_fields', {
form.destroy(); 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) { QUnit.test('widget reference on char field, reset by onchange', function (assert) {
assert.expect(4); assert.expect(4);
@@ -11376,6 +11408,32 @@ QUnit.module('relational_fields', {
form.destroy(); 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) { QUnit.test('one2many with extra field from server not in form', function (assert) {
assert.expect(6); assert.expect(6);
@@ -965,4 +965,45 @@ QUnit.module('Views', {
pivot.destroy(); 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();
});
});}); });});
+2 -2
View File
@@ -164,7 +164,7 @@ class Web_Editor(http.Controller):
'datas_fname': datas_fname, 'datas_fname': datas_fname,
'type': 'url', 'type': 'url',
'url': url, 'url': url,
'public': True, 'public': res_model == 'ir.ui.view',
'res_id': res_id, 'res_id': res_id,
'res_model': res_model, 'res_model': res_model,
}) })
@@ -195,7 +195,7 @@ class Web_Editor(http.Controller):
'name': name, 'name': name,
'datas': base64.b64encode(data), 'datas': base64.b64encode(data),
'datas_fname': datas_fname, 'datas_fname': datas_fname,
'public': True, 'public': res_model == 'ir.ui.view',
'res_id': res_id, 'res_id': res_id,
'res_model': res_model, 'res_model': res_model,
}) })
@@ -83,12 +83,50 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
// Private // Private
//-------------------------------------------------------------------------- //--------------------------------------------------------------------------
/**
* 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 * @private
* @returns {Object} the summernote configuration * @returns {Object} the summernote configuration
*/ */
_getSummernoteConfig: function () { _getSummernoteConfig: function () {
var summernoteConfig = { var summernoteConfig = {
model: this.model,
id: this.res_id,
focus: false, focus: false,
height: 180, height: 180,
toolbar: [ toolbar: [
@@ -107,6 +145,22 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
lang: "odoo", lang: "odoo",
onChange: this._doDebouncedAction.bind(this), 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) { if (config.debug) {
summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]); summernoteConfig.toolbar.splice(7, 0, ['view', ['codeview']]);
} }
@@ -124,6 +178,34 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
} }
return this.$content.html(); 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 * @override
* @private * @private
@@ -134,8 +216,6 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
this.$textarea.summernote(this._getSummernoteConfig()); this.$textarea.summernote(this._getSummernoteConfig());
this.$content = this.$('.note-editable:first'); this.$content = this.$('.note-editable:first');
this.$content.html(this._textToHtml(this.value)); 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 // trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup'); this.$content.trigger('mouseup');
if (this.nodeOptions['style-inline']) { if (this.nodeOptions['style-inline']) {
@@ -397,6 +397,7 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
media: media, media: media,
options: { options: {
lastFilters: ['background'], lastFilters: ['background'],
onUpload: $editable.data('callbacks').onImageUpload,
}, },
}); });
return new $.Deferred().reject(); return new $.Deferred().reject();
@@ -788,6 +789,14 @@ eventHandler.attach = function (oLayoutInfo, options) {
$(document).on("keyup", reRangeSelectKey); $(document).on("keyup", reRangeSelectKey);
var clone_data = false; 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(); var $node = oLayoutInfo.editor();
if ($node.data('oe-model') || $node.data('oe-translation-id')) { if ($node.data('oe-model') || $node.data('oe-translation-id')) {
$node.on('content_changed', function () { $node.on('content_changed', function () {
@@ -1089,6 +1098,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
_.extend({ _.extend({
res_model: data.$editable.data('oe-model'), res_model: data.$editable.data('oe-model'),
res_id: data.$editable.data('oe-id'), res_id: data.$editable.data('oe-id'),
domain: data.$editable.data('oe-media-domain'),
}, data.options), }, data.options),
data.$editable, data.$editable,
data.media data.media
@@ -209,7 +209,9 @@ var ImageWidget = MediaWidget.extend({
this.options = options; this.options = options;
this.accept = options.accept || (options.document ? '*/*' : 'image/*'); 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 = ['|', this.domain = ['|',
'&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id], '&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id],
['res_model', '=', 'ir.ui.view']]; ['res_model', '=', 'ir.ui.view']];
@@ -330,6 +332,11 @@ var ImageWidget = MediaWidget.extend({
self.$media.css(style); 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; return self.media;
}); });
}, },
@@ -504,6 +511,10 @@ var ImageWidget = MediaWidget.extend({
_processFile(attachments[i], error); _processFile(attachments[i], error);
} }
if (self.options.onUpload) {
self.options.onUpload(attachments);
}
function _processFile(attachment, error) { function _processFile(attachment, error) {
var $button = self.$('.o_upload_image_button'); var $button = self.$('.o_upload_image_button');
if (!error) { if (!error) {
+1
View File
@@ -39,6 +39,7 @@ Jesus Zapata jesus@vauxoo.com https://github.com/JesusZapata
Germana Oliveira germana@vauxoo.com https://github.com/goliveirab Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
Edgar Rivero edgar@vauxoo.com https://github.com/egrivero 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 Alexander Olivares alexander@vauxoo.com https://github.com/alxolivares
Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906 Jose Manuel Robles josemanuel@vauxoo.com https://github.com/keylor2906
Erick Birbe erick@vauxoo.com https://github.com/ebirbe Erick Birbe erick@vauxoo.com https://github.com/ebirbe
+1 -1
View File
@@ -190,7 +190,7 @@ Barcodes
-------- --------
Barcodes are images returned by a controller and can easily be embedded in 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 .. code-block:: html
+10
View File
@@ -442,6 +442,10 @@ security-related topics:
only for controlling/managing the installation. only for controlling/managing the installation.
*Never* use any default passwords like admin/admin, even for test/staging databases. *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>`) - Use appropriate database filters ( :option:`--db-filter <odoo-bin --db-filter>`)
to restrict the visibility of your databases according to the hostname. to restrict the visibility of your databases according to the hostname.
See :ref:`db_filter`. See :ref:`db_filter`.
@@ -474,6 +478,12 @@ security-related topics:
and then enable the :option:`proxy mode <odoo-bin --proxy-mode>` option. and then enable the :option:`proxy mode <odoo-bin --proxy-mode>` option.
See also :ref:`https_proxy`. 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 - 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 machines than the production ones. And apply the same security precautions as for
production. production.
+1 -1
View File
@@ -1,7 +1,7 @@
"id","name","code","iso_code","direction","grouping","decimal_point","thousands_sep","date_format","time_format","translatable","week_start" "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_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_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_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_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" "base.lang_bg","Bulgarian / български език","bg_BG","bg","Left-to-Right","[3,0]",",",,"%d.%m.%Y","%H,%M,%S","True","1"
1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format translatable week_start
2 base.lang_en English en_US en Left-to-Right [3,0] . , %m/%d/%Y %H:%M:%S True 7
3 base.lang_am_ET Amharic / አምሃርኛ am_ET am_ET Left-to-Right [3,0] . , %d/%m/%Y %I:%M:%S True 7
4 base.lang_ar_SY Arabic / الْعَرَبيّة ar_SY ar_SY ar Left-to-Right [3,0] . , %d %b, %Y %I:%M:%S True 6
5 base.lang_eu_ES Basque / Euskara eu_ES eu_ES Left-to-Right [] , %a, %Y.eko %bren %da %H:%M:%S True 1
6 base.lang_bs_BA Bosnian / bosanski jezik bs_BA bs Left-to-Right [] , %d.%m.%Y %H:%M:%S True 7
7 base.lang_bg Bulgarian / български език bg_BG bg Left-to-Right [3,0] , %d.%m.%Y %H,%M,%S True 1
+12
View File
@@ -12044,6 +12044,18 @@ msgstr ""
msgid "Memos pad" msgid "Memos pad"
msgstr "" 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 #. module: base
#: model:ir.model.fields,field_description:base.field_ir_ui_menu__name #: 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
+7
View File
@@ -466,6 +466,13 @@ class IrModelFields(models.Model):
def _onchange_ttype(self): def _onchange_ttype(self):
self.copy = (self.ttype != 'one2many') self.copy = (self.ttype != 'one2many')
if self.ttype == 'many2many' and self.model_id and self.relation: 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) names = self._custom_many2many_names(self.model_id.model, self.relation)
self.relation_table, self.column1, self.column2 = names self.relation_table, self.column1, self.column2 = names
else: else: