[MERGE] forward port branch saas-11.2 up to fed775bc0c
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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}],
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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'))):
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.'))
|
||||
|
||||
@@ -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')]"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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("'", "\'") +
|
||||
"'"),
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -2334,6 +2334,21 @@ var FieldReference = FieldMany2One.extend({
|
||||
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
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -361,6 +361,10 @@
|
||||
&:hover .o_form_image_controls {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.o_field_invalid > img {
|
||||
border: 1px solid @brand-danger;
|
||||
}
|
||||
}
|
||||
|
||||
// Input loading/file
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});});
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -89,12 +89,50 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
|
||||
// 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
|
||||
* @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']]);
|
||||
}
|
||||
@@ -130,6 +184,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']) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
@@ -12044,6 +12044,18 @@ msgstr ""
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user