[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>
<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.")
@@ -72,10 +72,10 @@ class AccountPayment(models.Model):
@api.model
def create(self, vals):
if vals['payment_method_id'] == self.env.ref('account_check_printing.account_payment_method_check').id\
and vals.get('check_manual_sequencing'):
sequence = self.env['account.journal'].browse(vals['journal_id']).check_sequence_id
vals.update({'check_number': sequence.next_by_id()})
if vals['payment_method_id'] == self.env.ref('account_check_printing.account_payment_method_check').id:
journal = self.env['account.journal'].browse(vals['journal_id'])
if journal.check_manual_sequencing:
vals.update({'check_number': journal.check_sequence_id.next_by_id()})
return super(AccountPayment, self).create(vals)
@api.multi
@@ -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();
});
});
+4 -4
View File
@@ -20,7 +20,7 @@ FormView.include({
*/
init: function (viewInfo) {
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) {
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 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));
+8 -8
View File
@@ -124,8 +124,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);
@@ -156,7 +156,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");
@@ -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(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();
});
@@ -293,8 +293,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);
@@ -340,8 +340,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);
+16 -7
View File
@@ -617,13 +617,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
@@ -1148,6 +1156,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)
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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],
+18 -21
View File
@@ -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'))):
+2 -2
View File
@@ -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])
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -425,7 +425,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.'))
+1
View File
@@ -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')]"/>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -95,7 +95,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:
+5 -2
View File
@@ -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("'", "\'") +
"'"),
}
+11 -8
View File
@@ -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):
@@ -1241,9 +1241,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({
@@ -2302,6 +2302,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
//--------------------------------------------------------------------------
@@ -3614,8 +3614,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]},
@@ -3623,6 +3624,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));
@@ -250,6 +250,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;
@@ -278,7 +279,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);
});
},
/**
+4
View File
@@ -361,6 +361,10 @@
&:hover .o_form_image_controls {
opacity: 0.8;
}
&.o_field_invalid > img {
border: 1px solid @brand-danger;
}
}
// Input loading/file
@@ -1736,7 +1736,6 @@ QUnit.module('basic_fields', {
assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90",
"the image should correctly set its attributes");
form.destroy();
});
QUnit.test('image fields in subviews are loaded correctly', function (assert) {
@@ -1788,6 +1787,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, {
@@ -11326,6 +11326,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);
@@ -11376,6 +11408,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);
@@ -965,4 +965,45 @@ QUnit.module('Views', {
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,
'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,
})
@@ -83,12 +83,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: [
@@ -107,6 +145,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']]);
}
@@ -124,6 +178,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
@@ -134,8 +216,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) {
+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
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
+1 -1
View File
@@ -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
+10
View File
@@ -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 -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"
"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"
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"
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
+7
View File
@@ -466,6 +466,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: