[MERGE] forward port branch saas-16 up to 3f66814a0d
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -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 <= 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
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user