[MERGE] forward port branch saas-16 up to 3f66814a0d

This commit is contained in:
Christophe Simonis
2017-11-28 15:12:31 +01:00
33 changed files with 559 additions and 47 deletions
+1
View File
@@ -978,6 +978,7 @@ class AccountTax(models.Model):
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
'analytic': tax.analytic,
'price_include': tax.price_include,
})
return {
@@ -773,6 +773,8 @@ var StatementModel = BasicModel.extend({
'__focus': false
});
prop.computed_with_tax = tax.price_include
prop.tax_amount = tax.amount
prop.amount = tax.base;
prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.invalid = !self._isValid(prop);
@@ -1042,7 +1044,8 @@ var StatementModel = BasicModel.extend({
* @returns {object}
*/
_formatToProcessReconciliation: function (line, prop) {
var amount = -prop.amount;
// Do not forward port in master. @CSN will change this
var amount = prop.computed_with_tax && -prop.base_amount || -prop.amount;
if (prop.partial_reconcile === true) {
amount = -line.st_line.amount;
}
@@ -1050,6 +1053,10 @@ var StatementModel = BasicModel.extend({
name : prop.label,
debit : amount > 0 ? amount : 0,
credit : amount < 0 ? -amount : 0,
// This one isn't usefull for the server,
// But since we need to change the amount (and thus its semantics) into base_amount
// It might be useful to have a trace in the RPC for debugging purposes
computed_with_tax: prop.computed_with_tax,
};
if (!isNaN(prop.id)) {
result.counterpart_aml_id = prop.id;
+4
View File
@@ -221,6 +221,10 @@ FormRenderer.include({
params: {action_id: params.actionID}
})
.then(function (action) {
if (!action) {
// the action does not exist anymore
return $.when();
}
var view = _.find(action.views, function (descr) {
return descr[1] === params.viewType;
});
@@ -365,4 +365,42 @@ QUnit.test('twice the same action in a dashboard', function (assert) {
form.destroy();
});
QUnit.test('non-existing action in a dashboard', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'board',
data: this.data,
arch: '<form string="My Dashboard">' +
'<board style="2-1">' +
'<column>' +
'<action context="{}" view_mode="kanban" string="ABC" name="51" domain="[]"></action>' +
'</column>' +
'</board>' +
'</form>',
intercepts: {
load_views: function () {
throw new Error('load_views should not be called');
}
},
mockRPC: function (route) {
if (route === '/board/static/src/img/layout_1-1-1.png') {
return $.when();
}
if (route === '/web/action/load') {
// server answer if the action doesn't exist anymore
return $.when(false);
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.oe_action:contains(ABC)').length, 1,
"there should be a box for the non-existing action");
form.destroy();
});
});
-5
View File
@@ -599,11 +599,6 @@ msgstr ""
msgid "Private Information"
msgstr ""
#. module: hr
#: model:ir.ui.view,arch_db:hr.res_config_settings_view_form
msgid "Prout prout prout"
msgstr ""
#. module: hr
#: model:ir.ui.view,arch_db:hr.view_hr_job_form
msgid "Recruitment"
@@ -9,7 +9,7 @@
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Employees" string="Employees" data-key="hr" groups="hr.group_hr_manager">
<h2>Work Organization</h2>
<div class="row mt16 o_settings_container" title="Prout prout prout">
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label for="resource_calendar_id"/>
@@ -401,7 +401,7 @@ elif salary &lt;= 2461.27:
<field name="category_id" ref="hr_payroll_head_div_impos"/>
<field name="name">ATN Company Car</field>
<field name="code">ATN.CAR</field>
<field name="sequence">99</field>
<field name="sequence">70</field>
<field name="condition_select">python</field>
<field name="condition_python">
result = bool(contract.transport_mode == 'company_car' and contract.car_atn)
@@ -621,7 +621,7 @@ result = -contract.car_atn
<record id="hr_contract_advantage_template_commission_on_target" model="hr.contract.advantage.template">
<field name="name">Commission on target</field>
<field name="code">commission_on_target</field>
<field name="default_value">1000.0</field>
<field name="default_value">0.0</field>
</record>
<record id="hr_contract_advantage_template_fuel_card" model="hr.contract.advantage.template">
<field name="name">Fuel card</field>
@@ -205,7 +205,7 @@ class HrContract(models.Model):
- 12.0 * contract.transport_employer_cost \
- (1.326 / 1.05 * 12.0) * contract.commission_on_target \
- 220.0 * contract.meal_voucher_paid_by_employer
gross = remaining_for_gross / (12.0 * 0.05 + 13.0 + 13.0 * 0.3507 + 0.92)
gross = remaining_for_gross / (13.0 + 13.0 * 0.3507 + 0.92)
return gross
+2 -3
View File
@@ -44,10 +44,9 @@ class ResPartnerBank(models.Model):
@api.depends('acc_number')
def _compute_l10n_ch_postal(self):
for record in self:
if record.acc_type == 'postal':
record.l10n_ch_postal = record.sanitized_acc_number
elif record.acc_type == 'iban':
if record.acc_type == 'iban':
record.l10n_ch_postal = record._retrieve_l10n_ch_postal(record.sanitized_acc_number)
record.l10n_ch_postal = record.sanitized_acc_number
def _retrieve_l10n_ch_postal(self, iban):
""" Reads a swiss postal account number from a an IBAN and returns it as
@@ -1878,11 +1878,18 @@ var PaymentScreenWidget = ScreenWidget.extend({
this.reset_input();
this.render_paymentlines();
this.order_changes();
// that one comes from BarcodeEvents
$('body').keypress(this.keyboard_handler);
// that one comes from the pos, but we prefer to cover all the basis
$('body').keydown(this.keyboard_keydown_handler);
// legacy vanilla JS listeners
window.document.body.addEventListener('keypress',this.keyboard_handler);
window.document.body.addEventListener('keydown',this.keyboard_keydown_handler);
this._super();
},
hide: function(){
$('body').off('keypress', this.keyboard_handler);
$('body').off('keydown', this.keyboard_keydown_handler);
window.document.body.removeEventListener('keypress',this.keyboard_handler);
window.document.body.removeEventListener('keydown',this.keyboard_keydown_handler);
this._super();
+10
View File
@@ -31,6 +31,16 @@ class ResourceMixin(models.AbstractModel):
values['resource_id'] = resource.id
return super(ResourceMixin, self).create(values)
@api.multi
def copy_data(self, default=None):
if default is None:
default = {}
resource = self.resource_id.copy()
default['resource_id'] = resource.id
default['company_id'] = resource.company_id.id
default['resource_calendar_id'] = resource.calendar_id.id
return super(ResourceMixin, self).copy_data(default)
def get_work_days_count(self, from_datetime, to_datetime, calendar=None):
""" Return the number of work days for the resource, taking into account
leaves. An optional calendar can be given in case multiple calendars can
@@ -187,7 +187,10 @@ var DateTime = Field.extend({
{value: "∄", text: _lt("is not set")}
],
get_value: function (index) {
return this["datewidget_" + (index || 0)].getValue();
// retrieve the datepicker value
var value = this["datewidget_" + (index || 0)].getValue();
// convert to utc
return value.add(-this.getSession().getTZOffset(value), 'minutes');
},
get_domain: function (field, operator) {
switch (operator.value) {
+9 -3
View File
@@ -51,9 +51,15 @@ return {
}
if (options.method === 'read_group') {
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
if (!(params.args && params.args[0] !== undefined)) {
params.kwargs.domain = options.domain || params.domain || params.kwargs.domain || [];
}
if (!(params.args && params.args[1] !== undefined)) {
params.kwargs.fields = options.fields || params.fields || params.kwargs.fields || [];
}
if (!(params.args && params.args[2] !== undefined)) {
params.kwargs.groupby = options.groupBy || params.groupBy || params.kwargs.groupby || [];
}
params.kwargs.offset = options.offset || params.offset || params.kwargs.offset;
params.kwargs.limit = options.limit || params.limit || params.kwargs.limit;
// In kwargs, we look for "orderby" rather than "orderBy" (note the absence of capital B),
@@ -423,8 +423,12 @@ return core.Class.extend({
if (fieldsInfo[node.attrs.name].fieldDependencies) {
var deps = fieldsInfo[node.attrs.name].fieldDependencies;
for (var dependency_name in deps) {
var dependency_dict = {name: dependency_name, type: deps[dependency_name].type};
if (!(dependency_name in fieldsInfo)) {
fieldsInfo[dependency_name] = {'name': dependency_name, 'type': deps[dependency_name].type, 'options': deps[dependency_name].options || {}};
fieldsInfo[dependency_name] = _.extend({}, dependency_dict, {options: deps[dependency_name].options || {}});
}
if (!(dependency_name in fields)) {
fields[dependency_name] = dependency_dict;
}
}
}
@@ -226,9 +226,6 @@ var AbstractView = Class.extend({
return;
}
attrs.limit = attrs.mode === "tree" ? 80 :
(attrs.widget === 'many2many_tags' ? 1000 : 40);
if (attrs.Widget.prototype.useSubview && !attrs.__no_fetch && !attrs.views[attrs.mode]) {
var context = {};
var regex = /'([a-z]*_view_ref)' *: *'(.*?)'/g;
@@ -244,12 +241,30 @@ var AbstractView = Class.extend({
for (var viewName in views) {
attrs.views[viewName] = views[viewName];
}
self._setSubViewLimit(attrs);
}));
} else {
self._setSubViewLimit(attrs);
}
});
}
return $.when.apply($, defs);
}
},
/**
* We set here the limit for the number of records fetched (in one page).
* This method is only called for subviews, not for main views.
*
* @private
* @param {Object} attrs
*/
_setSubViewLimit: function (attrs) {
var view = attrs.views && attrs.views[attrs.mode];
var limit = view && view.arch.attrs.limit && parseInt(view.arch.attrs.limit);
if (!limit && attrs.widget === 'many2many_tags') {
limit = 1000;
}
attrs.limit = limit || 40;
},
});
return AbstractView;
@@ -1588,7 +1588,9 @@ var BasicModel = AbstractModel.extend({
return $.when.apply($, defs).then(function () {
// ensure to fetch up to 'limit' records (may be useful if records of
// the current page have been removed)
return self._readUngroupedList(list);
return self._readUngroupedList(list).then(function () {
return self._fetchX2ManysBatched(list);
});
});
},
/**
@@ -2516,6 +2518,8 @@ var BasicModel = AbstractModel.extend({
changes[fieldName] = value ?
this.localData[value].model + ',' + this.localData[value].res_id :
false;
} else if (type === 'char' && changes[fieldName] === '') {
changes[fieldName] = false;
} else if (changes[fieldName] === null) {
changes[fieldName] = false;
}
@@ -356,6 +356,17 @@ var BasicRenderer = AbstractRenderer.extend({
_hasContent: function () {
return this.state.count !== 0;
},
/**
* This function is called each time a field widget is created, when it is
* ready (after its willStart and Start methods are complete). This is the
* place where work having to do with $el should be done.
*
* @private
* @param {Widget} widget the field widget instance
* @param {Object} node the attrs coming from the arch
*/
_postProcessField: function (widget, node) {
},
/**
* Registers or updates the modifiers data associated to the given node.
* This method is quiet complex as it handles all the needs of the basic
@@ -524,6 +535,7 @@ var BasicRenderer = AbstractRenderer.extend({
));
},
}, modifiersOptions || {}));
self._postProcessField(widget, node);
});
return widget;
@@ -485,6 +485,7 @@ var FormController = BasicController.extend({
* @param {OdooEvent} event
*/
_onOpenRecord: function (event) {
event.stopPropagation();
var self = this;
var record = this.model.get(event.data.id, {raw: true});
new dialogs.FormViewDialog(self, {
@@ -246,6 +246,17 @@ var FormRenderer = BasicRenderer.extend({
}
return idForLabel;
},
/**
* @override
* @private
*/
_postProcessField: function (widget, node) {
this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
this._handleAttributes(widget.$el, node);
if (JSON.parse(node.attrs.default_focus || "0")) {
this.defaultFocusField = widget;
}
},
/**
* @private
* @param {Object} node
@@ -307,23 +318,6 @@ var FormRenderer = BasicRenderer.extend({
this._registerModifiers(node, this.state, $result);
return $result;
},
/**
* @override
* @private
* @param {string} node
* @param {Object} record
* @param {Object} [options]
* @returns {AbstractField}
*/
_renderFieldWidget: function (node, record, options, modifiersOptions) {
var widget = this._super.apply(this, arguments);
this._setIDForLabel(widget, this._getIDForLabel(node.attrs.name));
this._handleAttributes(widget.$el, node);
if (JSON.parse(node.attrs.default_focus || "0")) {
this.defaultFocusField = widget;
}
return widget;
},
/**
* @private
* @param {Object} node
@@ -52,7 +52,7 @@ var KanbanColumn = Widget.extend({
this.grouped_by_m2o = options.grouped_by_m2o;
this.editable = options.editable;
this.deletable = options.deletable;
this.draggable = recordOptions.draggable;
this.draggable = options.draggable;
this.records_editable = options.records_editable;
this.records_deletable = options.records_deletable;
this.relation = options.relation;
@@ -97,12 +97,13 @@ var KanbanColumn = Widget.extend({
}
this.$header.find('.o_kanban_header_title').tooltip();
if (!config.device.isMobile && this.draggable !== false) {
if (!config.device.isMobile) {
// deactivate sortable in mobile mode. It does not work anyway,
// and it breaks horizontal scrolling in kanban views. Someday, we
// should find a way to use the touch events to make sortable work.
this.$el.sortable({
connectWith: '.o_kanban_group',
containment: this.draggable ? '.o_kanban_view' : 'parent',
revert: 0,
delay: 0,
items: '> .o_kanban_record:not(.o_updating)',
@@ -36,7 +36,6 @@ var KanbanRecord = Widget.extend({
this.options = options;
this.editable = options.editable;
this.deletable = options.deletable;
this.draggable = options.draggable;
this.read_only_mode = options.read_only_mode;
this.qweb = options.qweb;
this.subWidgets = {};
@@ -323,7 +323,7 @@ var KanbanRenderer = BasicRenderer.extend({
var groupByFieldInfo = state.fieldsInfo.kanban[state.groupedBy[0]];
// Deactivate the drag'n'drop if the groupedBy field:
// - is a date or datetime since we group by month or
// - is readonly
// - is readonly (on the field attrs or in the view)
var draggable = true;
if (groupByFieldAttrs) {
if (groupByFieldAttrs.type === "date" || groupByFieldAttrs.type === "datetime") {
@@ -332,6 +332,11 @@ var KanbanRenderer = BasicRenderer.extend({
draggable = !(groupByFieldAttrs.readonly);
}
}
if (groupByFieldInfo) {
if (draggable && groupByFieldInfo.readonly !== undefined) {
draggable = !(groupByFieldInfo.readonly);
}
}
this.groupedByM2O = groupByFieldAttrs && (groupByFieldAttrs.type === 'many2one');
var grouped_by_field = this.groupedByM2O && groupByFieldAttrs.relation;
var groupByTooltip = groupByFieldInfo && groupByFieldInfo.options.group_by_tooltip;
@@ -15,6 +15,7 @@
}
}
.o_boxed_footer {
white-space: nowrap;
border-top: 3px solid @gray;
ul {
margin: 4px 0;
+41
View File
@@ -243,6 +243,47 @@ QUnit.module('core', {}, function () {
assert.deepEqual(query.params.kwargs.lazy, undefined, "should not enforce a default value for lazy");
});
QUnit.test('read_group with args and kwargs', function (assert) {
assert.expect(9);
var query = rpc.buildQuery({
model: 'partner',
method: 'read_group',
kwargs: {
domain: ['name', '=', 'saucisse'],
fields: ['category_id'],
groupby: ['country_id'],
},
});
assert.deepEqual(query.params.kwargs.domain, ['name', '=', 'saucisse'], "should have ['name', '=', 'saucisse'] category_id as default domain");
assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
var query = rpc.buildQuery({
model: 'partner',
method: 'read_group',
args: [['name', '=', 'saucisse']],
kwargs: {
fields: ['category_id'],
groupby: ['country_id'],
},
});
assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
assert.deepEqual(query.params.kwargs.fields, ['category_id'], "should have category_id as default fields");
assert.deepEqual(query.params.kwargs.groupby, ['country_id'], "should have country_id as default groupby");
var query = rpc.buildQuery({
model: 'partner',
method: 'read_group',
args: [['name', '=', 'saucisse'], ['category_id'], ['country_id']],
});
assert.deepEqual(query.params.kwargs.domain, undefined, "should not enforce a default value for domain");
assert.deepEqual(query.params.kwargs.fields, undefined, "should not enforce a default value for fields");
assert.deepEqual(query.params.kwargs.groupby, undefined, "should not enforce a default value for groupby");
});
QUnit.test('search_read with no domain, nor fields', function (assert) {
assert.expect(5);
var query = rpc.buildQuery({
@@ -738,6 +738,38 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('setting a char field to empty string is saved as a false value', function (assert) {
assert.expect(1);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<sheet>' +
'<group>' +
'<field name="foo"/>' +
'</group>' +
'</sheet>' +
'</form>',
res_id: 1,
viewOptions: {mode: 'edit'},
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.strictEqual(args.args[1].foo, false,
'the foo value should be false');
}
return this._super.apply(this, arguments);
}
});
form.$('input[type="text"].o_field_widget').val('').trigger('input');
// save
form.$buttons.find('.o_form_button_save').click();
form.destroy();
});
QUnit.test('char field with size attribute', function (assert) {
assert.expect(1);
@@ -1502,6 +1534,55 @@ QUnit.module('basic_fields', {
});
QUnit.test('image fields in subviews are loaded correctly', function (assert) {
assert.expect(5);
this.data.partner.records[0].__last_update = '2017-02-08 10:00:00';
this.data.partner.records[0].document = 'myimage';
this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
this.data.partner_type.records[0].image = 'product_image';
this.data.partner.records[0].timmy = [12];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="document" widget="image" options="{\'size\': [90, 90]}"/>' +
'<field name="timmy" widget="many2many">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="image" widget="image"/>' +
'</form>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (route === 'data:image/png;base64,myimage') {
assert.step("The view's image should have been fetched");
return $.when('wow');
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(["The view's image should have been fetched"]);
assert.strictEqual(form.$('tr.o_data_row').length, 1,
'There should be one record in the many2many');
// Actual flow: click on an element of the m2m to get its form view
form.$('tbody td:contains(gold)').click();
assert.strictEqual($('.modal-dialog').length, 1,
'The modal should have opened');
assert.strictEqual($('.modal-dialog').find('.o_field_image > img')[0].src,
'data:image/png;base64,product_image',
'The image of the many2many in its form view should be present');
form.destroy();
});
QUnit.module('JournalDashboardGraph', {
beforeEach: function () {
_.extend(this.data.partner.fields, {
@@ -2124,6 +2124,65 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('use the limit attribute in arch (in field o2m inline tree view)', function (assert) {
assert.expect(2);
this.data.partner.records[0].turtles = [1,2,3];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles">' +
'<tree limit="2">' +
'<field name="turtle_foo"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
if (args.model === 'turtle') {
assert.deepEqual(args.args[0], [1,2],
'should only load first 2 records');
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_data_row').length, 2,
'should display 2 data rows');
form.destroy();
});
QUnit.test('use the limit attribute in arch (in field o2m non inline tree view)', function (assert) {
assert.expect(2);
this.data.partner.records[0].turtles = [1,2,3];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="turtles"/>' +
'</form>',
archs: {
'turtle,false,list': '<tree limit="2"><field name="turtle_foo"/></tree>',
},
res_id: 1,
mockRPC: function (route, args) {
if (args.model === 'turtle') {
assert.deepEqual(args.args[0], [1,2],
'should only load first 2 records');
}
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_data_row').length, 2,
'should display 2 data rows');
form.destroy();
});
QUnit.test('embedded one2many with widget', function (assert) {
assert.expect(1);
@@ -5248,6 +5307,52 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('many2manytag in one2many, onchange, some modifiers, and more than one page', function (assert) {
assert.expect(9);
this.data.partner.records[0].turtles = [1,2,3];
this.data.partner.onchanges.turtles = function () {};
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<field name="turtles">' +
'<tree editable="top" limit="2">' +
'<field name="turtle_foo"/>' +
'<field name="partner_ids" widget="many2many_tags" attrs="{\'readonly\': [(\'turtle_foo\', \'=\', \'a\')]}"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
viewOptions: {mode: 'edit'},
mockRPC: function (route, args) {
assert.step(args.method);
return this._super.apply(this, arguments);
},
});
assert.strictEqual(form.$('.o_data_row').length, 2,
'there should be only 2 rows displayed');
form.$('.o_list_record_delete').click();
form.$('.o_list_record_delete').click();
assert.strictEqual(form.$('.o_data_row').length, 1,
'there should be just one remaining row');
assert.verifySteps([
"read", // initial read on partner
"read", // initial read on turtle
"read", // batched read on partner (field partner_ids)
"read", // after first delete, read on turtle (to fetch 3rd record)
"onchange", // after first delete, onchange on field turtles
"onchange" // onchange after second delete
]);
form.destroy();
});
QUnit.test('onchange many2many in one2many list editable', function (assert) {
assert.expect(14);
+79 -2
View File
@@ -167,6 +167,37 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('attributes are transferred on async widgets', function (assert) {
assert.expect(1);
var def = $.Deferred();
var FieldChar = fieldRegistry.get('char');
fieldRegistry.add('asyncwidget', FieldChar.extend({
willStart: function () {
return def;
},
}));
createAsyncView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form string="Partners">' +
'<group>' +
'<field name="foo" style="color: blue" widget="asyncwidget"/>' +
'</group>' +
'</form>',
res_id: 2,
}).then(function (form) {
assert.strictEqual(form.$('.o_field_widget[name=foo]').attr('style'), 'color: blue',
"should apply style attribute on fields");
form.destroy();
delete fieldRegistry.map.asyncwidget;
});
def.resolve();
});
QUnit.test('only necessary fields are fetched with correct context', function (assert) {
assert.expect(2);
@@ -1858,6 +1889,52 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('circular many2many\'s', function (assert) {
assert.expect(4);
this.data.partner_type.fields.partner_ids = {string: "partners", type: "many2many", relation: 'partner'}
this.data.partner.records[0].timmy = [12];
this.data.partner_type.records[0].partner_ids = [1];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners">' +
'<field name="timmy">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="partner_ids">' +
'<tree>' +
'<field name="display_name"/>' +
'</tree>' +
'<form>' +
'<field name="display_name"/>' +
'</form>' +
'</field>' +
'</form>' +
'</field>' +
'</form>',
res_id: 1,
});
assert.strictEqual(form.$('td:contains(gold)').length, 1,
"should display the name of the many2many on the original form");
form.$('td:contains(gold)').click();
assert.strictEqual($('.modal-dialog').length, 1,
'The partner_type modal should have opened');
assert.strictEqual($('.modal-dialog').find('td:contains(first record)').length, 1,
"should display the name of the many2many on the modal form");
$('.modal-dialog').find('td:contains(first record)').click();
assert.strictEqual($('.modal-dialog').length, 2,
'There should be 2 modals (partner on top of partner_type) opened');
form.destroy();
});
QUnit.test('discard changes on a non dirty form view', function (assert) {
assert.expect(4);
@@ -5636,9 +5713,9 @@ QUnit.module('Views', {
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('input[name="foo"]').val(), '***',
"password should be displayed with stars");
assert.strictEqual(form.$('input[name="display_name"]').prop('autocomplete'), 'coucou',
assert.strictEqual(form.$('input[name="display_name"]').attr('autocomplete'), 'coucou',
"attribute autocomplete should be set");
assert.strictEqual(form.$('input[name="foo"]').prop('autocomplete'), 'new-password',
assert.strictEqual(form.$('input[name="foo"]').attr('autocomplete'), 'new-password',
"attribute autocomplete should be set to 'new-password' on password input");
form.destroy();
});
@@ -718,6 +718,79 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('prevent drag and drop of record if grouped by readonly', function (assert) {
assert.expect(12);
this.data.partner.fields.foo.readonly = true;
var kanban = createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban>' +
'<templates>' +
'<t t-name="kanban-box"><div>' +
'<field name="foo"/>' +
'<field name="state" readonly="1"/>' +
'</div></t>' +
'</templates>' +
'</kanban>',
mockRPC: function (route, args) {
if (route === '/web/dataset/resequence') {
return $.when();
}
if (args.model === 'partner' && args.method === 'write') {
throw new Error('should not be draggable');
}
return this._super(route, args);
},
});
// simulate an update coming from the searchview, with another groupby given
kanban.update({groupBy: ['state']});
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
"column should contain 1 record(s)");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 1,
"column should contain 1 record(s)");
// drag&drop a record in another column
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
var $group = kanban.$('.o_kanban_group:nth-child(2)');
testUtils.dragAndDrop($record, $group);
// should not be draggable
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
"column should now contain 1 record(s)");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 1,
"column should contain 1 record(s)");
// simulate an update coming from the searchview, with another groupby given
kanban.update({groupBy: ['foo']});
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
"column should contain 1 record(s)");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
"column should contain 2 record(s)");
// drag&drop a record in another column
$record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
$group = kanban.$('.o_kanban_group:nth-child(2)');
testUtils.dragAndDrop($record, $group);
// should not be draggable
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
"column should now contain 1 record(s)");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record').length, 2,
"column should contain 2 record(s)");
// drag&drop a record in the same column
var $record1 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)');
var $record2 = kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)');
assert.strictEqual($record1.text(), "blipDEF", "first record should be DEF");
assert.strictEqual($record2.text(), "blipGHI", "second record should be GHI");
testUtils.dragAndDrop($record2, $record1, {position: 'top'});
// should still be able to resequence
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(0)').text(), "blipGHI",
"records should have been resequenced");
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(2) .o_kanban_record:eq(1)').text(), "blipDEF",
"records should have been resequenced");
kanban.destroy();
});
QUnit.test('kanban view with default_group_by', function (assert) {
assert.expect(7);
this.data.partner.records.product_id = 1;
@@ -3190,6 +3190,30 @@ QUnit.module('Views', {
list.destroy();
delete widgetRegistry.map.test;
});
QUnit.test('use the limit attribute in arch', function (assert) {
assert.expect(3);
var list = createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree limit="2"><field name="foo"/></tree>',
mockRPC: function (route, args) {
assert.strictEqual(args.limit, 2,
'should use the correct limit value');
return this._super.apply(this, arguments);
},
});
assert.strictEqual(list.pager.$el.text().trim(), '1-2 / 4',
"pager should be correct");
assert.strictEqual(list.$('.o_data_row').length, 2,
'should display 2 data rows');
list.destroy();
});
});
});
@@ -167,6 +167,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
this.running_step_delay = parseInt(local_storage.getItem(get_running_delay_key()), 10) || 10;
this.edition = (_.last(session.server_version_info) === 'e') ? 'enterprise' : 'community';
this._log = [];
console.log('Tour Manager is ready. running_tour=' + this.running_tour);
},
/**
* Registers a tour described by the following arguments (in order)
@@ -236,6 +237,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
}).bind(this));
},
run: function (tour_name, step_delay) {
console.log(_.str.sprintf("Preparing tour %s", tour_name));
if (this.running_tour) {
this._deactivate_tip(this.active_tooltips[this.running_tour]);
this._consume_tour(this.running_tour, _.str.sprintf("Killing tour %s", this.running_tour));
@@ -57,7 +57,7 @@
<h4 class="media-heading">
<a itemprop="url" t-att-class="event.state == 'done' and 'text-success'" t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register' or ''}"><span itemprop="name" t-field="event.name"> </span></a>
<small t-if="event.is_participating" class="label label-info">Participating</small>
<small t-if="not event.is_online" class="label label-info">Online</small>
<small t-if="event.is_online" class="label label-info">Online</small>
<small t-if="not event.website_published" class="label label-danger">unpublished</small>
</h4>
<div>
+2
View File
@@ -171,6 +171,8 @@ root can have the following attributes:
``create``, ``edit``, ``delete``
allows *dis*\ abling the corresponding action in the view by setting the
corresponding attribute to ``false``
``limit``
the default size of a page. It should be a positive integer
``on_write``
only makes sense on an ``editable`` list. Should be the name of a method
on the list's model. The method will be called with the ``id`` of a record
+1
View File
@@ -61,6 +61,7 @@ function PhantomTest() {
console.log('<phantomLog>'+message+'</phantomLog>');
};
this.page.onLoadFinished = function(status) {
console.log('PhantomJS: onLoadFinished. Status=', status);
if (status === "success") {
for (var k in self.inject) {
var found = false;