[MERGE] forward port branch saas-16 up to aec6248bb3
This commit is contained in:
@@ -499,7 +499,6 @@ class AccountBankStatementLine(models.Model):
|
||||
'st_line': st_line.get_statement_line_for_reconciliation_widget(),
|
||||
'reconciliation_proposition': rp
|
||||
})
|
||||
|
||||
return ret
|
||||
|
||||
def get_statement_line_for_reconciliation_widget(self):
|
||||
|
||||
@@ -79,10 +79,8 @@ class AccountConfigSettings(models.TransientModel):
|
||||
self.env.ref('base.group_user').write({'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)]})
|
||||
""" Set the product taxes if they have changed """
|
||||
ir_values_obj = self.env['ir.values']
|
||||
if self.default_sale_tax_id:
|
||||
ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id], for_all_users=True, company_id=self.company_id.id)
|
||||
if self.default_purchase_tax_id:
|
||||
ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id], for_all_users=True, company_id=self.company_id.id)
|
||||
ir_values_obj.sudo().set_default('product.template', "taxes_id", [self.default_sale_tax_id.id] if self.default_sale_tax_id else False, for_all_users=True, company_id=self.company_id.id)
|
||||
ir_values_obj.sudo().set_default('product.template', "supplier_taxes_id", [self.default_purchase_tax_id.id] if self.default_purchase_tax_id else False, for_all_users=True, company_id=self.company_id.id)
|
||||
""" install a chart of accounts for the given company (if required) """
|
||||
if self.chart_template_id and self.chart_template_id != self.company_id.chart_template_id:
|
||||
wizard = self.env['wizard.multi.charts.accounts'].create({
|
||||
|
||||
@@ -338,9 +338,9 @@ class AccountInvoice(models.Model):
|
||||
related='partner_id.commercial_partner_id', store=True, readonly=True,
|
||||
help="The commercial entity that will be used on Journal Entries for this invoice")
|
||||
|
||||
outstanding_credits_debits_widget = fields.Text(compute='_get_outstanding_info_JSON')
|
||||
payments_widget = fields.Text(compute='_get_payment_info_JSON')
|
||||
has_outstanding = fields.Boolean(compute='_get_outstanding_info_JSON')
|
||||
outstanding_credits_debits_widget = fields.Text(compute='_get_outstanding_info_JSON', groups="account.group_account_invoice")
|
||||
payments_widget = fields.Text(compute='_get_payment_info_JSON', groups="account.group_account_invoice")
|
||||
has_outstanding = fields.Boolean(compute='_get_outstanding_info_JSON', groups="account.group_account_invoice")
|
||||
|
||||
#fields use to set the sequence, on the first invoice of the journal
|
||||
sequence_number_next = fields.Char(string='Next Number', compute="_get_sequence_prefix", inverse="_set_sequence_next")
|
||||
@@ -741,7 +741,7 @@ class AccountInvoice(models.Model):
|
||||
self.ensure_one()
|
||||
credit_aml = self.env['account.move.line'].browse(credit_aml_id)
|
||||
if not credit_aml.currency_id and self.currency_id != self.company_id.currency_id:
|
||||
credit_aml.with_context(allow_amount_currency=True).write({
|
||||
credit_aml.with_context(allow_amount_currency=True, check_move_validity=False).write({
|
||||
'amount_currency': self.company_id.currency_id.with_context(date=credit_aml.date).compute(credit_aml.balance, self.currency_id),
|
||||
'currency_id': self.currency_id.id})
|
||||
if credit_aml.payment_id:
|
||||
|
||||
@@ -46,7 +46,7 @@ var ShowPaymentLineWidget = AbstractField.extend({
|
||||
k.index = v;
|
||||
k.amount = field_utils.format.float(k.amount, {digits: k.digits});
|
||||
if (k.date){
|
||||
k.date = field_utils.format.date(field_utils.parse.date(k.date, {isUTC: true}));
|
||||
k.date = field_utils.format.date(field_utils.parse.date(k.date, {}, {isUTC: true}));
|
||||
}
|
||||
});
|
||||
this.$el.html(QWeb.render('ShowPaymentInfo', {
|
||||
|
||||
@@ -30,6 +30,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
validate_all_balanced: '_onValidate',
|
||||
change_name: '_onChangeName',
|
||||
close_statement: '_onCloseStatement',
|
||||
load_more: '_onLoadMore',
|
||||
},
|
||||
config: {
|
||||
// used to instanciate the model
|
||||
@@ -71,6 +72,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
'bank_statement_id': self.model.bank_statement_id,
|
||||
'valuenow': self.model.valuenow,
|
||||
'valuemax': self.model.valuemax,
|
||||
'defaultDisplayQty': self.model.defaultDisplayQty,
|
||||
'title': self.title,
|
||||
});
|
||||
});
|
||||
@@ -89,12 +91,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
this.update_control_panel({breadcrumbs: breadcrumbs, search_view_hidden: true}, {clear: true});
|
||||
|
||||
this.renderer.prependTo(self.$('.o_form_sheet'));
|
||||
_.each(this.model.lines, function (line, handle) {
|
||||
var widget = new self.config.LineRenderer(self, self.model, line);
|
||||
widget.handle = handle;
|
||||
self.widgets.push(widget);
|
||||
widget.appendTo(self.$('.o_reconciliation_lines'));
|
||||
});
|
||||
this._renderLines();
|
||||
this._openFirstLine();
|
||||
},
|
||||
|
||||
@@ -134,6 +131,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
return _.find(this.widgets, function (widget) {return widget.handle===handle;});
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
_loadMore: function(qty) {
|
||||
var self = this;
|
||||
return this.model.loadMore(qty).then(function () {
|
||||
self._renderLines();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* sitch to 'match' the first available line
|
||||
*
|
||||
@@ -153,6 +159,24 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
}
|
||||
return handle;
|
||||
},
|
||||
/**
|
||||
* render line widget and append to view
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_renderLines: function () {
|
||||
var self = this;
|
||||
var linesToDisplay = this.model.getStatementLines();
|
||||
_.each(linesToDisplay, function (line, handle) {
|
||||
var widget = new self.config.LineRenderer(self, self.model, line);
|
||||
widget.handle = handle;
|
||||
self.widgets.push(widget);
|
||||
widget.appendTo(self.$('.o_reconciliation_lines'));
|
||||
});
|
||||
if (this.model.hasMoreLines() === false) {
|
||||
this.renderer.hideLoadMoreButton();
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
@@ -223,6 +247,14 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Load more statement and render them
|
||||
*
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onLoadMore: function (event) {
|
||||
return this._loadMore(this.model.defaultDisplayQty);
|
||||
},
|
||||
/**
|
||||
* call 'validate' or 'autoReconciliation' model method then destroy the
|
||||
* validated lines and update the action renderer with the new status bar
|
||||
@@ -246,7 +278,15 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
|
||||
});
|
||||
_.each(result.handles, function (handle) {
|
||||
self._getWidget(handle).destroy();
|
||||
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
|
||||
self.widgets.splice(index, 1);
|
||||
});
|
||||
// Get number of widget and if less than constant and if there are more to laod, load until constant
|
||||
if (self.widgets.length < self.model.defaultDisplayQty
|
||||
&& self.model.valuemax - self.model.valuenow >= self.model.defaultDisplayQty) {
|
||||
var toLoad = self.model.defaultDisplayQty - self.widgets.length;
|
||||
self._loadMore(toLoad);
|
||||
}
|
||||
self._openFirstLine();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -100,6 +100,8 @@ var StatementModel = BasicModel.extend({
|
||||
this.lines = {};
|
||||
this.valuenow = 0;
|
||||
this.valuemax = 0;
|
||||
this.alreadyDisplayed = [];
|
||||
this.defaultDisplayQty = 10;
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -230,10 +232,10 @@ var StatementModel = BasicModel.extend({
|
||||
var line = this.getLine(handle);
|
||||
line.st_line.partner_id = partner && partner.id;
|
||||
line.st_line.partner_name = partner && partner.display_name || '';
|
||||
return this._changePartner(handle, partner.id)
|
||||
return $.when(partner && this._changePartner(handle, partner.id))
|
||||
.then(function() {
|
||||
line.reconciliation_proposition = [];
|
||||
return self._performMoveLine(handle)
|
||||
return self.changeMode(handle, 'match');
|
||||
})
|
||||
.then(function () {
|
||||
if (line.mode === 'create') {
|
||||
@@ -253,7 +255,7 @@ var StatementModel = BasicModel.extend({
|
||||
args: [self.bank_statement_id.id],
|
||||
})
|
||||
.then(function () {
|
||||
return self.bank_statement_id.id
|
||||
return self.bank_statement_id.id;
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -283,6 +285,34 @@ var StatementModel = BasicModel.extend({
|
||||
getContext: function () {
|
||||
return this.context;
|
||||
},
|
||||
/**
|
||||
* Return the lines that needs to be displayed by the widget
|
||||
*
|
||||
* @returns {Object} lines that are loaded and not yet displayed
|
||||
*/
|
||||
getStatementLines: function () {
|
||||
var self = this;
|
||||
var linesToDisplay = _.pick(this.lines, function(value, key, object) {
|
||||
if (value.visible === true && self.alreadyDisplayed.indexOf(key) === -1) {
|
||||
self.alreadyDisplayed.push(key);
|
||||
return object;
|
||||
}
|
||||
});
|
||||
return linesToDisplay;
|
||||
},
|
||||
/**
|
||||
* Return a boolean telling if load button needs to be displayed or not
|
||||
*
|
||||
* @returns {boolean} true if load more button needs to be displayed
|
||||
*/
|
||||
hasMoreLines: function () {
|
||||
var self = this;
|
||||
var notDisplayed = _.filter(this.lines, function(line) { return !line.visible; });
|
||||
if (notDisplayed.length > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
/**
|
||||
* get the line data for this handle
|
||||
*
|
||||
@@ -355,14 +385,42 @@ var StatementModel = BasicModel.extend({
|
||||
line.reconcileModels = self.reconcileModels;
|
||||
});
|
||||
var ids = _.pluck(self.lines, 'id');
|
||||
return self._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'get_data_for_reconciliation_widget',
|
||||
args: [ids],
|
||||
})
|
||||
.then(self._formatLine.bind(self));
|
||||
ids = ids.splice(0, self.defaultDisplayQty);
|
||||
self.pagerIndex = ids.length;
|
||||
return self.loadData(ids, []);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Load more bank statement line
|
||||
*
|
||||
* @param {integer} quantity to load
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
loadMore: function(qty) {
|
||||
if (qty === undefined) {
|
||||
qty = this.defaultDisplayQty;
|
||||
}
|
||||
var ids = _.pluck(this.lines, 'id');
|
||||
ids = ids.splice(this.pagerIndex, qty);
|
||||
this.pagerIndex += qty;
|
||||
return this.loadData(ids, this._getExcludedIds());
|
||||
},
|
||||
/**
|
||||
* RPC method to load informations on lines
|
||||
*
|
||||
* @param {Array} ids of bank statement line passed to rpc call
|
||||
* @param {Array} list of move_line ids that needs to be excluded from search
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
loadData: function(ids, excluded_ids) {
|
||||
var self = this;
|
||||
return self._rpc({
|
||||
model: 'account.bank.statement.line',
|
||||
method: 'get_data_for_reconciliation_widget',
|
||||
args: [ids, excluded_ids],
|
||||
})
|
||||
.then(self._formatLine.bind(self));
|
||||
},
|
||||
/**
|
||||
* Add lines into the propositions from the reconcile model
|
||||
* Can add 2 lines, and each with its taxes. The second line become editable
|
||||
@@ -770,6 +828,7 @@ var StatementModel = BasicModel.extend({
|
||||
var line = _.find(self.lines, function (l) {
|
||||
return l.id === data.st_line.id;
|
||||
});
|
||||
line.visible = true;
|
||||
_.extend(line, data);
|
||||
self._formatLineProposition(line, line.reconciliation_proposition);
|
||||
if (!line.reconciliation_proposition.length) {
|
||||
@@ -850,6 +909,26 @@ var StatementModel = BasicModel.extend({
|
||||
prop.amount = prop.base_amount;
|
||||
return prop;
|
||||
},
|
||||
/**
|
||||
* Return list of account_move_line that has been selected and needs to be removed
|
||||
* from other calls.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array} list of excluded ids
|
||||
*/
|
||||
_getExcludedIds: function () {
|
||||
var excludedIds = [];
|
||||
_.each(this.lines, function(line) {
|
||||
if (line.reconciliation_proposition) {
|
||||
_.each(line.reconciliation_proposition, function(prop) {
|
||||
if (parseInt(prop['id'])) {
|
||||
excludedIds.push(prop['id']);
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
return excludedIds;
|
||||
},
|
||||
/**
|
||||
* Defined whether the line is to be displayed or not. Here, we only display
|
||||
* the line if it comes from the server or if an account is defined when it
|
||||
@@ -1168,7 +1247,8 @@ var ManualModel = StatementModel.extend({
|
||||
'filter': "",
|
||||
'reconcileModels': [],
|
||||
'account_id': this._formatNameGet([data.account_id, data.account_name]),
|
||||
'st_line': data
|
||||
'st_line': data,
|
||||
'visible': true
|
||||
});
|
||||
this._formatLineProposition(line, line.reconciliation_proposition);
|
||||
if (!line.reconciliation_proposition.length) {
|
||||
|
||||
@@ -24,6 +24,7 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
|
||||
"click *[rel='do_action']": "_onDoAction",
|
||||
'click button.button_back_to_statement': '_onGoToBankStatement',
|
||||
'click button.button_close_statement': '_onCloseBankStatement',
|
||||
'click button.js_load_more': '_onLoadMore',
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -87,6 +88,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
/*
|
||||
* hide the button to load more statement line
|
||||
*/
|
||||
hideLoadMoreButton: function () {
|
||||
this.$('.js_load_more').hide();
|
||||
},
|
||||
/**
|
||||
* update the statement rendering
|
||||
*
|
||||
@@ -214,6 +221,14 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
|
||||
view_mode: 'form',
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Load more statement lines for reconciliation
|
||||
* @private
|
||||
* @param {MouseEvent} event
|
||||
*/
|
||||
_onLoadMore: function (e) {
|
||||
this.trigger_up('load_more');
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
||||
@@ -166,7 +166,6 @@
|
||||
vertical-align: top;
|
||||
padding-right: 10px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +208,7 @@
|
||||
.o-transition(max-height, 250ms);
|
||||
}
|
||||
&[data-mode="match"] > .match {
|
||||
max-height: 130px;
|
||||
max-height: none;
|
||||
.o-transition(max-height, 400ms);
|
||||
}
|
||||
&[data-mode="create"] > .create {
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
<div class="notification_area"/>
|
||||
</div>
|
||||
<div class="o_reconciliation_lines"/>
|
||||
<div t-if="widget._initialState.valuemax > widget._initialState.defaultDisplayQty">
|
||||
<button class="btn btn-default js_load_more">Load more</button>
|
||||
</div>
|
||||
<div class="pull-right text-muted">Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.</div>
|
||||
</div>
|
||||
<div t-else="" class="o_view_nocontent">
|
||||
@@ -203,7 +206,16 @@
|
||||
<td class="cell_action"><span class="fa fa-add-remove"/></td>
|
||||
<td class="cell_account_code"><t t-esc="line.account_code"/></td>
|
||||
<td class="cell_due_date"><t t-esc="line.date_maturity === false ? line.date : line.date_maturity"/></td>
|
||||
<td class="cell_label"><t t-if="line.partner_id && line.partner_id !== state.st_line.partner_id"><t t-if="line.partner_name.length"><t t-esc="line.partner_name"/>: </t><t t-esc="line.label || line.name"/><t t-if="line.ref && line.ref.length"> : </t><t t-esc="line.ref"/></t><t t-else=""><t t-esc="line.ref && line.ref.length ? line.ref : line.label || line.name"/></t></td>
|
||||
<td class="cell_label">
|
||||
<t t-if="line.partner_id && line.partner_id !== state.st_line.partner_id">
|
||||
<t t-if="line.partner_name.length">
|
||||
<t t-esc="line.partner_name"/>:
|
||||
</t>
|
||||
</t>
|
||||
<t t-esc="line.label || line.name"/>
|
||||
<t t-if="line.ref && line.ref.length"> : </t>
|
||||
<t t-esc="line.ref"/>
|
||||
</td>
|
||||
<td class="cell_left"><t t-if="line.amount < 0"><span t-if="line.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money" t-att-data-content="line.amount_currency_str"/><t t-raw="line.amount_str"/></t></td>
|
||||
<td class="cell_right"><t t-if="line.amount > 0"><span t-if="line.amount_currency_str" t-attf-class="o_multi_currency o_multi_currency_color_#{line.currency_id%8} line_info_button fa fa-money" t-att-data-content="line.amount_currency_str"/><t t-raw="line.amount_str"/></t></td>
|
||||
<td class="cell_info_popover"></td>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
odoo.define('account.reconciliation_tests.data', function () {
|
||||
"use strict";
|
||||
|
||||
var data = {
|
||||
var Datas = {};
|
||||
|
||||
var db = {
|
||||
'res.partner': {
|
||||
fields: {
|
||||
id: {string: "ID", type: 'integer'},
|
||||
@@ -58,7 +60,7 @@ var data = {
|
||||
{id: 7, display_name: "Tax 10.00% include", amount: 10, price_include: true, account_id: 288},
|
||||
],
|
||||
json_friendly_compute_all: function (args) {
|
||||
var tax = _.find(data['account.tax'].records, {'id': args[0][0]});
|
||||
var tax = _.find(db['account.tax'].records, {'id': args[0][0]});
|
||||
var amount = args[1];
|
||||
var tax_base = tax.price_include ? amount*100/(100+tax.amount) : amount;
|
||||
return $.when({
|
||||
@@ -103,7 +105,7 @@ var data = {
|
||||
'account.bank.statement': {
|
||||
fields: {},
|
||||
reconciliation_widget_preprocess: function () {
|
||||
return $.when(data_preprocess);
|
||||
return $.when(Datas.used.data_preprocess);
|
||||
},
|
||||
},
|
||||
'account.bank.statement.line': {
|
||||
@@ -122,19 +124,19 @@ var data = {
|
||||
var partner_id = args.splice(1, 1)[0];
|
||||
var excluded_ids = args.splice(1, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!mv_lines[key]) {
|
||||
if (!Datas.used.mv_lines[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_reconciliation_widget: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(mv_lines[key], function (line) {
|
||||
return $.when(_.filter(Datas.used.mv_lines[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1 && (!partner_id || partner_id === line.partner_id);
|
||||
}));
|
||||
},
|
||||
get_data_for_reconciliation_widget: function (args) {
|
||||
var ids = args[0];
|
||||
return $.when(_.filter(data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
|
||||
return $.when(_.filter(Datas.used.data_widget, function (w) {return _.contains(ids, w.st_line.id);}));
|
||||
},
|
||||
reconciliation_widget_auto_reconcile: function () {
|
||||
return $.when(auto_reconciliation);
|
||||
return $.when(Datas.used.auto_reconciliation);
|
||||
},
|
||||
process_reconciliations: function (args) {
|
||||
var datas = args[1];
|
||||
@@ -142,8 +144,8 @@ var data = {
|
||||
ids = ids.concat(_.flatten(_.pluck(datas, 'payment_aml_ids')));
|
||||
ids = _.compact(ids);
|
||||
|
||||
for (var key in move_lines_for_manual_reconciliation) {
|
||||
move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
@@ -154,18 +156,18 @@ var data = {
|
||||
fields: {},
|
||||
get_data_for_manual_reconciliation_widget: function (args) {
|
||||
var key = JSON.stringify(args);
|
||||
if (!data_for_manual_reconciliation_widget[key]) {
|
||||
if (!Datas.used.data_for_manual_reconciliation_widget[key]) {
|
||||
throw new Error("Unknown parameters for get_data_for_manual_reconciliation_widget: '"+ key + "'");
|
||||
}
|
||||
return $.when(data_for_manual_reconciliation_widget[key]);
|
||||
return $.when(Datas.used.data_for_manual_reconciliation_widget[key]);
|
||||
},
|
||||
get_move_lines_for_manual_reconciliation: function (args) {
|
||||
var excluded_ids = args.splice(2, 1)[0];
|
||||
var key = JSON.stringify(args);
|
||||
if (!move_lines_for_manual_reconciliation[key]) {
|
||||
if (!Datas.used.move_lines_for_manual_reconciliation[key]) {
|
||||
throw new Error("Unknown parameters for get_move_lines_for_manual_reconciliation: '"+ key + "'");
|
||||
}
|
||||
return $.when(_.filter(move_lines_for_manual_reconciliation[key], function (line) {
|
||||
return $.when(_.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (line) {
|
||||
return excluded_ids.indexOf(line.id) === -1;
|
||||
}));
|
||||
},
|
||||
@@ -174,8 +176,8 @@ var data = {
|
||||
var datas = args[0];
|
||||
for (var i in datas) {
|
||||
var data = datas[i];
|
||||
for (var key in move_lines_for_manual_reconciliation) {
|
||||
move_lines_for_manual_reconciliation[key] = _.filter(move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
for (var key in Datas.used.move_lines_for_manual_reconciliation) {
|
||||
Datas.used.move_lines_for_manual_reconciliation[key] = _.filter(Datas.used.move_lines_for_manual_reconciliation[key], function (mv_line) {
|
||||
return data.mv_line_ids.indexOf(mv_line.id) === -1;
|
||||
});
|
||||
}
|
||||
@@ -367,7 +369,12 @@ var mv_lines = {
|
||||
'[7,"",0,6]': [
|
||||
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0012", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 133, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 4,610.00", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 106, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 4,610.00", 'debit': 4610.0, 'account_id': [287, "101200 Account Receivable"], 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false}
|
||||
{'account_type': "payable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-28", 'date': "2017-01-01", 'total_amount_str': "$ 10,000.00", 'partner_id': 12, 'account_name': "Account Payable", 'name': "BILL/2017/0001", 'partner_name': "Camptocamp", 'total_amount_currency_str': "", 'id': 114, 'credit': 10000.0, 'journal_id': [2, "Vendor Bills"], 'amount_str': "$ 10,000.00", 'debit': 0.0, 'account_id': [284, "101110 Stock Valuation Account"], 'account_code': "111100", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 376.00", 'partner_id': 7, 'account_name': "Bank", 'name': "BNK1/2017/0002: SUPP.OUT/2017/0002", 'partner_name': "ASUSTeK", 'total_amount_currency_str': "", 'id': 392, 'credit': 376.0, 'journal_id': "Bank", 'amount_str': "$ 376.00", 'debit': 0.0, 'account_code': "101401", 'ref': "BILL/2017/0003", 'already_paid': true},
|
||||
{'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 100.00", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0003: CUST.IN/2017/0001", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 394, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 100.00", 'debit': 100.0, 'account_code': "101401", 'ref': "", 'already_paid': true},
|
||||
{'account_type': "liquidity", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-01-23", 'date': "2017-01-23", 'total_amount_str': "$ 525.50", 'partner_id': 8, 'account_name': "Bank", 'name': "BNK1/2017/0004: CUST.IN/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 396, 'credit': 0.0, 'journal_id': "Bank", 'amount_str': "$ 525.50", 'debit': 525.5, 'account_code': "101401", 'ref': "INV/2017/0003", 'already_paid': true},
|
||||
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 650.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0002", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 109, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 650.00", 'debit': 650.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
{'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-22", 'date': "2017-01-23", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0004", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 399, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
|
||||
],
|
||||
'[8,"",0,6]': [],
|
||||
};
|
||||
@@ -468,23 +475,22 @@ var options = {
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
params: {
|
||||
data: data,
|
||||
data_preprocess: data_preprocess,
|
||||
data_widget: data_widget,
|
||||
mv_lines: mv_lines,
|
||||
auto_reconciliation: auto_reconciliation,
|
||||
data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
|
||||
move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
|
||||
session: session,
|
||||
options: options,
|
||||
},
|
||||
// this is the main function for this module. Its job is to export (and clone) all data for a test.
|
||||
getParams: function () {
|
||||
return $.extend(true, {}, this.params);
|
||||
}
|
||||
Datas.params = {
|
||||
data: db,
|
||||
data_preprocess: data_preprocess,
|
||||
data_widget: data_widget,
|
||||
mv_lines: mv_lines,
|
||||
auto_reconciliation: auto_reconciliation,
|
||||
data_for_manual_reconciliation_widget: data_for_manual_reconciliation_widget,
|
||||
move_lines_for_manual_reconciliation: move_lines_for_manual_reconciliation,
|
||||
session: session,
|
||||
options: options,
|
||||
};
|
||||
// this is the main function for this module. Its job is to export (and clone) all data for a test.
|
||||
Datas.getParams = function () {
|
||||
return (this.used = $.extend(true, {}, this.params));
|
||||
};
|
||||
return Datas;
|
||||
});
|
||||
|
||||
odoo.define('account.reconciliation_tests', function (require) {
|
||||
@@ -534,7 +540,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation fields', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
@@ -561,7 +566,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation basic data', function (assert) {
|
||||
assert.expect(14);
|
||||
|
||||
@@ -630,7 +634,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation partial', function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
@@ -701,7 +704,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation currencies', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
@@ -736,7 +738,7 @@ QUnit.module('account', {
|
||||
});
|
||||
|
||||
QUnit.test('Reconciliation change partner', function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(10);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
|
||||
@@ -754,7 +756,21 @@ QUnit.module('account', {
|
||||
$('.ui-autocomplete .ui-menu-item a:contains(Camptocamp)').trigger('mouseenter').trigger('click');
|
||||
clientAction._onAction({target: widget, name: 'change_partner', data: {data: {display_name: 'Camptocamp', id: 12}}, stopped: false});
|
||||
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display Camptocamp");
|
||||
assert.strictEqual(widget.$('.match table tr').length, 3, "camptocamp should have 3 propositions for reconciliation");
|
||||
assert.strictEqual(widget.$('.match table tr.mv_line').length, 3, "camptocamp should have 3 propositions for reconciliation");
|
||||
|
||||
widget = clientAction.widgets[2];
|
||||
widget.$('.accounting_view thead td:first').trigger('click');
|
||||
widget.$('.accounting_view .mv_line .cell_label').trigger('click');
|
||||
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Camptocamp", "the partner many2one should display agrolait");
|
||||
assert.strictEqual(widget.$('.match table tr').length, 3, "Camptocamp should have 3 propositions for reconciliation");
|
||||
assert.strictEqual(widget.$('.match .match_controls .fa:not(.disabled)').length, 0, "should not display the right arrow");
|
||||
|
||||
// Simulate remove partner
|
||||
widget.$('.o_input_dropdown input').trigger('click');
|
||||
widget.$('.o_input_dropdown input').val('').trigger('keyup').trigger('blur');
|
||||
assert.strictEqual(widget.$('.o_input_dropdown input').val(), "", "the partner many2one should be empty");
|
||||
assert.strictEqual(widget.$('.match table tr.mv_line').length, 5, "should have 5 propositions for reconciliation if partner is false");
|
||||
assert.strictEqual(widget.$('.match .match_controls .fa:not(.disabled)').length, 1, "should not display the right arrow");
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
@@ -839,7 +855,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation create line with taxes', function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
@@ -883,7 +898,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation create line from reconciliation model', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
@@ -900,7 +914,7 @@ QUnit.module('account', {
|
||||
widget.$('.create .quick_add button:contains(ATOS)').trigger('click');
|
||||
|
||||
assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '),
|
||||
"ATOS Banque1145.62Tax 20.00%229.12ATOS Frais26.71Tax 10.00% include2.67", "should display 4 lines");
|
||||
" ATOS Banque 1145.62 Tax 20.00% 229.12 ATOS Frais 26.71 Tax 10.00% include 2.67", "should display 4 lines");
|
||||
assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '),
|
||||
"Create Write-off229.12", "should display the 'Create Write-off' line with value in left column");
|
||||
|
||||
@@ -926,7 +940,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation auto reconciliation', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
@@ -958,7 +971,6 @@ QUnit.module('account', {
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation manual', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
@@ -992,5 +1004,229 @@ QUnit.module('account', {
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('Reconciliation pager', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
this.params.data['account.bank.statement.line'].records.push(
|
||||
{id: 9, display_name: "add 9"},
|
||||
{id: 10, display_name: "add 10"},
|
||||
{id: 11, display_name: "add 11"},
|
||||
{id: 12, display_name: "add 12"},
|
||||
{id: 13, display_name: "add 13"},
|
||||
{id: 14, display_name: "add 14"},
|
||||
{id: 15, display_name: "add 15"},
|
||||
{id: 16, display_name: "add 16"}
|
||||
);
|
||||
|
||||
var data_preprocess = this.params.data_preprocess;
|
||||
data_preprocess.st_lines_ids.push(9, 10, 11, 12, 13, 14, 15, 16);
|
||||
|
||||
this.params.data_widget.push(
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 9",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 9,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 10",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 10,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 11",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 11,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 12",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 12,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 13",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 13,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 14",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 14,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 15",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 15,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
},
|
||||
{
|
||||
'st_line': {
|
||||
'currency_id': 3,
|
||||
'communication_partner_name': false,
|
||||
'open_balance_account_id': 287,
|
||||
'name': "add 16",
|
||||
'partner_name': "Camptocamp",
|
||||
'partner_id': 12,
|
||||
'has_no_partner': false,
|
||||
'journal_id': 84,
|
||||
'account_name': "Bank",
|
||||
'note': "",
|
||||
'amount': 650.0,
|
||||
'amount_str': "$ 650.00",
|
||||
'amount_currency_str': "",
|
||||
'date': "2017-01-01",
|
||||
'account_code': "101401",
|
||||
'ref': "",
|
||||
'id': 16,
|
||||
'statement_id': 2
|
||||
},
|
||||
'reconciliation_proposition': []
|
||||
}
|
||||
);
|
||||
|
||||
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
|
||||
testUtils.addMockEnvironment(clientAction, {
|
||||
'data': this.params.data,
|
||||
});
|
||||
|
||||
clientAction.appendTo($('#qunit-fixture'));
|
||||
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should display 10 lines");
|
||||
assert.strictEqual(clientAction.$('.js_load_more:visible').length, 1, "should display the load more button");
|
||||
clientAction.$('.o_reconcile:visible').click();
|
||||
assert.strictEqual(clientAction.$('.o_reconcile:visible').length, 0, "should remove the reconciled line");
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 10, "should load one line to complete the 10");
|
||||
clientAction.$('.js_load_more').click();
|
||||
assert.strictEqual(clientAction.$('.o_reconciliation_line').length, 11, "should load the last record");
|
||||
assert.strictEqual(clientAction.$('.js_load_more:visible').length, 0, "should hide the load more button");
|
||||
|
||||
clientAction.destroy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
<button name="action_invoice_draft" states="cancel" string="Set to Draft" type="object" groups="account.group_account_invoice"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,open,paid" />
|
||||
</header>
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
|
||||
<div groups="account.group_account_invoice" class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
|
||||
You have <bold><a class="alert-link" href="#outstanding" role="button">outstanding debits</a></bold> for this supplier. You can allocate them to mark this bill as paid.
|
||||
</div>
|
||||
<field name="has_outstanding" invisible="1"/>
|
||||
@@ -363,7 +363,7 @@
|
||||
<button name="action_invoice_draft" states="cancel" string="Reset to Draft" type="object" groups="account.group_account_invoice"/>
|
||||
<field name="state" widget="statusbar" nolabel="1" statusbar_visible="draft,open,paid"/>
|
||||
</header>
|
||||
<div class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
|
||||
<div groups="account.group_account_invoice" class="alert alert-info" role="alert" style="margin-bottom:0px;" attrs="{'invisible': [('has_outstanding','=',False)]}">
|
||||
You have <bold><a class="alert-link" href="#outstanding" role="button">outstanding payments</a></bold> for this customer. You can allocate them to mark this invoice as paid.
|
||||
</div>
|
||||
<field name="has_outstanding" invisible="1"/>
|
||||
|
||||
@@ -74,7 +74,7 @@ class AccountInvoiceRefund(models.TransientModel):
|
||||
for tmpline in refund.move_id.line_ids:
|
||||
if tmpline.account_id.id == inv.account_id.id:
|
||||
to_reconcile_lines += tmpline
|
||||
to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile()
|
||||
to_reconcile_lines.filtered(lambda l: l.reconciled == False).reconcile()
|
||||
if mode == 'modify':
|
||||
invoice = inv.read(inv_obj._get_refund_modify_read_fields())
|
||||
invoice = invoice[0]
|
||||
|
||||
@@ -393,7 +393,7 @@ class AccountVoucherLine(models.Model):
|
||||
if product.description_purchase:
|
||||
values['name'] += '\n' + product.description_purchase
|
||||
else:
|
||||
values['price_unit'] = product.lst_price
|
||||
values['price_unit'] = price_unit or product.lst_price
|
||||
taxes = product.taxes_id or account.tax_ids
|
||||
if product.description_sale:
|
||||
values['name'] += '\n' + product.description_sale
|
||||
@@ -403,7 +403,7 @@ class AccountVoucherLine(models.Model):
|
||||
if company and currency:
|
||||
if company.currency_id != currency:
|
||||
if type == 'purchase':
|
||||
values['price_unit'] = product.standard_price
|
||||
values['price_unit'] = price_unit or product.standard_price
|
||||
values['price_unit'] = values['price_unit'] * currency.rate
|
||||
|
||||
return {'value': values, 'domain': {}}
|
||||
|
||||
@@ -658,7 +658,13 @@ class Import(models.TransientModel):
|
||||
}]
|
||||
|
||||
_logger.info('importing %d rows...', len(data))
|
||||
import_result = self.env[self.res_model].with_context(import_file=True).load(import_fields, data)
|
||||
|
||||
model = self.env[self.res_model].with_context(import_file=True)
|
||||
defer_parent_store = self.env.context.get('defer_parent_store_computation', True)
|
||||
if defer_parent_store and model._parent_store:
|
||||
model = model.with_context(defer_parent_store_computation=True)
|
||||
|
||||
import_result = model.load(import_fields, data)
|
||||
_logger.info('done')
|
||||
|
||||
# If transaction aborted, RELEASE SAVEPOINT is going to raise
|
||||
|
||||
@@ -503,8 +503,10 @@ class Challenge(models.Model):
|
||||
|
||||
line_data['own_goal_id'] = False,
|
||||
line_data['goals'] = []
|
||||
|
||||
goals = Goals.search(domain, order="completeness desc, current desc")
|
||||
if line.condition=='higher':
|
||||
goals = Goals.search(domain, order="completeness desc, current desc")
|
||||
else:
|
||||
goals = Goals.search(domain, order="completeness desc, current asc")
|
||||
for ranking, goal in enumerate(goals):
|
||||
if user and goal.user_id == user:
|
||||
line_data['own_goal_id'] = goal.id
|
||||
|
||||
@@ -9,6 +9,7 @@ import requests
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import api, fields, models, registry, _
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
@@ -125,6 +126,9 @@ class GoogleService(models.TransientModel):
|
||||
client_id = get_param('google_%s_client_id' % (service,), default=False)
|
||||
client_secret = get_param('google_%s_client_secret' % (service,), default=False)
|
||||
|
||||
if not client_id or not client_secret:
|
||||
raise UserError(_("The account for the Google service '%s' is not configured") % service)
|
||||
|
||||
headers = {"content-type": "application/x-www-form-urlencoded"}
|
||||
data = {
|
||||
'refresh_token': refresh_token,
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import print_function
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
import subprocess
|
||||
from threading import Lock
|
||||
|
||||
|
||||
@@ -23,7 +24,7 @@ BANNED_DEVICES = {
|
||||
}
|
||||
|
||||
|
||||
# drivers modules must add to drivers an object with a get_status() method
|
||||
# drivers modules must add to drivers an object with a get_status() method
|
||||
# so that 'status' can return the status of all active drivers
|
||||
drivers = {}
|
||||
|
||||
@@ -108,7 +109,7 @@ class Proxy(http.Controller):
|
||||
if not (device_id in BANNED_DEVICES):
|
||||
resp += "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
|
||||
count += 1
|
||||
|
||||
|
||||
if count == 0:
|
||||
resp += "<div class='device'>No USB Device Found</div>"
|
||||
|
||||
@@ -126,7 +127,7 @@ class Proxy(http.Controller):
|
||||
""" % subprocess.check_output('lsusb -v', shell=True)
|
||||
|
||||
return request.make_response(resp,{
|
||||
'Cache-Control': 'no-cache',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET',
|
||||
@@ -167,7 +168,7 @@ class Proxy(http.Controller):
|
||||
@http.route('/hw_proxy/payment_request', type='json', auth='none', cors='*')
|
||||
def payment_request(self, price):
|
||||
"""
|
||||
The PoS will activate the method payment
|
||||
The PoS will activate the method payment
|
||||
"""
|
||||
print("payment_request: price:%s" % price)
|
||||
return 'ok'
|
||||
@@ -175,7 +176,7 @@ class Proxy(http.Controller):
|
||||
@http.route('/hw_proxy/payment_status', type='json', auth='none', cors='*')
|
||||
def payment_status(self):
|
||||
print("payment_status")
|
||||
return { 'status':'waiting' }
|
||||
return { 'status':'waiting' }
|
||||
|
||||
@http.route('/hw_proxy/payment_cancel', type='json', auth='none', cors='*')
|
||||
def payment_cancel(self):
|
||||
|
||||
@@ -119,7 +119,7 @@ class MailController(http.Controller):
|
||||
follower_id = None
|
||||
follower_recs = request.env['mail.followers'].sudo().browse(follower_ids)
|
||||
res_ids = follower_recs.mapped('res_id')
|
||||
request.env[res_model].browse(res_ids).check_access_rule("write")
|
||||
request.env[res_model].browse(res_ids).check_access_rule("read")
|
||||
for follower in follower_recs:
|
||||
is_uid = partner_id == follower.partner_id
|
||||
follower_id = follower.id if is_uid else follower_id
|
||||
|
||||
@@ -23,7 +23,7 @@ class Followers(models.Model):
|
||||
index=True, required=True, ondelete='cascade',
|
||||
help='Model of the followed resource')
|
||||
res_model = fields.Char(
|
||||
'Related Document Model Name', index=True, readonly=True, related='res_model_id.model', store=True)
|
||||
'Related Document Model Name', index=True, readonly=True, related='res_model_id.model', compute_sudo=True, store=True)
|
||||
res_id = fields.Integer(
|
||||
'Related Document ID', index=True, help='Id of the followed resource')
|
||||
partner_id = fields.Many2one(
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<t t-name="mail.chat.SidebarItems">
|
||||
<div t-if="!disable_add_channel" class="o_mail_add_channel" t-attf-data-type="#{channel_type}">
|
||||
<span t-if="display_hash">#</span>
|
||||
<input type="text" t-attf-placeholder="#{input_placeholder}"/>
|
||||
<input type="text" class="o_input" t-attf-placeholder="#{input_placeholder}"/>
|
||||
</div>
|
||||
<t t-foreach="channels" t-as="channel">
|
||||
<t t-set="counter" t-value="channel.is_chat ? channel.unread_counter : channel.needaction_counter"/>
|
||||
@@ -134,6 +134,6 @@
|
||||
|
||||
<!-- Invite dialog's content -->
|
||||
<div t-name="mail.PartnerInviteDialog">
|
||||
<input type="text" class="o_mail_chat_partner_invite_input" id="mail_search_partners"/>
|
||||
<input type="text" class="o_input o_mail_chat_partner_invite_input" id="mail_search_partners"/>
|
||||
</div>
|
||||
</templates>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="o_composer_attachments_list"/>
|
||||
<t t-if="widget.extended">
|
||||
<div class="o_composer_subject">
|
||||
<input tabindex="1" type="text" placeholder="Subject"></input>
|
||||
<input tabindex="1" type="text" class="o_input" placeholder="Subject"></input>
|
||||
<div class="btn-group o_composer_buttons">
|
||||
<button tabindex="3" class="btn btn-sm btn-primary o_composer_button_send" type="button"><t t-esc="widget.options.send_text"/></button>
|
||||
<button tabindex="4" class="btn btn-sm btn-icon fa fa-smile-o o_composer_button_emoji" type="button" data-toggle="popover"/>
|
||||
@@ -15,7 +15,7 @@
|
||||
</t>
|
||||
<div t-attf-class="o_composer #{widget.extended ? 'o_extended_composer' : ''}">
|
||||
<div class="o_composer_input">
|
||||
<textarea class="o_composer_text_field" tabindex="2" placeholder="Write something..."/>
|
||||
<textarea class="o_input o_composer_text_field" tabindex="2" placeholder="Write something..."/>
|
||||
</div>
|
||||
<t t-if="!widget.extended">
|
||||
<div class="btn-group o_composer_buttons">
|
||||
|
||||
@@ -148,11 +148,11 @@
|
||||
<field name="name"/>
|
||||
</div>
|
||||
<div t-if="record.type.raw_value == 'binary'">
|
||||
<a t-attf-href="/web/content/#{record.id.value}?download=1">
|
||||
<a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
|
||||
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
|
||||
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
|
||||
<div t-if="webimage" class="o_attachment_image">
|
||||
<img t-attf-src="/web/image/#{record.id.value}?download=1"/>
|
||||
<img t-attf-src="/web/image/#{record.id.raw_value}?download=1"/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -295,8 +295,11 @@ class MailComposer(models.TransientModel):
|
||||
'mail_server_id': self.mail_server_id.id,
|
||||
'mail_activity_type_id': self.mail_activity_type_id.id,
|
||||
}
|
||||
|
||||
# mass mailing: rendering override wizard static values
|
||||
if mass_mail_mode and self.model:
|
||||
if self.model in self.env and hasattr(self.env[self.model], 'message_get_email_values'):
|
||||
mail_values.update(self.env[self.model].browse(res_id).message_get_email_values())
|
||||
# keep a copy unless specifically requested, reset record name (avoid browsing records)
|
||||
mail_values.update(notification=not self.auto_delete_message, model=self.model, res_id=res_id, record_name=False)
|
||||
# auto deletion of mail_mail
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
string="Mail Track Speakers"
|
||||
icon="fa-envelope-o"
|
||||
attrs="{'invisible': [('track_count', '=', 0)]}">
|
||||
Mail Track Speakers
|
||||
</button>
|
||||
</button>
|
||||
</field>
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
<div class="oe_attachment">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 o_kanban_primary_left" t-if="record.type.raw_value == 'binary'">
|
||||
<a t-attf-href="/web/content/#{record.id.value}?download=1">
|
||||
<a t-attf-href="/web/content/#{record.id.raw_value}?download=1">
|
||||
<t t-set="webimage" t-value="new RegExp('image.*(gif|jpeg|jpg|png)').test(record.mimetype.value)"/>
|
||||
<div t-if="!webimage" class="o_image" t-att-data-mimetype="record.mimetype.value"/>
|
||||
<div t-if="webimage" class="o_attachment_image">
|
||||
<img t-attf-src="/web/image/#{record.id.value}?download=1"/>
|
||||
<img t-attf-src="/web/image/#{record.id.raw_value}?download=1"/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -230,4 +230,37 @@ QUnit.module('pad widget', {
|
||||
delete FieldPad.prototype.isPadConfigured;
|
||||
});
|
||||
|
||||
QUnit.test('record should be discarded properly even if only pad has changed', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'task',
|
||||
data: this.data,
|
||||
arch:'<form>' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="description" widget="pad"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 2,
|
||||
mockRPC: function (route, args) {
|
||||
if (!args.method) {
|
||||
return $.when(true);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
session: {
|
||||
userName: "batman",
|
||||
},
|
||||
});
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
form.$buttons.find('.o_form_button_cancel').click();
|
||||
assert.strictEqual(form.$('.oe_pad_readonly').text(), this.data.task.pad_get_content(),
|
||||
"pad content should not have changed");
|
||||
form.destroy();
|
||||
delete FieldPad.prototype.isPadConfigured;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -17,8 +17,8 @@ STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
|
||||
|
||||
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
|
||||
INT_CURRENCIES = [
|
||||
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYGí', 'RWF', 'KRW', 'VUV',
|
||||
'VND', 'XOF'
|
||||
u'BIF', u'XAF', u'XPF', u'CLP', u'KMF', u'DJF', u'GNF', u'JPY', u'MGA', u'PYGí', u'RWF', u'KRW',
|
||||
u'VUV', u'VND', u'XOF'
|
||||
];
|
||||
|
||||
|
||||
@@ -154,10 +154,13 @@ class PaymentTransactionStripe(models.Model):
|
||||
def _stripe_form_get_tx_from_data(self, data):
|
||||
""" Given a data dict coming from stripe, verify it and find the related
|
||||
transaction record. """
|
||||
reference = data['metadata']['reference']
|
||||
reference = data.get('metadata', {}).get('reference')
|
||||
if not reference:
|
||||
error_msg = _('Stripe: invalid reply received from provider, missing reference')
|
||||
_logger.error(error_msg, data['metadata'])
|
||||
error_msg = _(
|
||||
'Stripe: invalid reply received from provider, missing reference. Additional message: %s'
|
||||
% data.get('error', {}).get('message', '')
|
||||
)
|
||||
_logger.error(error_msg)
|
||||
raise ValidationError(error_msg)
|
||||
tx = self.search([('reference', '=', reference)])
|
||||
if not tx:
|
||||
|
||||
@@ -2,6 +2,11 @@ odoo.define('payment_stripe.stripe', function(require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
|
||||
|
||||
// The following currencies are integer only, see
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
var int_currencies = [
|
||||
@@ -33,6 +38,10 @@ odoo.define('payment_stripe.stripe', function(require) {
|
||||
}).done(function(data){
|
||||
handler.isTokenGenerate = false;
|
||||
window.location.href = data;
|
||||
}).fail(function(){
|
||||
var msg = arguments && arguments[1] && arguments[1].data && arguments[1].data.message;
|
||||
var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')}));
|
||||
wizard.appendTo($('body')).modal({'keyboard': true});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="stripe.error">
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
|
||||
<h4 class="modal-title">Error</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<t t-esc="msg"></t>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="#" class="btn btn-link btn-xs" data-dismiss="modal">Close</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -110,7 +110,8 @@ odoo.define('point_of_sale.Tour', function (require) {
|
||||
trigger: ".header-button",
|
||||
}, {
|
||||
content: "confirm closing the frontend",
|
||||
trigger: ".header-button",
|
||||
trigger: ".header-button.confirm",
|
||||
run: function() {}, //it's a check,
|
||||
}]);
|
||||
|
||||
tour.register('pos_basic_order', { test: true, url: '/pos/web' }, steps);
|
||||
|
||||
@@ -191,7 +191,12 @@ models.Order = models.Order.extend({
|
||||
var product_id = line.get_product().id;
|
||||
|
||||
if (typeof resume[line_hash] === 'undefined') {
|
||||
resume[line_hash] = { qty: qty, note: note, product_id: product_id };
|
||||
resume[line_hash] = {
|
||||
qty: qty,
|
||||
note: note,
|
||||
product_id: product_id,
|
||||
product_name_wrapped: line.generate_wrapped_product_name(),
|
||||
};
|
||||
} else {
|
||||
resume[line_hash].qty += qty;
|
||||
}
|
||||
@@ -222,6 +227,7 @@ models.Order = models.Order.extend({
|
||||
add.push({
|
||||
'id': curr.product_id,
|
||||
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
|
||||
'name_wrapped': curr.product_name_wrapped,
|
||||
'note': curr.note,
|
||||
'qty': curr.qty,
|
||||
});
|
||||
@@ -229,6 +235,7 @@ models.Order = models.Order.extend({
|
||||
add.push({
|
||||
'id': curr.product_id,
|
||||
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
|
||||
'name_wrapped': curr.product_name_wrapped,
|
||||
'note': curr.note,
|
||||
'qty': curr.qty - old.qty,
|
||||
});
|
||||
@@ -236,6 +243,7 @@ models.Order = models.Order.extend({
|
||||
rem.push({
|
||||
'id': curr.product_id,
|
||||
'name': this.pos.db.get_product_by_id(curr.product_id).display_name,
|
||||
'name_wrapped': curr.product_name_wrapped,
|
||||
'note': curr.note,
|
||||
'qty': old.qty - curr.qty,
|
||||
});
|
||||
@@ -248,6 +256,7 @@ models.Order = models.Order.extend({
|
||||
rem.push({
|
||||
'id': old.product_id,
|
||||
'name': this.pos.db.get_product_by_id(old.product_id).display_name,
|
||||
'name_wrapped': old.product_name_wrapped,
|
||||
'note': old.note,
|
||||
'qty': old.qty,
|
||||
});
|
||||
|
||||
@@ -8,6 +8,15 @@
|
||||
</span>
|
||||
</t>
|
||||
|
||||
<t t-name="NameWrapped">
|
||||
<t t-foreach="change.name_wrapped.slice(1)" t-as="wrapped_line">
|
||||
<line>
|
||||
<left></left>
|
||||
<right><t t-esc="wrapped_line"/></right>
|
||||
</line>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="OrderChangeReceipt">
|
||||
<receipt
|
||||
align='center'
|
||||
@@ -33,8 +42,9 @@
|
||||
<t t-foreach="changes.cancelled" t-as="change">
|
||||
<line>
|
||||
<left><value><t t-esc="change.qty" /></value></left>
|
||||
<right><t t-esc="change.name" /></right>
|
||||
<right><t t-esc="change.name_wrapped[0]" /></right>
|
||||
</line>
|
||||
<t t-call="NameWrapped"/>
|
||||
<t t-if="change.note">
|
||||
<line>
|
||||
<left>NOTE</left>
|
||||
@@ -55,8 +65,9 @@
|
||||
<t t-foreach="changes.new" t-as="change">
|
||||
<line>
|
||||
<left><value><t t-esc="change.qty" /></value></left>
|
||||
<right><t t-esc="change.name" /></right>
|
||||
<right><t t-esc="change.name_wrapped[0]" /></right>
|
||||
</line>
|
||||
<t t-call="NameWrapped"/>
|
||||
<t t-if="change.note">
|
||||
<line>
|
||||
<left>NOTE</left>
|
||||
|
||||
@@ -423,9 +423,9 @@ class ProductTemplate(models.Model):
|
||||
variants_to_activate = self.env['product.product']
|
||||
variants_to_unlink = self.env['product.product']
|
||||
for product_id in tmpl_id.product_variant_ids:
|
||||
if not product_id.active and product_id.attribute_value_ids in variant_matrix:
|
||||
if not product_id.active and product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) in variant_matrix:
|
||||
variants_to_activate |= product_id
|
||||
elif product_id.attribute_value_ids not in variant_matrix:
|
||||
elif product_id.attribute_value_ids.filtered(lambda r: r.attribute_id.create_variant) not in variant_matrix:
|
||||
variants_to_unlink |= product_id
|
||||
if variants_to_activate:
|
||||
variants_to_activate.write({'active': True})
|
||||
|
||||
@@ -431,7 +431,7 @@ class Task(models.Model):
|
||||
@api.onchange('project_id')
|
||||
def _onchange_project(self):
|
||||
default_partner_id = self.env.context.get('default_partner_id')
|
||||
default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else None
|
||||
default_partner = self.env['res.partner'].browse(default_partner_id) if default_partner_id else self.env['res.partner']
|
||||
if self.project_id:
|
||||
self.partner_id = self.project_id.partner_id or default_partner
|
||||
self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<field name="use_tasks" position="after">
|
||||
<field name="use_issues"/>
|
||||
<field name="issue_ids" invisible="1"/>
|
||||
<field name="issue_count" invisible="1"/>
|
||||
<field name="issue_needaction_count" invisible="1"/>
|
||||
</field>
|
||||
<xpath expr="//div[contains(@class, 'o_kanban_manage_reports')]/div[last()]" position="after">
|
||||
@@ -35,7 +35,7 @@
|
||||
<xpath expr="//div[contains(@class, 'o_project_kanban_box')]/div[last()]" position="after">
|
||||
<div attrs="{'invisible': [('use_issues', '=', False)]}" class="o_project_kanban_box">
|
||||
<a name="%(act_project_project_2_project_issue_all)d" type="action">
|
||||
<span class="o_value"><t t-raw="record.issue_ids.raw_value.length"/></span>
|
||||
<span class="o_value"><t t-raw="record.issue_count.raw_value"/></span>
|
||||
<span class="o_label"><field name="label_issues"/></span>
|
||||
<a t-if="record.issue_needaction_count.raw_value" class="o_needaction" name="%(act_project_project_2_project_issue_all)d" type="action" context="{'search_default_message_needaction': 1}">
|
||||
<t t-esc="record.issue_needaction_count.raw_value"/>
|
||||
|
||||
@@ -248,9 +248,9 @@
|
||||
<div>
|
||||
<field name="customer_lead" class="oe_inline"/> days
|
||||
</div>
|
||||
<label for="analytic_tag_ids"/>
|
||||
<label for="analytic_tag_ids" groups="analytic.group_analytic_accounting"/>
|
||||
<div>
|
||||
<field name="analytic_tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_accounting" options="{'color_field': 'color'}"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -63,7 +63,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
self.order_line.mapped('procurement_ids').cancel()
|
||||
self.mapped('order_line').mapped('procurement_ids').cancel()
|
||||
return super(SaleOrder, self).action_cancel()
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -59,7 +59,7 @@ class SaleTimesheetController(http.Controller):
|
||||
values['dashboard'] = dashboard_values
|
||||
|
||||
# -- Time Repartition (per employee)
|
||||
repartition_domain = domain + [('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
repartition_domain = domain + [('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
repartition_data = request.env['account.analytic.line'].read_group(repartition_domain, ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
|
||||
|
||||
# set repartition per type per employee
|
||||
|
||||
@@ -27,9 +27,9 @@ class AccountAnalyticLine(models.Model):
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
# prevent to update invoiced timesheets
|
||||
if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
|
||||
if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
|
||||
raise UserError(_('You can not modify already invoiced timesheets.'))
|
||||
#if self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
|
||||
# if any([field_name in values for field_name in ['unit_amount', 'employee_id', 'task_id', 'timesheet_revenue', 'so_line', 'amount', 'date']]):
|
||||
# raise UserError(_('You can not modify already invoiced timesheets.'))
|
||||
|
||||
so_lines = self.mapped('so_line')
|
||||
if values.get('task_id'):
|
||||
@@ -97,26 +97,35 @@ class AccountAnalyticLine(models.Model):
|
||||
if self.env.context.get('create'): # avoid bad loop
|
||||
return result
|
||||
|
||||
# find the timesheet UoM
|
||||
timesheet_uom = self.product_uom_id
|
||||
if values.get('product_uom_id'):
|
||||
timesheet_uom = self.env['product.uom'].browse(values['product_uom_id'])
|
||||
if not timesheet_uom: # fallback on default company timesheet UoM
|
||||
timesheet_uom = self.env.user.company_id.project_time_mode_id
|
||||
|
||||
unit_amount = values.get('unit_amount', 0.0) or self.unit_amount
|
||||
billable_type = 'non_billable'
|
||||
revenue = 0.0
|
||||
|
||||
# set the revenue and billable type according to the product and the SO line
|
||||
so_line_id = values.get('so_line') or self.so_line.id
|
||||
so_line = self.env['sale.order.line'].browse(so_line_id) if so_line_id else self.env['sale.order.line'].browse()
|
||||
so_line = self.env['sale.order.line'].browse(so_line_id).sudo() if so_line_id else self.env['sale.order.line'].browse().sudo()
|
||||
if so_line.product_id.type == 'service':
|
||||
# find the analytic account to convert revenue into its currency
|
||||
account_id = values.get('account_id') or self.account_id.id
|
||||
analytic_account = self.env['account.analytic.account'].browse(account_id)
|
||||
# convert the unit of mesure into hours
|
||||
sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
|
||||
sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
|
||||
# calculate the revenue on the timesheet
|
||||
if so_line.product_id.invoice_policy == 'delivery':
|
||||
sale_price_unit = so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
|
||||
revenue = analytic_account.currency_id.round(unit_amount * sale_price_unit * (1-so_line.discount))
|
||||
revenue = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
|
||||
billable_type = 'billable_time'
|
||||
elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.track_service == 'task':
|
||||
quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
|
||||
# compute the total revenue the SO since we are in fixed price
|
||||
sale_price_unit = so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id)
|
||||
total_revenue_so = analytic_account.currency_id.round(so_line.product_uom_qty * sale_price_unit * (1-so_line.discount))
|
||||
total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
|
||||
# compute the total revenue already existing (without the current timesheet line)
|
||||
domain = [('so_line', '=', so_line.id)]
|
||||
if self.ids:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.addons.sale.tests.test_sale_common import TestSale
|
||||
from odoo.tools import float_compare
|
||||
from odoo.tools import float_compare, float_repr
|
||||
|
||||
|
||||
class TestSaleTimesheet(TestSale):
|
||||
@@ -281,10 +281,10 @@ class TestSaleTimesheet(TestSale):
|
||||
invoice.action_invoice_open()
|
||||
|
||||
# check concrete revenue
|
||||
self.assertEquals(float_compare(timesheet1.timesheet_revenue, 385.85, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_compare(timesheet2.timesheet_revenue, 154.35, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_compare(timesheet3.timesheet_revenue, 114.5, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_compare(timesheet4.timesheet_revenue, 152.68, precision_digits=2), 0, "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_repr(timesheet1.timesheet_revenue, precision_digits=2), '385.85', "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_repr(timesheet2.timesheet_revenue, precision_digits=2), '154.35', "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_repr(timesheet3.timesheet_revenue, precision_digits=2), '114.50', "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
self.assertEquals(float_repr(timesheet4.timesheet_revenue, precision_digits=2), '152.68', "Revenue computation on invoice validation does not return the correct revenue !")
|
||||
|
||||
# check the invoice is well set
|
||||
self.assertEquals(timesheet1.timesheet_invoice_id, invoice)
|
||||
|
||||
@@ -32,4 +32,18 @@
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_inherit_sale_timesheet" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.timesheet</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="hr_timesheet.view_task_form2_inherited"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='timesheet_ids']/tree" position="attributes">
|
||||
<attribute name="decoration-muted">timesheet_invoice_id != False</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='timesheet_ids']/tree" position="inside">
|
||||
<field name="timesheet_invoice_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -232,7 +232,7 @@ class CrmTeam(models.Model):
|
||||
|
||||
for data_item in graph_data:
|
||||
index = int((data_item.get('x_value') - start_date.isocalendar()[1]) % weeks_in_start_year)
|
||||
values[index][y_field] = int(data_item.get('y_value'))
|
||||
values[index][y_field] = data_item.get('y_value')
|
||||
|
||||
elif self.dashboard_graph_group == 'month':
|
||||
for month in range(0, (end_date.month - start_date.month) % 12 + 1):
|
||||
|
||||
@@ -201,6 +201,19 @@ class Inventory(models.Model):
|
||||
return True
|
||||
prepare_inventory = action_start
|
||||
|
||||
@api.multi
|
||||
def action_inventory_line_tree(self):
|
||||
action = self.env.ref('stock.action_inventory_line_tree').read()[0]
|
||||
action['context'] = {
|
||||
'default_location_id': self.location_id.id,
|
||||
'default_product_id': self.product_id.id,
|
||||
'default_prod_lot_id': self.lot_id.id,
|
||||
'default_package_id': self.package_id.id,
|
||||
'default_partner_id': self.partner_id.id,
|
||||
'default_inventory_id': self.id,
|
||||
}
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def _get_inventory_lines_values(self):
|
||||
# TDE CLEANME: is sql really necessary ? I don't think so
|
||||
@@ -334,6 +347,8 @@ class InventoryLine(models.Model):
|
||||
theoretical_qty = fields.Float(
|
||||
'Theoretical Quantity', compute='_compute_theoretical_qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'), readonly=True, store=True)
|
||||
inventory_location_id = fields.Many2one(
|
||||
'stock.location', 'Location', related='inventory_id.location_id', related_sudo=False)
|
||||
|
||||
@api.one
|
||||
@api.depends('location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id')
|
||||
|
||||
@@ -211,7 +211,8 @@ class StockMove(models.Model):
|
||||
@api.one
|
||||
@api.depends('product_id', 'product_uom', 'product_uom_qty')
|
||||
def _compute_product_qty(self):
|
||||
self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
|
||||
rounding_method = self._context.get('rounding_method', 'UP')
|
||||
self.product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id, rounding_method=rounding_method)
|
||||
|
||||
def _get_move_lines(self):
|
||||
""" This will return the move lines to consider when applying _quantity_done_compute on a stock.move.
|
||||
@@ -954,13 +955,13 @@ class StockMove(models.Model):
|
||||
# TDE CLEANME: remove context key + add as parameter
|
||||
if self.env.context.get('source_location_id'):
|
||||
defaults['location_id'] = self.env.context['source_location_id']
|
||||
new_move = self.copy(defaults)
|
||||
new_move = self.with_context(rounding_method='HALF-UP').copy(defaults)
|
||||
# ctx = context.copy()
|
||||
# TDE CLEANME: used only in write in this file, to clean
|
||||
# ctx['do_not_propagate'] = True
|
||||
|
||||
# FIXME: pim fix your crap
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True).write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||
self.with_context(do_not_propagate=True, do_not_unreserve=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
|
||||
|
||||
# if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
|
||||
# new_move_prop = self.move_dest_id.split(qty)
|
||||
|
||||
@@ -14,6 +14,40 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_line_tree2" model="ir.ui.view">
|
||||
<field name="name">stock.inventory.line.tree2</field>
|
||||
<field name="model">stock.inventory.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree editable="top" string="Inventory Details" decoration-info="product_qty != theoretical_qty" decoration-danger="theoretical_qty < 0">
|
||||
<field name="product_id" domain="[('type','=','product')]"/>
|
||||
<field name="product_uom_id" string="UoM" groups="product.group_uom"/>
|
||||
<field name="location_id" domain="[('id', 'child_of', inventory_location_id)]" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="prod_lot_id" domain="[('product_id', '=', product_id)]" context="{'default_product_id': product_id}" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" domain="['|', ('location_id','=', False), ('location_id', '=', location_id)]" groups="stock.group_tracking_lot"/>
|
||||
<field name="partner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="theoretical_qty" readonly="1"/>
|
||||
<field name="product_qty" string="Real Quantity"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="inventory_id" invisible="1"/>
|
||||
<field name="inventory_location_id" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_inventory_line_search" model="ir.ui.view">
|
||||
<field name="name">stock.inventory.line.search</field>
|
||||
<field name="model">stock.inventory.line</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search Inventory Lines">
|
||||
<field name="product_id"/>
|
||||
<field name="location_id" groups="stock.group_stock_multi_locations"/>
|
||||
<field name="prod_lot_id" groups="stock.group_production_lot"/>
|
||||
<field name="package_id" groups="stock.group_tracking_lot"/>
|
||||
<field name="partner_id" groups="stock.group_tracking_owner"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_inventory_filter" model="ir.ui.view">
|
||||
<field name="name">stock.inventory.filter</field>
|
||||
<field name="model">stock.inventory</field>
|
||||
@@ -87,6 +121,19 @@
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,confirm,done"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="action_inventory_line_tree"
|
||||
class="oe_stat_button"
|
||||
icon="fa-building-o"
|
||||
type="object"
|
||||
help="List view of lines"
|
||||
groups="base.group_no_one"
|
||||
states="confirm">
|
||||
<div class="o_form_field o_stat_info">
|
||||
<span class="o_stat_text">Details</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only"/>
|
||||
<h1><field name="name" placeholder="e.g. Annual inventory"/></h1>
|
||||
@@ -213,5 +260,16 @@
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_inventory_line_tree" model="ir.actions.act_window">
|
||||
<field name="name">Inventory Lines</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">stock.inventory.line</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="stock_inventory_line_tree2"/>
|
||||
<field name="domain">[('inventory_id', '=', active_id)]</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_inventory_form" id="menu_action_inventory_form" parent="menu_stock_inventory_control" sequence="30"/>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -5,8 +5,6 @@ import json
|
||||
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
import pytz
|
||||
import datetime
|
||||
|
||||
import odoo
|
||||
|
||||
@@ -26,7 +24,6 @@ class Http(models.AbstractModel):
|
||||
version_info = odoo.service.common.exp_version()
|
||||
return {
|
||||
"session_id": request.session.sid,
|
||||
"tzOffset": self._context.get('tz') and datetime.datetime.now(pytz.timezone(self._context['tz'])).utcoffset().total_seconds() / 60 or False,
|
||||
"uid": request.session.uid,
|
||||
"is_system": request.env.user._is_system(),
|
||||
"is_superuser": request.env.user._is_superuser(),
|
||||
|
||||
@@ -232,7 +232,7 @@ var IntegerField = NumberField.extend(/** @lends instance.web.search.IntegerFiel
|
||||
error_message: _t("not a valid integer"),
|
||||
parse: function (value) {
|
||||
try {
|
||||
return field_utils.parse.interger(value);
|
||||
return field_utils.parse.integer(value);
|
||||
} catch (e) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
|
||||
search: function(event) {
|
||||
var d = event.data;
|
||||
_.extend(this.env, this._process_search_data(d.domains, d.contexts, d.groupbys));
|
||||
this.active_view.controller.reload(this.env);
|
||||
this.active_view.controller.reload(_.extend({}, this.env));
|
||||
},
|
||||
switch_view: function(event) {
|
||||
if ('res_id' in event.data) {
|
||||
|
||||
@@ -285,10 +285,6 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
var result = _.extend({}, window.odoo.session_info);
|
||||
delete result.session_id;
|
||||
_.extend(this, result);
|
||||
if (this.tzOffset === false) {
|
||||
// send by server to have the user timezone and not the browser timezone
|
||||
this.tzOffset = -new Date().getTimezoneOffset();
|
||||
}
|
||||
return $.when();
|
||||
},
|
||||
check_session_id: function () {
|
||||
@@ -410,6 +406,17 @@ var Session = core.Class.extend(mixins.EventDispatcherMixin, {
|
||||
}) ? '' : this.prefix;
|
||||
return prefix + path + qs;
|
||||
},
|
||||
/**
|
||||
* Returns the time zone difference (in minutes) from the current locale
|
||||
* (host system settings) to UTC, for a given date. The offset is positive
|
||||
* if the local timezone is behind UTC, and negative if it is ahead.
|
||||
*
|
||||
* @param {string || moment} date a valid string date or moment instance
|
||||
* @returns {integer}
|
||||
*/
|
||||
getTZOffset: function (date) {
|
||||
return -new Date(date).getTimezoneOffset();
|
||||
},
|
||||
});
|
||||
|
||||
return Session;
|
||||
|
||||
@@ -341,6 +341,9 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
var def = this._super.apply(this, arguments);
|
||||
if (this.field.size && this.field.size > 0) {
|
||||
this.$el.attr('maxlength', this.field.size);
|
||||
}
|
||||
this.$el = this.$el.add(this._renderTranslateButton());
|
||||
return def;
|
||||
},
|
||||
@@ -450,7 +453,7 @@ var FieldDateTime = FieldDate.extend({
|
||||
*/
|
||||
_getValue: function () {
|
||||
var value = this.datewidget.getValue();
|
||||
return value && value.add(-this.getSession().tzOffset, 'minutes');
|
||||
return value && value.add(-this.getSession().getTZOffset(value), 'minutes');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -460,7 +463,7 @@ var FieldDateTime = FieldDate.extend({
|
||||
* @private
|
||||
*/
|
||||
_makeDatePicker: function () {
|
||||
var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
|
||||
var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
|
||||
return new datepicker.DateTimeWidget(this, {defaultDate: value});
|
||||
},
|
||||
|
||||
@@ -471,7 +474,7 @@ var FieldDateTime = FieldDate.extend({
|
||||
* @private
|
||||
*/
|
||||
_renderEdit: function () {
|
||||
var value = this.value && this.value.clone().add(this.getSession().tzOffset, 'minutes');
|
||||
var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
|
||||
this.datewidget.setValue(value);
|
||||
this.$input = this.datewidget.$input;
|
||||
},
|
||||
@@ -791,6 +794,7 @@ var FieldFloatTime = FieldFloat.extend({
|
||||
var FieldText = InputField.extend(TranslatableFieldMixin, {
|
||||
className: 'o_field_text',
|
||||
supportedFieldTypes: ['text'],
|
||||
tagName: 'span',
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
|
||||
@@ -95,8 +95,10 @@ function formatDate(value, field, options) {
|
||||
if (value === false) {
|
||||
return "";
|
||||
}
|
||||
if (!options || !('timezone' in options) || options.timezone) {
|
||||
value = value.clone().add(session.tzOffset < 0 ? -1 : 0, 'days');
|
||||
if (field && field.type === 'datetime') {
|
||||
if (!options || !('timezone' in options) || options.timezone) {
|
||||
value = value.clone().add(session.getTZOffset(value), 'minutes');
|
||||
}
|
||||
}
|
||||
var l10n = core._t.database.parameters;
|
||||
var date_format = time.strftime_to_moment_format(l10n.date_format);
|
||||
@@ -121,7 +123,7 @@ function formatDateTime(value, field, options) {
|
||||
return "";
|
||||
}
|
||||
if (!options || !('timezone' in options) || options.timezone) {
|
||||
value = value.clone().add(session.tzOffset, 'minutes');
|
||||
value = value.clone().add(session.getTZOffset(value), 'minutes');
|
||||
}
|
||||
var l10n = core._t.database.parameters;
|
||||
var date_format = time.strftime_to_moment_format(l10n.date_format);
|
||||
@@ -357,11 +359,8 @@ function parseDate(value, field, options) {
|
||||
date = moment.utc(value);
|
||||
} else {
|
||||
date = moment.utc(value, [datePattern, datePatternWoZero, moment.ISO_8601], true);
|
||||
if (options && options.timezone) {
|
||||
date.add(session.tzOffset > 0 ? -1 : 0, 'days');
|
||||
}
|
||||
}
|
||||
if (date.isValid() && date.year() >= 1900) {
|
||||
if (date.isValid()) {
|
||||
if (date.year() === 0) {
|
||||
date.year(moment.utc().year());
|
||||
}
|
||||
@@ -405,7 +404,7 @@ function parseDateTime(value, field, options) {
|
||||
} else {
|
||||
datetime = moment.utc(value, [pattern1, pattern2, moment.ISO_8601], true);
|
||||
if (options && options.timezone) {
|
||||
datetime.add(-session.tzOffset, 'minutes');
|
||||
datetime.add(-session.getTZOffset(datetime), 'minutes');
|
||||
}
|
||||
}
|
||||
if (datetime.isValid()) {
|
||||
|
||||
@@ -21,7 +21,6 @@ var dialogs = require('web.view_dialogs');
|
||||
var core = require('web.core');
|
||||
var data = require('web.data');
|
||||
var Dialog = require('web.Dialog');
|
||||
var field_utils = require('web.field_utils');
|
||||
var KanbanRenderer = require('web.KanbanRenderer');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var Pager = require('web.Pager');
|
||||
@@ -226,7 +225,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
var self = this;
|
||||
var def = $.Deferred();
|
||||
var slowCreate = function () {
|
||||
var dialog = self._searchCreatePopup.bind(self, "form", false, self._createContext(name));
|
||||
var dialog = self._searchCreatePopup("form", false, self._createContext(name));
|
||||
dialog.on('closed', self, def.resolve.bind(def));
|
||||
};
|
||||
if (this.nodeOptions.quick_create) {
|
||||
@@ -290,7 +289,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
_reset: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.floating = false;
|
||||
this.m2o_value = field_utils.format.many2one(this.value);
|
||||
this.m2o_value = this._formatValue(this.value);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
@@ -394,7 +393,7 @@ var FieldMany2One = AbstractField.extend({
|
||||
*/
|
||||
_searchCreatePopup: function (view, ids, context) {
|
||||
var self = this;
|
||||
new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
|
||||
return new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
|
||||
res_model: this.field.relation,
|
||||
domain: this.record.getDomain({fieldName: this.name}),
|
||||
context: _.extend({}, this.record.getContext(this.recordParams), context || {}),
|
||||
@@ -567,7 +566,7 @@ var KanbanFieldMany2One = AbstractField.extend({
|
||||
tagName: 'span',
|
||||
init: function () {
|
||||
this._super.apply(this, arguments);
|
||||
this.m2o_value = field_utils.format.many2one(this.value);
|
||||
this.m2o_value = this._formatValue(this.value);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -2023,7 +2022,8 @@ var FieldRadio = FieldSelection.extend({
|
||||
* `name_get` as data).
|
||||
*/
|
||||
var FieldReference = FieldMany2One.extend({
|
||||
supportedFieldTypes: ['reference'],
|
||||
specialData: "_fetchSpecialReference",
|
||||
supportedFieldTypes: ['char', 'reference'],
|
||||
template: 'FieldReference',
|
||||
events: _.extend({}, FieldMany2One.prototype.events, {
|
||||
'change select': '_onSelectionChange',
|
||||
@@ -2036,22 +2036,8 @@ var FieldReference = FieldMany2One.extend({
|
||||
|
||||
// needs to be copied as it is an unmutable object
|
||||
this.field = _.extend({}, this.field);
|
||||
if (this.value) {
|
||||
var model = null;
|
||||
// The value of a Reference field can either be a string in the form
|
||||
// of model,record_id or an object
|
||||
if (typeof this.value === "string") {
|
||||
var value = this.value.split(',');
|
||||
model = value[0];
|
||||
this.value = {
|
||||
res_id: parseInt(value[1]),
|
||||
};
|
||||
} else {
|
||||
model = this.value.model;
|
||||
}
|
||||
|
||||
this._setRelation(model);
|
||||
}
|
||||
this._setState();
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
@@ -2071,7 +2057,13 @@ var FieldReference = FieldMany2One.extend({
|
||||
* @override
|
||||
*/
|
||||
_formatValue: function () {
|
||||
return this.recordData.display_name;
|
||||
var value;
|
||||
if (this.field.type === 'char') {
|
||||
value = this.record.specialData[this.name];
|
||||
} else {
|
||||
value = this.value;
|
||||
}
|
||||
return value && value.data && value.data.display_name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -2097,8 +2089,10 @@ var FieldReference = FieldMany2One.extend({
|
||||
* @private
|
||||
*/
|
||||
_reset: function () {
|
||||
var value = this.$('select').val();
|
||||
this._setState();
|
||||
this._super.apply(this, arguments);
|
||||
this._setRelation(this.value && this.value.model);
|
||||
this.$('select').val(this.value && this.value.model || value);
|
||||
},
|
||||
/**
|
||||
* Set `relation` key in field properties.
|
||||
@@ -2110,6 +2104,19 @@ var FieldReference = FieldMany2One.extend({
|
||||
// used to generate the search in many2one
|
||||
this.field.relation = model;
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setState: function () {
|
||||
if (this.field.type === 'char') {
|
||||
// in this case, the value is stored in specialData instead
|
||||
this.value = this.record.specialData[this.name];
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
this._setRelation(this.value.model);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
* @private
|
||||
|
||||
@@ -54,7 +54,7 @@ var FieldTimezoneMismatch = FieldSelection.extend({
|
||||
}
|
||||
var offset = this.recordData.tz_offset.match(/([+-])([0-9]{2})([0-9]{2})/);
|
||||
offset = (offset[1] === '-' ? -1 : 1) * (parseInt(offset[2])*60 + parseInt(offset[3]));
|
||||
var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), {timezone: false});
|
||||
var datetime = field_utils.format.datetime(moment.utc().add(offset, 'minutes'), this.field, {timezone: false});
|
||||
var content = this.$option.html().split(' ')[0];
|
||||
content += ' ('+ datetime + ')';
|
||||
this.$option.html(content);
|
||||
|
||||
@@ -329,9 +329,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
|
||||
if (options && options.readonlyIfRealDiscard && !needDiscard) {
|
||||
return;
|
||||
}
|
||||
if (needDiscard) { // Just some optimization
|
||||
self.model.discardChanges(recordID);
|
||||
}
|
||||
self.model.discardChanges(recordID);
|
||||
if (self.model.isNew(recordID)) {
|
||||
self._abandonRecord(recordID);
|
||||
return;
|
||||
|
||||
@@ -185,6 +185,29 @@ var BasicModel = AbstractModel.extend({
|
||||
return id;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Onchange RPCs may return values for fields that are not in the current
|
||||
* view. Those fields might even be unknown when the onchange returns (e.g.
|
||||
* in x2manys, we only know the fields that are used in the inner view, but
|
||||
* not those used in the potential form view opened in a dialog when a sub-
|
||||
* record is clicked). When this happens, we can't infer their type, so the
|
||||
* given value can't be processed. It is instead stored in the '_rawChanges'
|
||||
* key of the record, without any processing. Later on, if this record is
|
||||
* displayed in another view (e.g. the user clicked on it in the x2many
|
||||
* list, and the record opens in a dialog), those changes that were left
|
||||
* behind must be applied. This function applies changes stored in
|
||||
* '_rawChanges' for a given viewType.
|
||||
*
|
||||
* @param {string} id local resource id of a record
|
||||
* @param {string} viewType the current viewType
|
||||
* @returns {Deferred -> string} resolves to the id of the record
|
||||
*/
|
||||
applyRawChanges: function (recordID, viewType) {
|
||||
var record = this.localData[recordID];
|
||||
return this._applyOnChange(record._rawChanges, record, viewType).then(function () {
|
||||
return record.id;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Delete a list of records, then, if the records have a parent, reload it.
|
||||
*
|
||||
@@ -754,14 +777,15 @@ var BasicModel = AbstractModel.extend({
|
||||
if (newValue instanceof Array) {
|
||||
rec._savePoint = newValue.slice(0);
|
||||
} else {
|
||||
// save the viewType of edition, so that the correct readonly modifiers
|
||||
// can be evaluated when the record will be saved
|
||||
for (var fieldName in (rec._changes || {})) {
|
||||
rec._editionViewType[fieldName] = options.viewType;
|
||||
}
|
||||
rec._savePoint = _.extend({}, newValue);
|
||||
}
|
||||
});
|
||||
|
||||
// save the viewType of edition, so that the correct readonly modifiers
|
||||
// can be evaluated when the record will be saved
|
||||
_.each((record._changes || {}), function (value, fieldName) {
|
||||
record._editionViewType[fieldName] = options.viewType;
|
||||
});
|
||||
}
|
||||
var shouldReload = 'reload' in options ? options.reload : true;
|
||||
var method = self.isNew(record_id) ? 'create' : 'write';
|
||||
@@ -896,18 +920,23 @@ var BasicModel = AbstractModel.extend({
|
||||
* @returns {Deferred -> string} resolves to the group id
|
||||
*/
|
||||
toggleGroup: function (groupId) {
|
||||
var self = this;
|
||||
var group = this.localData[groupId];
|
||||
if (group.isOpen) {
|
||||
group.isOpen = false;
|
||||
group.data = [];
|
||||
group.res_ids = [];
|
||||
group.offset = 0;
|
||||
this._updateParentResIDs(group);
|
||||
return $.when(groupId);
|
||||
}
|
||||
if (!group.isOpen) {
|
||||
group.isOpen = true;
|
||||
var def;
|
||||
if (group.count > 0) {
|
||||
def = this._load(group);
|
||||
def = this._load(group).then(function () {
|
||||
self._updateParentResIDs(group);
|
||||
});
|
||||
}
|
||||
return $.when(def).then(function () {
|
||||
return groupId;
|
||||
@@ -1093,21 +1122,33 @@ var BasicModel = AbstractModel.extend({
|
||||
* @param {Object} values the result of the onchange RPC (a mapping of
|
||||
* fieldnames to their value)
|
||||
* @param {Object} record
|
||||
* @param {string} [viewType] current viewType. If not set, we will assume
|
||||
* main viewType from the record
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_applyOnChange: function (values, record) {
|
||||
_applyOnChange: function (values, record, viewType) {
|
||||
var self = this;
|
||||
var defs = [];
|
||||
var rec;
|
||||
viewType = viewType || record.viewType;
|
||||
record._changes = record._changes || {};
|
||||
_.each(values, function (val, name) {
|
||||
var field = record.fields[name];
|
||||
if (!field) {
|
||||
return; // ignore changes of unknown fields
|
||||
// this field is unknown so we can't process it for now (it is not
|
||||
// in the current view anyway, otherwise it wouldn't be unknown.
|
||||
// we store its value without processing it, so that if we later
|
||||
// on switch to another view in which this field is displayed,
|
||||
// we could process it as we would know its type then.
|
||||
// use case: an onchange sends a create command for a one2many,
|
||||
// in the dict of values, there is a value for a field that is
|
||||
// not in the one2many list, but that is in the one2many form.
|
||||
record._rawChanges[name] = val;
|
||||
return;
|
||||
}
|
||||
|
||||
if (field.type === 'many2one' ) {
|
||||
var id = false;
|
||||
var id;
|
||||
if (field.type === 'many2one') {
|
||||
id = false;
|
||||
// in some case, the value returned by the onchange can
|
||||
// be false (no value), so we need to avoid creating a
|
||||
// local record for that.
|
||||
@@ -1126,13 +1167,27 @@ var BasicModel = AbstractModel.extend({
|
||||
id = rec.id;
|
||||
}
|
||||
record._changes[name] = id;
|
||||
} else if (field.type === 'reference') {
|
||||
id = false;
|
||||
if (val) {
|
||||
var ref = val.split(',');
|
||||
rec = self._makeDataPoint({
|
||||
context: record.context,
|
||||
data: {id: parseInt(ref[1])},
|
||||
modelName: ref[0],
|
||||
parentID: record.id,
|
||||
});
|
||||
defs.push(self._fetchNameGet(rec));
|
||||
id = rec.id;
|
||||
}
|
||||
record._changes[name] = id;
|
||||
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
var listId = record._changes[name] || record.data[name];
|
||||
var list;
|
||||
if (listId) {
|
||||
list = self.localData[listId];
|
||||
} else {
|
||||
var fieldInfo = record.fieldsInfo[record.viewType][name];
|
||||
var fieldInfo = record.fieldsInfo[viewType][name];
|
||||
if (!fieldInfo) {
|
||||
return; //ignore changes of x2many not in view
|
||||
}
|
||||
@@ -1142,7 +1197,7 @@ var BasicModel = AbstractModel.extend({
|
||||
parentID: record.id,
|
||||
static: true,
|
||||
type: 'list',
|
||||
viewtype: fieldInfo.viewType,
|
||||
viewType: fieldInfo.viewType,
|
||||
});
|
||||
}
|
||||
record._changes[name] = list.id;
|
||||
@@ -1535,15 +1590,17 @@ var BasicModel = AbstractModel.extend({
|
||||
var self = this;
|
||||
var model;
|
||||
var records = [];
|
||||
var ids = [];
|
||||
list = this._applyX2ManyOperations(list);
|
||||
var ids = _.map(list.data, function (localId) {
|
||||
_.each(list.data, function (localId) {
|
||||
var record = self.localData[localId];
|
||||
var data = record._changes || record.data;
|
||||
var many2oneId = data[fieldName];
|
||||
if (!many2oneId) { return; }
|
||||
var many2oneRecord = self.localData[many2oneId];
|
||||
records.push(many2oneRecord);
|
||||
ids.push(many2oneRecord.res_id);
|
||||
model = many2oneRecord.model;
|
||||
return many2oneRecord.res_id;
|
||||
});
|
||||
return this._rpc({
|
||||
model: model,
|
||||
@@ -1599,6 +1656,39 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Fetch the `name_get` for a reference field.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} record
|
||||
* @param {string} fieldName
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_fetchReference: function (record, fieldName) {
|
||||
var self = this;
|
||||
var def;
|
||||
var value = record._changes && record._changes[fieldName] || record.data[fieldName];
|
||||
var model = value && value.split(',')[0];
|
||||
var resID = value && parseInt(value.split(',')[1]);
|
||||
if (model && model !== 'False' && resID) {
|
||||
def = self._rpc({
|
||||
model: model,
|
||||
method: 'name_get',
|
||||
args: [resID],
|
||||
context: record.getContext({fieldName: fieldName}),
|
||||
}).then(function (result) {
|
||||
return self._makeDataPoint({
|
||||
data: {
|
||||
id: result[0][0],
|
||||
display_name: result[0][1],
|
||||
},
|
||||
modelName: model,
|
||||
parentID: record.id,
|
||||
});
|
||||
});
|
||||
}
|
||||
return $.when(def);
|
||||
},
|
||||
/**
|
||||
* Fetch the extra data (`name_get`) for the reference fields of the record
|
||||
* model.
|
||||
@@ -1614,28 +1704,12 @@ var BasicModel = AbstractModel.extend({
|
||||
_.each(fieldNames, function (fieldName) {
|
||||
var field = record.fields[fieldName];
|
||||
if (field.type === 'reference') {
|
||||
var value = record.data[fieldName];
|
||||
if (value) {
|
||||
var model = value.split(',')[0];
|
||||
var resID = parseInt(value.split(',')[1]);
|
||||
var def = self._rpc({
|
||||
model: model,
|
||||
method: 'name_get',
|
||||
args: [resID],
|
||||
context: record.getContext({fieldName: fieldName}),
|
||||
}).then(function (result) {
|
||||
var referenceDp = self._makeDataPoint({
|
||||
data: {
|
||||
id: result[0][0],
|
||||
display_name: result[0][1],
|
||||
},
|
||||
modelName: model,
|
||||
parentID: record.id,
|
||||
});
|
||||
record.data[fieldName] = referenceDp.id;
|
||||
});
|
||||
defs.push(def);
|
||||
}
|
||||
var def = self._fetchReference(record, fieldName).then(function (dataPoint) {
|
||||
if (dataPoint) {
|
||||
record.data[fieldName] = dataPoint.id;
|
||||
}
|
||||
});
|
||||
defs.push(def);
|
||||
}
|
||||
});
|
||||
return $.when.apply($, defs);
|
||||
@@ -1905,6 +1979,25 @@ var BasicModel = AbstractModel.extend({
|
||||
context: context
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Fetches the `name_get` associated to the reference widget if the field is
|
||||
* a `char` (which is a supported case).
|
||||
*
|
||||
* @private
|
||||
* @param {Object} record - an element from the localData
|
||||
* @param {Object} fieldName - the name of the field
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_fetchSpecialReference: function (record, fieldName) {
|
||||
var def;
|
||||
var field = record.fields[fieldName];
|
||||
if (field.type === 'char') {
|
||||
// if the widget reference is set on a char field, the name_get
|
||||
// needs to be fetched a posteriori
|
||||
def = this._fetchReference(record, fieldName);
|
||||
}
|
||||
return $.when(def);
|
||||
},
|
||||
/**
|
||||
* Fetches all the m2o records associated to the given fieldName. If the
|
||||
* given fieldName is not a m2o field, nothing is done. The difference with
|
||||
@@ -2712,6 +2805,7 @@ var BasicModel = AbstractModel.extend({
|
||||
_cache: type === 'list' ? {} : undefined,
|
||||
_changes: null,
|
||||
_domains: {},
|
||||
_rawChanges: {},
|
||||
aggregateValues: params.aggregateValues || {},
|
||||
context: params.context || {},
|
||||
count: params.count || res_ids.length,
|
||||
@@ -2826,14 +2920,25 @@ var BasicModel = AbstractModel.extend({
|
||||
var field = params.fields[name];
|
||||
data[name] = null;
|
||||
record._changes = record._changes || {};
|
||||
var dp;
|
||||
if (field.type === 'many2one' && result[name]) {
|
||||
var rec = self._makeDataPoint({
|
||||
dp = self._makeDataPoint({
|
||||
context: record.context,
|
||||
data: {id: result[name]},
|
||||
modelName: field.relation,
|
||||
parentID: record.id,
|
||||
});
|
||||
record._changes[name] = rec.id;
|
||||
record._changes[name] = dp.id;
|
||||
} else if (field.type === 'reference' && result[name]) {
|
||||
var ref = result[name].split(',');
|
||||
dp = self._makeDataPoint({
|
||||
context: record.context,
|
||||
data: {id: parseInt(ref[1])},
|
||||
modelName: ref[0],
|
||||
parentID: record.id,
|
||||
});
|
||||
defs.push(self._fetchNameGet(dp));
|
||||
record._changes[name] = dp.id;
|
||||
} else if (field.type === 'one2many' || field.type === 'many2many') {
|
||||
var fieldInfo = record.fieldsInfo[record.viewType][name];
|
||||
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
|
||||
@@ -2859,6 +2964,7 @@ var BasicModel = AbstractModel.extend({
|
||||
x2manyList._changes = [];
|
||||
var many2ones = {};
|
||||
var r;
|
||||
result[name] = result[name] || []; // handle false value
|
||||
var isCommandList = result[name].length && _.isArray(result[name][0]);
|
||||
if (!isCommandList) {
|
||||
result[name] = [[6, false, result[name]]];
|
||||
@@ -2885,7 +2991,7 @@ var BasicModel = AbstractModel.extend({
|
||||
|
||||
r._changes = _.defaults(value[2], r.data);
|
||||
for (var name in r._changes) {
|
||||
if (r._changes[name] === null) {
|
||||
if (!r._changes[name]) {
|
||||
continue;
|
||||
}
|
||||
var isFieldInView = name in r.fields;
|
||||
@@ -3318,6 +3424,7 @@ var BasicModel = AbstractModel.extend({
|
||||
list.data = data;
|
||||
list.res_ids = ids;
|
||||
}
|
||||
self._updateParentResIDs(list);
|
||||
return list;
|
||||
});
|
||||
},
|
||||
@@ -3363,6 +3470,26 @@ var BasicModel = AbstractModel.extend({
|
||||
});
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Updates the res_ids of the parent of a given element of type list.
|
||||
*
|
||||
* After some operations (e.g. loading more records, folding/unfolding a
|
||||
* group), the res_ids list of an element may be updated. When this happens,
|
||||
* the res_ids of its ancestors need to be updated as well. This is the
|
||||
* purpose of this function.
|
||||
*
|
||||
* @param {Object} element
|
||||
*/
|
||||
_updateParentResIDs: function (element) {
|
||||
var self = this;
|
||||
if (element.parentID) {
|
||||
var parent = this.localData[element.parentID];
|
||||
parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
|
||||
return self.localData[dataPointID].res_ids;
|
||||
}));
|
||||
this._updateParentResIDs(parent);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Helper method. Recursively traverses the data, starting from the element
|
||||
* record (or list), then following all relations. This is useful when one
|
||||
|
||||
@@ -63,6 +63,7 @@ var BasicView = AbstractView.extend({
|
||||
*/
|
||||
_loadData: function () {
|
||||
if (this.recordID) {
|
||||
var self = this;
|
||||
var record = this.model.get(this.recordID);
|
||||
var viewType = this.viewType;
|
||||
var viewFields = Object.keys(record.fieldsInfo[viewType]);
|
||||
@@ -107,15 +108,27 @@ var BasicView = AbstractView.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
if (fieldNames.length && !this.model.isNew(record.id)) {
|
||||
return this.model.reload(this.recordID, {
|
||||
fieldNames: fieldNames,
|
||||
keepChanges: true,
|
||||
viewType: viewType,
|
||||
|
||||
var def;
|
||||
if (fieldNames.length) {
|
||||
// Some fields in the new view weren't in the previous one, so
|
||||
// we might have stored changes for them (e.g. coming from
|
||||
// onchange RPCs), that we haven't been able to process earlier
|
||||
// (because those fields were unknow at that time). So we ask
|
||||
// the model to process them.
|
||||
def = this.model.applyRawChanges(record.id, viewType).then(function () {
|
||||
if (!self.model.isNew(record.id)) {
|
||||
return self.model.reload(record.id, {
|
||||
fieldNames: fieldNames,
|
||||
keepChanges: true,
|
||||
viewType: viewType,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return $.when(this.recordID);
|
||||
}
|
||||
return $.when(def).then(function () {
|
||||
return record.id;
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
@@ -38,39 +38,47 @@ return AbstractModel.extend({
|
||||
var start = event.start.clone();
|
||||
var end = event.end && event.end.clone();
|
||||
|
||||
if (!end || end.diff(start) < 0) {
|
||||
end = start.clone().add(1, 'h');
|
||||
// Detects allDay events (86400000 = 1 day in ms)
|
||||
if (event.allDay || (end && end.diff(start) % 86400000 === 0)) {
|
||||
event.allDay = true;
|
||||
}
|
||||
|
||||
if (event.allDay || end.diff(start) % 86400000 === 0) {
|
||||
event.allDay = true;
|
||||
// Set end date if not existing
|
||||
if (!end || end.diff(start) < 0) { // undefined or invalid end date
|
||||
if (event.allDay) {
|
||||
end = start.clone();
|
||||
} else {
|
||||
// in week mode or day mode, convert allday event to event
|
||||
end = start.clone().add(2, 'h');
|
||||
}
|
||||
} else if (event.allDay) {
|
||||
// For an "allDay", FullCalendar gives the end day as the
|
||||
// next day at midnight (instead of 23h59).
|
||||
end.add(-1, 'days');
|
||||
}
|
||||
|
||||
if (this.scale === 'month') {
|
||||
// An "allDay" event without the "all_day" option is not considered
|
||||
// as a 24h day. It's just a part of the day (by default: 7h-19h).
|
||||
if (event.allDay) {
|
||||
if (!this.mapping.all_day) {
|
||||
if (event.r_start) {
|
||||
start.hours(event.r_start.hours())
|
||||
.minutes(event.r_start.minutes())
|
||||
.seconds(event.r_start.seconds())
|
||||
.utc();
|
||||
end.add(-1, 'days')
|
||||
.hours(event.r_end.hours())
|
||||
end.hours(event.r_end.hours())
|
||||
.minutes(event.r_end.minutes())
|
||||
.seconds(event.r_end.seconds())
|
||||
.utc();
|
||||
} else {
|
||||
start.utc();
|
||||
end.utc();
|
||||
// default hours in the user's timezone
|
||||
start.hours(7).add(-this.getSession().getTZOffset(start), 'minutes');
|
||||
end.hours(19).add(-this.getSession().getTZOffset(end), 'minutes');
|
||||
}
|
||||
} else if (this.mapping.all_day) {
|
||||
start.startOf('day');
|
||||
end.startOf('day').add(-1, 'days');
|
||||
} else {
|
||||
// default hours in the user's timezone
|
||||
start.hours(7).add(-this.getSession().tzOffset, 'minutes');
|
||||
end.hours(19).add(-this.getSession().tzOffset, 'minutes');
|
||||
}
|
||||
} else {
|
||||
start.add(-this.getSession().tzOffset, 'minutes');
|
||||
end.add(-this.getSession().tzOffset, 'minutes');
|
||||
start.add(-this.getSession().getTZOffset(start), 'minutes');
|
||||
end.add(-this.getSession().getTZOffset(end), 'minutes');
|
||||
}
|
||||
|
||||
if (this.mapping.all_day) {
|
||||
@@ -365,6 +373,7 @@ return AbstractModel.extend({
|
||||
monthNamesShort: moment.monthsShort(),
|
||||
dayNames: moment.weekdays(),
|
||||
dayNamesShort: moment.weekdaysShort(),
|
||||
firstDay: moment().startOf('week').isoWeekday(),
|
||||
};
|
||||
},
|
||||
_getRangeDomain: function () {
|
||||
@@ -599,8 +608,8 @@ return AbstractModel.extend({
|
||||
date_stop = date_start.clone().add(date_delay,'hours');
|
||||
}
|
||||
|
||||
date_start.add(this.getSession().tzOffset, 'minutes');
|
||||
date_stop.add(this.getSession().tzOffset, 'minutes');
|
||||
date_start.add(this.getSession().getTZOffset(date_start), 'minutes');
|
||||
date_stop.add(this.getSession().getTZOffset(date_stop), 'minutes');
|
||||
|
||||
if (this.mapping.all_day && evt[this.mapping.all_day]) {
|
||||
date_stop.add(1, 'days');
|
||||
@@ -622,6 +631,10 @@ return AbstractModel.extend({
|
||||
// r.start = date_start.format('YYYY-MM-DD');
|
||||
// r.end = date_stop.format('YYYY-MM-DD');
|
||||
} else if (this.data.scale === 'month' && this.fields[this.mapping.date_start].type !== 'date') {
|
||||
// In month, FullCalendar gives the end day as the
|
||||
// next day at midnight (instead of 23h59).
|
||||
date_stop.add(1, 'days');
|
||||
|
||||
// allow to resize in month mode
|
||||
r.reset_allday = r.allDay;
|
||||
r.allDay = true;
|
||||
|
||||
@@ -331,12 +331,15 @@ return AbstractRenderer.extend({
|
||||
}
|
||||
element.find('.fc-content .fc-time').text(display_hour);
|
||||
},
|
||||
// Dirty hack to ensure a correct first render
|
||||
eventAfterAllRender: function (view) {
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
height: 'parent',
|
||||
unselectAuto: false,
|
||||
});
|
||||
|
||||
this.$calendar.fullCalendar(fc_options);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
},
|
||||
/**
|
||||
* Initialize the mini calendar in the sidebar
|
||||
|
||||
@@ -107,7 +107,10 @@ var KanbanController = BasicController.extend({
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_resequenceRecords: function (column_id, ids) {
|
||||
return this.model.resequence(this.modelName, ids, column_id);
|
||||
var self = this;
|
||||
return this.model.resequence(this.modelName, ids, column_id).then(function () {
|
||||
self._updateEnv();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* In grouped mode, set 'Create' button as btn-default if there is no column
|
||||
@@ -181,6 +184,7 @@ var KanbanController = BasicController.extend({
|
||||
.then(function (db_id) {
|
||||
var data = self.model.get(db_id);
|
||||
self.renderer.updateColumn(db_id, data);
|
||||
self._updateEnv();
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -218,6 +222,7 @@ var KanbanController = BasicController.extend({
|
||||
this.model.loadMore(column.db_id).then(function (db_id) {
|
||||
var data = self.model.get(db_id);
|
||||
self.renderer.updateColumn(db_id, data);
|
||||
self._updateEnv();
|
||||
});
|
||||
},
|
||||
/**
|
||||
@@ -325,9 +330,12 @@ var KanbanController = BasicController.extend({
|
||||
* @param {OdooEvent} event
|
||||
*/
|
||||
_onResequenceColumn: function (event) {
|
||||
var self = this;
|
||||
var state = this.model.get(this.handle, {raw: true});
|
||||
var model = state.fields[state.groupedBy[0]].relation;
|
||||
this.model.resequence(model, event.data.ids, this.handle);
|
||||
this.model.resequence(model, event.data.ids, this.handle).then(function () {
|
||||
self._updateEnv();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @param {OdooEvent} event
|
||||
@@ -338,6 +346,7 @@ var KanbanController = BasicController.extend({
|
||||
this.model.toggleGroup(column.db_id).then(function (db_id) {
|
||||
var data = self.model.get(db_id);
|
||||
self.renderer.updateColumn(db_id, data);
|
||||
self._updateEnv();
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -35,12 +35,8 @@ var KanbanModel = BasicModel.extend({
|
||||
group.count++;
|
||||
|
||||
// update the res_ids and count of the parent
|
||||
var self = this;
|
||||
var parent = this.localData[group.parentID];
|
||||
parent.res_ids = _.flatten(_.map(parent.data, function (dataPointID) {
|
||||
return self.localData[dataPointID].res_ids;
|
||||
}));
|
||||
parent.count++;
|
||||
this.localData[group.parentID].count++;
|
||||
this._updateParentResIDs(group);
|
||||
|
||||
return this._fetchRecord(new_record).then(function (result) {
|
||||
return result.id;
|
||||
@@ -151,6 +147,7 @@ var KanbanModel = BasicModel.extend({
|
||||
return this.notifyChanges(recordID, changes).then(function () {
|
||||
return self.save(recordID);
|
||||
}).then(function () {
|
||||
var resID = self.localData[recordID].res_id;
|
||||
// Remove record from its current group
|
||||
var old_group;
|
||||
for (var i = 0; i < parent.data.length; i++) {
|
||||
@@ -159,11 +156,14 @@ var KanbanModel = BasicModel.extend({
|
||||
if (index >= 0) {
|
||||
old_group.data.splice(index, 1);
|
||||
old_group.count--;
|
||||
old_group.res_ids = _.without(old_group.res_ids, resID);
|
||||
self._updateParentResIDs(old_group);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Add record to its new group
|
||||
new_group.data.push(recordID);
|
||||
new_group.res_ids.push(resID);
|
||||
new_group.count++;
|
||||
return [old_group.id, new_group.id];
|
||||
});
|
||||
@@ -201,6 +201,16 @@ var KanbanModel = BasicModel.extend({
|
||||
data.data = _.sortBy(data.data, function (d) {
|
||||
return _.indexOf(resIDs, self.localData[d].res_id);
|
||||
});
|
||||
data.res_ids = [];
|
||||
_.each(data.data, function (d) {
|
||||
var dataPoint = self.localData[d];
|
||||
if (dataPoint.type === 'record') {
|
||||
data.res_ids.push(dataPoint.res_id);
|
||||
} else {
|
||||
data.res_ids = data.res_ids.concat(dataPoint.res_ids);
|
||||
}
|
||||
});
|
||||
self._updateParentResIDs(data);
|
||||
return parentID;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -387,15 +387,15 @@ var ListRenderer = BasicRenderer.extend({
|
||||
var $th = $('<th>')
|
||||
.addClass('o_group_name')
|
||||
.text(name + ' (' + group.count + ')');
|
||||
if (group.count > 0) {
|
||||
var $arrow = $('<span>')
|
||||
var $arrow = $('<span>')
|
||||
.css('padding-left', (groupLevel * 20) + 'px')
|
||||
.css('padding-right', '5px')
|
||||
.addClass('fa')
|
||||
.toggleClass('fa-caret-right', !group.isOpen)
|
||||
.toggleClass('fa-caret-down', group.isOpen);
|
||||
$th.prepend($arrow);
|
||||
.addClass('fa');
|
||||
if (group.count > 0) {
|
||||
$arrow.toggleClass('fa-caret-right', !group.isOpen)
|
||||
.toggleClass('fa-caret-down', group.isOpen);
|
||||
}
|
||||
$th.prepend($arrow);
|
||||
if (group.isOpen && !group.groupedBy.length && (group.count > group.data.length)) {
|
||||
var $pager = this._renderGroupPager(group);
|
||||
var $lastCell = $cells[$cells.length-1];
|
||||
|
||||
@@ -343,6 +343,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
groupBy: searchResult.groupBy,
|
||||
modelName: self.dataset.model,
|
||||
hasSelectors: !self.options.disable_multiple_selection,
|
||||
readonly: true,
|
||||
}, self.options.list_view_options));
|
||||
listView.setController(SelectCreateListController);
|
||||
return listView.getController(self);
|
||||
@@ -402,6 +403,7 @@ var SelectCreateDialog = ViewDialog.extend({
|
||||
},
|
||||
})).open();
|
||||
dialog.on('closed', this, this.close.bind(this));
|
||||
return dialog;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ var DataExport = Dialog.extend({
|
||||
var self = this;
|
||||
this.exports.create({
|
||||
name: value,
|
||||
resource: this.dataset.model,
|
||||
resource: this.record.model,
|
||||
export_fields: _.map(fields, function (field) {
|
||||
return [0, 0, {name: field}];
|
||||
}),
|
||||
|
||||
@@ -348,7 +348,117 @@ DebugManager.include({
|
||||
});
|
||||
},
|
||||
set_defaults: function() {
|
||||
this._active_view.controller.open_defaults_dialog();
|
||||
var self = this;
|
||||
|
||||
var display = function (fieldInfo, value) {
|
||||
var displayed = value;
|
||||
if (value && fieldInfo.type === 'many2one') {
|
||||
displayed = value.data.display_name;
|
||||
value = value.data.id;
|
||||
} else if (value && fieldInfo.type === 'selection') {
|
||||
displayed = _.find(fieldInfo.selection, function (option) {
|
||||
return option[0] === value;
|
||||
})[1];
|
||||
}
|
||||
return [value, displayed];
|
||||
};
|
||||
|
||||
var renderer = this._active_view.controller.renderer;
|
||||
var fields = self._active_view.fields_view.fields;
|
||||
var fieldsInfo = self._active_view.fields_view.fieldsInfo.form;
|
||||
var fieldNamesInView = renderer.state.getFieldNames();
|
||||
var fieldsValues = renderer.state.data;
|
||||
var modifierDatas = {};
|
||||
_.each(fieldNamesInView, function (fieldName) {
|
||||
modifierDatas[fieldName] = _.find(renderer.allModifiersData, function (modifierdata) {
|
||||
return modifierdata.node.attrs.name === fieldName;
|
||||
});
|
||||
});
|
||||
this.fields = _.chain(fieldNamesInView)
|
||||
.map(function (fieldName) {
|
||||
var modifierData = modifierDatas[fieldName];
|
||||
var invisibleOrReadOnly;
|
||||
if (modifierData) {
|
||||
var evaluatedModifiers = modifierData.evaluatedModifiers[renderer.state.id];
|
||||
invisibleOrReadOnly = evaluatedModifiers.invisible || evaluatedModifiers.readonly;
|
||||
}
|
||||
var fieldInfo = fields[fieldName];
|
||||
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
|
||||
var value = valueDisplayed[0];
|
||||
var displayed = valueDisplayed[1];
|
||||
// ignore fields which are empty, invisible, readonly, o2m
|
||||
// or m2m
|
||||
if (!value || invisibleOrReadOnly || fieldInfo.type === 'one2many' ||
|
||||
fieldInfo.type === 'many2many' || fieldInfo.type === 'binary' ||
|
||||
fieldsInfo[fieldName].options.isPassword) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
name: fieldName,
|
||||
string: fieldInfo.string,
|
||||
value: value,
|
||||
displayed: displayed,
|
||||
};
|
||||
})
|
||||
.compact()
|
||||
.sortBy(function (field) { return field.string; })
|
||||
.value();
|
||||
|
||||
var conditions = _.chain(fieldNamesInView)
|
||||
.filter(function (fieldName) {
|
||||
var fieldInfo = fields[fieldName];
|
||||
return fieldInfo.change_default;
|
||||
})
|
||||
.map(function (fieldName) {
|
||||
var fieldInfo = fields[fieldName];
|
||||
var valueDisplayed = display(fieldInfo, fieldsValues[fieldName]);
|
||||
var value = valueDisplayed[0];
|
||||
var displayed = valueDisplayed[1];
|
||||
return {
|
||||
name: fieldName,
|
||||
string: fieldInfo.string,
|
||||
value: value,
|
||||
displayed: displayed,
|
||||
};
|
||||
})
|
||||
.value();
|
||||
var d = new Dialog(this, {
|
||||
title: _t("Set Default"),
|
||||
buttons: [
|
||||
{text: _t("Close"), close: true},
|
||||
{text: _t("Save default"), click: function () {
|
||||
var $defaults = d.$el.find('#formview_default_fields');
|
||||
var fieldToSet = $defaults.val();
|
||||
if (!fieldToSet) {
|
||||
$defaults.parent().addClass('o_form_invalid');
|
||||
return;
|
||||
}
|
||||
var allUsers = d.$el.find('#formview_default_all').is(':checked');
|
||||
var condition = d.$el.find('#formview_default_conditions').val();
|
||||
var value = _.find(self.fields, function (field) {
|
||||
return field.name === fieldToSet;
|
||||
}).value;
|
||||
self._rpc({
|
||||
model: 'ir.values',
|
||||
method: 'set_default',
|
||||
args: [
|
||||
self._active_view.fields_view.model,
|
||||
fieldToSet,
|
||||
value,
|
||||
allUsers,
|
||||
true,
|
||||
condition || false,
|
||||
],
|
||||
}).done(function () { d.close(); });
|
||||
}}
|
||||
]
|
||||
});
|
||||
d.args = {
|
||||
fields: this.fields,
|
||||
conditions: conditions,
|
||||
};
|
||||
d.template = 'FormView.set_default';
|
||||
d.open();
|
||||
},
|
||||
fvg: function() {
|
||||
var dialog = new Dialog(this, { title: _t("Fields View Get") }).open();
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
</label>
|
||||
</td>
|
||||
<td class="oe_form_required">
|
||||
<select id="formview_default_fields">
|
||||
<select id="formview_default_fields" class="o_input">
|
||||
<option value=""/>
|
||||
<option t-foreach="args.fields" t-as="field"
|
||||
t-att-value="field.name">
|
||||
@@ -356,7 +356,7 @@
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="formview_default_conditions">
|
||||
<select id="formview_default_conditions" class="o_input">
|
||||
<option value=""/>
|
||||
<option t-foreach="args.conditions" t-as="cond"
|
||||
t-att-value="cond.name + '=' + cond.value">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<span class="o_kanban_title">Add new Column</span>
|
||||
</div>
|
||||
<div class="o_kanban_quick_create">
|
||||
<input type="text" placeholder="Add"/>
|
||||
<input type="text" class="o_input" placeholder="Add"/>
|
||||
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
|
||||
<button class="btn btn-sm btn-default o_kanban_cancel">Discard</button>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
<t t-name="KanbanView.QuickCreate">
|
||||
<div class="o_kanban_quick_create">
|
||||
<input type="text" placeholder="Add"/>
|
||||
<input type="text" class="o_input" placeholder="Add"/>
|
||||
<button class="btn btn-sm btn-primary o_kanban_add">Add</button>
|
||||
<button class="btn btn-sm btn-default o_kanban_cancel">Discard</button>
|
||||
</div>
|
||||
|
||||
@@ -736,6 +736,31 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('char field with size attribute', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.fields.foo.size = 5; // max length
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<group><field name="foo"/></group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('input.o_field_widget').attr('maxlength'), '5',
|
||||
"maxlength attribute should have been set correctly on the input");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('char field in editable list view', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
@@ -1072,8 +1097,8 @@ QUnit.module('basic_fields', {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
assert.ok(form.$('div.o_field_text').length, "should have a text area");
|
||||
assert.strictEqual(form.$('div.o_field_text').text(), 'yop', 'should be "yop" in readonly');
|
||||
assert.ok(form.$('.o_field_text').length, "should have a text area");
|
||||
assert.strictEqual(form.$('.o_field_text').text(), 'yop', 'should be "yop" in readonly');
|
||||
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
|
||||
@@ -1089,7 +1114,7 @@ QUnit.module('basic_fields', {
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
assert.strictEqual(form.$('div.o_field_text').text(), 'hello world',
|
||||
assert.strictEqual(form.$('.o_field_text').text(), 'hello world',
|
||||
'should be "hello world" after save');
|
||||
form.destroy();
|
||||
});
|
||||
@@ -1109,7 +1134,7 @@ QUnit.module('basic_fields', {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
var $field = form.$('div.o_field_text');
|
||||
var $field = form.$('.o_field_text');
|
||||
|
||||
assert.strictEqual($field.outerHeight(), $field[0].scrollHeight,
|
||||
"text field should not have a scroll bar");
|
||||
@@ -1211,6 +1236,31 @@ QUnit.module('basic_fields', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
// Firefox-specific
|
||||
// Copying from <div style="white-space:pre-wrap"> does not keep line breaks
|
||||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1390115
|
||||
QUnit.test('copying text fields in RO mode should preserve line breaks', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="txt"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// Copying from a div tag with white-space:pre-wrap doesn't work in Firefox
|
||||
assert.strictEqual(form.$('[name="txt"]').prop("tagName").toLowerCase(), 'span',
|
||||
"the field contents should be surrounded by a span tag");
|
||||
});
|
||||
|
||||
QUnit.module('FieldBinary');
|
||||
|
||||
QUnit.test('binary fields are correctly rendered', function (assert) {
|
||||
@@ -1563,7 +1613,7 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('date field in form view', function (assert) {
|
||||
QUnit.test('date field in form view (with positive time zone offset)', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
var form = createView({
|
||||
@@ -1582,7 +1632,9 @@ QUnit.module('basic_fields', {
|
||||
date_format: '%m/%d/%Y',
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120; // Should be ignored by date fields
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1614,6 +1666,36 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('date field in form view (with negative time zone offset)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners"><field name="date"/></form>',
|
||||
res_id: 1,
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return -120; // Should be ignored by date fields
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_field_date').text(), '02/03/2017',
|
||||
'the date should be correctly displayed in readonly');
|
||||
|
||||
// switch to edit mode
|
||||
form.$buttons.find('.o_form_button_edit').click();
|
||||
assert.strictEqual(form.$('.o_datepicker_input').val(), '02/03/2017',
|
||||
'the date should be correct in edit mode');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('date field in editable list view', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
@@ -1628,7 +1710,9 @@ QUnit.module('basic_fields', {
|
||||
date_format: '%m/%d/%Y',
|
||||
},
|
||||
session: {
|
||||
tzOffset: 0
|
||||
getTZOffset: function () {
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1717,7 +1801,9 @@ QUnit.module('basic_fields', {
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1774,7 +1860,9 @@ QUnit.module('basic_fields', {
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1844,7 +1932,9 @@ QUnit.module('basic_fields', {
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1863,6 +1953,93 @@ QUnit.module('basic_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('datetime field with date/datetime widget (with day change)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].p = [2];
|
||||
this.data.partner.records[1].datetime = "2017-02-08 02:00:00"; // UTC
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="datetime"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="datetime" widget="date"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var expectedDateString = "02/07/2017 22:00:00"; // local time zone
|
||||
assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
|
||||
'the datetime (datetime widget) should be correctly displayed in tree view');
|
||||
|
||||
// switch to form view
|
||||
form.$('.o_field_widget[name=p] .o_data_row').click();
|
||||
assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/07/2017',
|
||||
'the datetime (date widget) should be correctly displayed in form view');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('datetime field with date/datetime widget (without day change)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
this.data.partner.records[0].p = [2];
|
||||
this.data.partner.records[1].datetime = "2017-02-08 10:00:00"; // without timezone
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:'<form string="Partners">' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="datetime"/>' +
|
||||
'</tree>' +
|
||||
'<form>' +
|
||||
'<field name="datetime" widget="date"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
translateParameters: { // Avoid issues due to localization formats
|
||||
date_format: '%m/%d/%Y',
|
||||
time_format: '%H:%M:%S',
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var expectedDateString = "02/08/2017 06:00:00"; // with timezone
|
||||
assert.strictEqual(form.$('.o_field_widget[name=p] .o_data_cell').text(), expectedDateString,
|
||||
'the datetime (datetime widget) should be correctly displayed in tree view');
|
||||
|
||||
// switch to form view
|
||||
form.$('.o_field_widget[name=p] .o_data_row').click();
|
||||
assert.strictEqual($('.modal .o_field_date[name=datetime]').text(), '02/08/2017',
|
||||
'the datetime (date widget) should be correctly displayed in form view');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.module('FieldMonetary');
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web.field_utils_tests', function (require) {
|
||||
var core = require('web.core');
|
||||
var session = require('web.session');
|
||||
var fieldUtils = require('web.field_utils');
|
||||
var testUtils = require('web.test_utils');
|
||||
|
||||
QUnit.module('fields', {}, function () {
|
||||
|
||||
@@ -59,10 +60,35 @@ QUnit.test("format_datetime", function (assert) {
|
||||
|
||||
var date_string = "2009-05-04 12:34:23";
|
||||
var date = fieldUtils.parse.datetime(date_string, {}, {timezone: false});
|
||||
var str = fieldUtils.format.datetime(date, {timezone: false});
|
||||
var str = fieldUtils.format.datetime(date, {}, {timezone: false});
|
||||
assert.strictEqual(str, moment(date).format("MM/DD/YYYY HH:mm:ss"));
|
||||
});
|
||||
|
||||
QUnit.test("format_datetime (with different timezone offset)", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// mock the date format to avoid issues due to localisation
|
||||
var dateFormat = core._t.database.parameters.date_format;
|
||||
core._t.database.parameters.date_format = '%m/%d/%Y';
|
||||
session.getTZOffset = function (date) {
|
||||
// simulate daylight saving time
|
||||
var startDate = new Date(2017, 2, 26);
|
||||
var endDate = new Date(2017, 9, 29);
|
||||
if (startDate < date && date < endDate) {
|
||||
return 120; // UTC+2
|
||||
} else {
|
||||
return 60; // UTC+1
|
||||
}
|
||||
};
|
||||
|
||||
var str = fieldUtils.format.datetime(moment.utc('2017-01-01T10:00:00Z'));
|
||||
assert.strictEqual(str, '01/01/2017 11:00:00');
|
||||
str = fieldUtils.format.datetime(moment.utc('2017-06-01T10:00:00Z'));
|
||||
assert.strictEqual(str, '06/01/2017 12:00:00');
|
||||
|
||||
core._t.database.parameters.date_format = dateFormat;
|
||||
});
|
||||
|
||||
QUnit.test("format_many2one", function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -958,6 +958,45 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: create with one2many with many2one (version 2)', function (assert) {
|
||||
// This test simulates the exact same scenario as the previous one,
|
||||
// except that the value for the many2one is explicitely set to false,
|
||||
// which is stupid, but this happens, so we have to handle it
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'default_get') {
|
||||
return $.when({p: [[0, 0, {display_name: 'new record', trululu: false}]]});
|
||||
} else if (args.method === 'name_get') {
|
||||
// This should not be called at all and thus is not accounted for
|
||||
// in the assert.expect. If this is called, you broke this test.
|
||||
assert.notOk(_.str.startsWith(args.args[0][0], 'virtual_'),
|
||||
"should not call name_get for the m2o inside o2m which has no value");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual($('td.o_data_cell:first').text(), 'new record',
|
||||
"should have created the new record in the o2m with the correct name");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list in form: default_get with x2many create', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
@@ -1250,6 +1289,38 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('slow create on a many2one', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<form>' +
|
||||
'<sheet>' +
|
||||
'<field name="product_id" options="{\'quick_create\': False}"/>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'product,false,form':
|
||||
'<form>' +
|
||||
'<field name="name"/>' +
|
||||
'</form>',
|
||||
},
|
||||
});
|
||||
|
||||
form.$('.o_field_many2one input').focus();
|
||||
form.$('.o_field_many2one input').val('new partner').trigger('keyup').trigger('focusout');
|
||||
|
||||
$('.modal .modal-footer .btn-primary').first().click();
|
||||
|
||||
assert.strictEqual($('.modal:visible:last .o_form_view').length, 1,
|
||||
'a new modal should be opened and contain a form view');
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('no_create option on a many2one', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
@@ -1732,7 +1803,9 @@ QUnit.module('relational_fields', {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.strictEqual(form.$('td:eq(0)').text(), "01/25/2017",
|
||||
@@ -5227,6 +5300,59 @@ QUnit.module('relational_fields', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2many: onchange that returns unknow field in list, but not in form', function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
name: function () {},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="name"/>' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'<form string="Partners">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="timmy" widget="many2many_tags"/>' +
|
||||
'</form>' +
|
||||
'</field>' +
|
||||
'</form>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'onchange') {
|
||||
return $.when({
|
||||
value: {
|
||||
p: [[5], [0, 0, {display_name: 'new', timmy: [[5], [4, 12]]}]],
|
||||
},
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('.o_data_row').length, 1,
|
||||
"the one2many should contain one row");
|
||||
assert.strictEqual(form.$('.o_field_widget[name="timmy"]').length, 0,
|
||||
"timmy should not be displayed in the list view");
|
||||
|
||||
form.$('.o_data_row td:first').click(); // open the record
|
||||
|
||||
assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"]').length, 1,
|
||||
"timmy should be displayed in the form view");
|
||||
assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .badge').length, 1,
|
||||
"m2mtags should contain one tag");
|
||||
assert.strictEqual($('.modal .o_field_many2manytags[name="timmy"] .o_badge_text').text(),
|
||||
'gold', "tag name should have been correctly loaded");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
|
||||
QUnit.module('FieldMany2Many');
|
||||
|
||||
QUnit.test('many2many kanban: edition', function (assert) {
|
||||
@@ -7214,7 +7340,7 @@ QUnit.module('relational_fields', {
|
||||
"should do a do_action with correct parameters");
|
||||
});
|
||||
|
||||
assert.strictEqual(form.$('a.o_form_uri:contains(first record)').length, 1,
|
||||
assert.strictEqual(form.$('a.o_form_uri:contains(xphone)').length, 1,
|
||||
"should contain a link");
|
||||
form.$('a.o_form_uri').click(); // click on the link in readonly mode (should trigger do_action)
|
||||
|
||||
@@ -7226,7 +7352,7 @@ QUnit.module('relational_fields', {
|
||||
"should contain one many2one");
|
||||
assert.strictEqual(form.$('.o_field_widget select').val(), "product",
|
||||
"widget should contain one select with the model");
|
||||
assert.strictEqual(form.$('.o_field_widget input').val(), "first record",
|
||||
assert.strictEqual(form.$('.o_field_widget input').val(), "xphone",
|
||||
"widget should contain one input with the record");
|
||||
|
||||
var options = _.map(form.$('.o_field_widget select > option'), function (el) {
|
||||
@@ -7251,12 +7377,114 @@ QUnit.module('relational_fields', {
|
||||
$dropdown.find('li:first()').click();
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
assert.strictEqual(form.$('a.o_form_uri').length, 1,
|
||||
assert.strictEqual(form.$('a.o_form_uri:contains(gold)').length, 1,
|
||||
"should contain a link with the new value");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('default_get and onchange with a reference field', function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
this.data.partner.fields.reference.default = 'product,37';
|
||||
this.data.partner.onchanges = {
|
||||
int_field: function (obj) {
|
||||
if (obj.int_field !== 0) {
|
||||
obj.reference = 'partner_type,' + obj.int_field;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="reference"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_get') {
|
||||
assert.step(args.model);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.verifySteps(['product'], "the first name_get should have been done");
|
||||
assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "product",
|
||||
"reference field model should be correctly set");
|
||||
assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "xphone",
|
||||
"reference field value should be correctly set");
|
||||
|
||||
// trigger onchange
|
||||
form.$('.o_field_widget[name=int_field]').val(12).trigger('input');
|
||||
|
||||
assert.verifySteps(['product', 'partner_type'], "the second name_get should have been done");
|
||||
assert.strictEqual(form.$('.o_field_widget[name="reference"] select').val(), "partner_type",
|
||||
"reference field model should be correctly set");
|
||||
assert.strictEqual(form.$('.o_field_widget[name="reference"] input').val(), "gold",
|
||||
"reference field value should be correctly set");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('widget reference on char field, reset by onchange', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.partner.records[0].foo = 'product,37';
|
||||
this.data.partner.onchanges = {
|
||||
int_field: function (obj) {
|
||||
obj.foo = 'product,' + obj.int_field;
|
||||
},
|
||||
};
|
||||
|
||||
var nbNameGet = 0;
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<group>' +
|
||||
'<field name="int_field"/>' +
|
||||
'<field name="foo" widget="reference" readonly="1"/>' +
|
||||
'</group>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
viewOptions: {
|
||||
mode: 'edit',
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.model === 'product' && args.method === 'name_get') {
|
||||
nbNameGet++;
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(nbNameGet, 1,
|
||||
"the first name_get should have been done");
|
||||
assert.strictEqual(form.$('a[name="foo"]').text(), "xphone",
|
||||
"foo field should be correctly set");
|
||||
|
||||
// trigger onchange
|
||||
form.$('.o_field_widget[name=int_field]').val(41).trigger('input');
|
||||
|
||||
assert.strictEqual(nbNameGet, 2,
|
||||
"the second name_get should have been done");
|
||||
assert.strictEqual(form.$('a[name="foo"]').text(), "xpad",
|
||||
"foo field should have been updated");
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -256,8 +256,11 @@ function addMockEnvironment(widget, params) {
|
||||
var initialDebounce = DebouncedField.prototype.DEBOUNCE;
|
||||
DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
|
||||
var initialSession, initialConfig, initialParameters;
|
||||
initialSession = _.extend({}, session);
|
||||
session.getTZOffset = function () {
|
||||
return 0; // by default, but may be overriden in specific tests
|
||||
};
|
||||
if ('session' in params) {
|
||||
initialSession = _.extend({}, session);
|
||||
_.extend(session, params.session);
|
||||
}
|
||||
if ('config' in params) {
|
||||
@@ -282,8 +285,8 @@ function addMockEnvironment(widget, params) {
|
||||
for (key in session) {
|
||||
delete session[key];
|
||||
}
|
||||
_.extend(session, initialSession);
|
||||
}
|
||||
_.extend(session, initialSession);
|
||||
if ('config' in params) {
|
||||
for (key in config) {
|
||||
delete config[key];
|
||||
|
||||
@@ -1323,6 +1323,64 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('default_get: fetch many2one with default (empty & not) inside x2manys', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.partner.fields.o2m = {
|
||||
string: "O2M", type: 'one2many', relation: 'partner', default: [
|
||||
[6, 0, []],
|
||||
[0, 0, {category: false}],
|
||||
[0, 0, {category: 12}],
|
||||
],
|
||||
};
|
||||
this.data.partner.fields.category.type = 'many2one';
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'name_get' && args.model === 'partner_type') {
|
||||
assert.deepEqual(args.args, [[12]], "should name_get on category 12");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
var params = {
|
||||
fieldNames: ['o2m'],
|
||||
fields: this.data.partner.fields,
|
||||
fieldsInfo: {
|
||||
form: {
|
||||
o2m: {
|
||||
relatedFields: this.data.partner.fields,
|
||||
fieldsInfo: {
|
||||
list: {
|
||||
category: {
|
||||
relatedFields: { display_name: {} },
|
||||
},
|
||||
},
|
||||
},
|
||||
viewType: 'list',
|
||||
},
|
||||
},
|
||||
},
|
||||
modelName: 'partner',
|
||||
type: 'record',
|
||||
viewType: 'form',
|
||||
};
|
||||
|
||||
model.load(params).then(function (resultID) {
|
||||
var record = model.get(resultID);
|
||||
assert.strictEqual(record.data.o2m.count, 2, "o2m field should contain 2 records");
|
||||
assert.strictEqual(record.data.o2m.data[0].data.category, false,
|
||||
"first category field should be empty");
|
||||
assert.strictEqual(record.data.o2m.data[1].data.category.data.display_name, "gold",
|
||||
"second category field should have been correctly fetched");
|
||||
});
|
||||
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('default_get: fetch x2manys inside x2manys', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
@@ -2007,5 +2065,25 @@ QUnit.module('Views', {
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('default_get with value false for a one2many', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.partner.fields.product_ids.default = false;
|
||||
this.params.fieldNames = ['product_ids'];
|
||||
this.params.res_id = undefined;
|
||||
this.params.type = 'record';
|
||||
|
||||
var model = createModel({
|
||||
Model: BasicModel,
|
||||
data: this.data,
|
||||
});
|
||||
|
||||
model.load(this.params).then(function (resultID) {
|
||||
var record = model.get(resultID);
|
||||
assert.deepEqual(record.data.product_ids.data, [], "o2m default should be []");
|
||||
});
|
||||
|
||||
model.destroy();
|
||||
});
|
||||
|
||||
});});
|
||||
|
||||
@@ -440,7 +440,9 @@ QUnit.module('Views', {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
@@ -507,7 +509,7 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('create event with timezone in week mode with formViewDialog', function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(8);
|
||||
|
||||
this.data.event.records = [];
|
||||
this.data.event.onchanges = {
|
||||
@@ -541,7 +543,9 @@ QUnit.module('Views', {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
@@ -554,12 +558,8 @@ QUnit.module('Views', {
|
||||
"should send the context to create events");
|
||||
}
|
||||
if (args.method === "write") {
|
||||
assert.deepEqual(args.args[1], {
|
||||
"allday": false,
|
||||
"start": "2016-12-12 06:00:00",
|
||||
"stop": "2016-12-12 08:00:00"
|
||||
},
|
||||
"should move the event");
|
||||
assert.deepEqual(args.args[1], expectedEvent,
|
||||
"should move the event");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
@@ -631,17 +631,34 @@ QUnit.module('Views', {
|
||||
left = pos.left + 5;
|
||||
top = pos.top + 5;
|
||||
|
||||
// Mode this event to another day
|
||||
var expectedEvent = {
|
||||
"allday": false,
|
||||
"start": "2016-12-12 06:00:00",
|
||||
"stop": "2016-12-12 08:00:00"
|
||||
};
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
|
||||
left = calendar.$('.fc-day:eq(1)').offset().left + 5;
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mousemove");
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
|
||||
|
||||
// Move to "All day"
|
||||
expectedEvent = {
|
||||
"allday": true,
|
||||
"start": "2016-12-12 00:00:00",
|
||||
"stop": "2016-12-12 00:00:00"
|
||||
};
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mousedown");
|
||||
top = calendar.$('.fc-day:eq(1)').offset().top + 5;
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mousemove");
|
||||
testUtils.triggerPositionalMouseEvent(left, top, "mouseup");
|
||||
|
||||
calendar.destroy();
|
||||
$view.remove();
|
||||
});
|
||||
|
||||
QUnit.test('create all day event', function (assert) {
|
||||
assert.expect(2);
|
||||
QUnit.test('create all day event in week mode', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.event.records = [];
|
||||
|
||||
@@ -664,7 +681,9 @@ QUnit.module('Views', {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -690,6 +709,8 @@ QUnit.module('Views', {
|
||||
|
||||
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "newevent",
|
||||
"should display the new event with time and title");
|
||||
assert.strictEqual($newevent.parent().attr('colspan'), "2",
|
||||
"should appear over two days.");
|
||||
|
||||
assert.deepEqual($newevent.data('fcSeg').event.record,
|
||||
{
|
||||
@@ -706,6 +727,141 @@ QUnit.module('Views', {
|
||||
$view.remove();
|
||||
});
|
||||
|
||||
QUnit.test('create all day event in week mode (no quickCreate)', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
this.data.event.records = [];
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<calendar class="o_calendar_test" '+
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'mode="week" '+
|
||||
'quick_add="False" '+
|
||||
'readonly_form_view_id="1">'+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
intercepts: {
|
||||
do_action: function (event) {
|
||||
assert.deepEqual(event.data.action.context, {
|
||||
"default_name": null,
|
||||
"default_start": "2016-12-14 05:00:00",
|
||||
"default_stop": "2016-12-15 17:00:00",
|
||||
},
|
||||
"should send the correct data to create events");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var $view = $('#qunit-fixture').contents();
|
||||
$view.prependTo('body'); // => select with click position
|
||||
|
||||
|
||||
var pos = calendar.$('.fc-bg td:eq(4)').offset();
|
||||
try {
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
|
||||
} catch (e) {
|
||||
calendar.destroy();
|
||||
$view.remove();
|
||||
throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
|
||||
}
|
||||
pos = calendar.$('.fc-bg td:eq(5)').offset();
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove");
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup");
|
||||
|
||||
calendar.destroy();
|
||||
$view.remove();
|
||||
});
|
||||
|
||||
QUnit.test('create event in month mode', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.event.records = [];
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<calendar class="o_calendar_test" '+
|
||||
'event_open_popup="true" '+
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'mode="month" '+
|
||||
'readonly_form_view_id="1">'+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === "create") {
|
||||
assert.deepEqual(args.args[0], {
|
||||
"name": "new event",
|
||||
"start": "2016-12-14 05:00:00",
|
||||
"stop": "2016-12-15 17:00:00",
|
||||
},
|
||||
"should send the correct data to create events");
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
});
|
||||
|
||||
var $view = $('#qunit-fixture').contents();
|
||||
$view.prependTo('body'); // => select with click position
|
||||
|
||||
var pos = calendar.$('.fc-bg td:eq(20)').offset();
|
||||
try {
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousedown");
|
||||
} catch (e) {
|
||||
calendar.destroy();
|
||||
$view.remove();
|
||||
throw new Error('The test fails to simulate a click in the screen. Your screen is probably too small or your dev tools is open.');
|
||||
}
|
||||
pos = calendar.$('.fc-bg td:eq(21)').offset();
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mousemove");
|
||||
testUtils.triggerPositionalMouseEvent(pos.left+15, pos.top+15, "mouseup");
|
||||
|
||||
$('.modal input:first').val('new event').trigger('input');
|
||||
$('.modal button.btn:contains(Create)').trigger('click');
|
||||
var $newevent = calendar.$('.fc-event:contains(new event)');
|
||||
|
||||
assert.strictEqual($newevent.text().replace(/[\s\n\r]+/g, ''), "newevent",
|
||||
"should display the new event with time and title");
|
||||
assert.strictEqual($newevent.parent().attr('colspan'), "2",
|
||||
"should appear over two days.");
|
||||
|
||||
assert.deepEqual($newevent.data('fcSeg').event.record, {
|
||||
display_name: "new event",
|
||||
start: fieldUtils.parse.datetime("2016-12-14 05:00:00", this.data.event.fields.start, {isUTC: true}),
|
||||
stop: fieldUtils.parse.datetime("2016-12-15 17:00:00", this.data.event.fields.stop, {isUTC: true}),
|
||||
name: "new event",
|
||||
id: 1
|
||||
}, "the new record should have the utc datetime (quickCreate)");
|
||||
|
||||
calendar.destroy();
|
||||
$view.remove();
|
||||
});
|
||||
|
||||
QUnit.test('use mini calendar', function (assert) {
|
||||
assert.expect(12);
|
||||
|
||||
@@ -728,7 +884,9 @@ QUnit.module('Views', {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -866,6 +1024,62 @@ QUnit.module('Views', {
|
||||
assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
|
||||
});
|
||||
|
||||
QUnit.test('create and edit event in month mode (all_day: false)', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch:
|
||||
'<calendar class="o_calendar_test" '+
|
||||
'string="Events" ' +
|
||||
'date_start="start" '+
|
||||
'date_stop="stop" '+
|
||||
'mode="month" '+
|
||||
'readonly_form_view_id="1">'+
|
||||
'<field name="name"/>'+
|
||||
'</calendar>',
|
||||
archs: archs,
|
||||
viewOptions: {
|
||||
initialDate: initialDate,
|
||||
},
|
||||
session: {
|
||||
getTZOffset: function () {
|
||||
return -240;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// create a new event and edit it
|
||||
var $cell = calendar.$('.fc-day-grid .fc-row:eq(4) .fc-day:eq(2)');
|
||||
testUtils.triggerMouseEvent($cell, "mousedown");
|
||||
testUtils.triggerMouseEvent($cell, "mouseup");
|
||||
$('.modal-body input:first').val('coucou').trigger('input');
|
||||
|
||||
testUtils.intercept(calendar, 'do_action', function (event) {
|
||||
assert.deepEqual(event.data.action,
|
||||
{
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "event",
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
context: {
|
||||
"default_name": "coucou",
|
||||
"default_start": "2016-12-27 11:00:00", // 7:00 + 4h
|
||||
"default_stop": "2016-12-27 23:00:00", // 19:00 + 4h
|
||||
}
|
||||
},
|
||||
"should open the form view with the context default values");
|
||||
});
|
||||
|
||||
$('.modal button.btn:contains(Edit)').trigger('click');
|
||||
|
||||
calendar.destroy();
|
||||
assert.strictEqual($('#ui-datepicker-div:empty').length, 0, "should have a clean body");
|
||||
});
|
||||
|
||||
|
||||
QUnit.test('readonly date_start field', function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
@@ -981,37 +1195,6 @@ QUnit.module('Views', {
|
||||
calendar.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('ensure calendar adapts to screen size', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
// We want to be able to restore changes on 'body'
|
||||
var bodyStyle = $('body').attr('style');
|
||||
|
||||
// Force the view height to be smaller than the natural calendar height
|
||||
// (FullCalendar requires that the height be modified on body not $target)
|
||||
var height = 500;
|
||||
$('body').height(height);
|
||||
|
||||
var calendar = createView({
|
||||
View: CalendarView,
|
||||
model: 'event',
|
||||
data: this.data,
|
||||
arch: '<calendar mode="week" date_start="start" />',
|
||||
});
|
||||
|
||||
var $scroller = calendar.$('.fc-scroller')
|
||||
assert.strictEqual($scroller.height() < height, true,
|
||||
"the scroller should be smaller than the view height");
|
||||
assert.strictEqual($scroller.prop('scrollHeight') > height, true,
|
||||
"the scroller should have a scrollbar");
|
||||
calendar.destroy();
|
||||
|
||||
$('body').removeAttr('style');
|
||||
if (bodyStyle) {
|
||||
$('body').attr('style', bodyStyle);
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test('ensure events are still shown if filters give an empty domain', function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
|
||||
@@ -2885,7 +2885,9 @@ QUnit.module('Views', {
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3779,6 +3781,60 @@ QUnit.module('Views', {
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('one2manys (list editable) inside one2manys are saved correctly', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var form = createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<sheet>' +
|
||||
'<field name="p">' +
|
||||
'<tree>' +
|
||||
'<field name="p"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</sheet>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
"partner,false,form": '<form>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="top">' +
|
||||
'<field name="display_name"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
'</form>'
|
||||
},
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
assert.deepEqual(args.args[0].p,
|
||||
[[0, false, {
|
||||
p: [[0, false, {display_name: "xtv"}]],
|
||||
}]],
|
||||
"create should be called with the correct arguments");
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// add a o2m subrecord
|
||||
form.$('.o_field_x2many_list_row_add a').click();
|
||||
$('.modal-body .o_field_one2many .o_field_x2many_list_row_add a').click();
|
||||
$('.modal-body input').val('xtv').trigger('input');
|
||||
$('.modal-footer button:first').click(); // save & close
|
||||
assert.strictEqual($('.modal').length, 0,
|
||||
"dialog should be closed");
|
||||
|
||||
var row = form.$('.o_field_one2many .o_list_view .o_data_row');
|
||||
assert.strictEqual(row.children()[0].textContent, '1 record',
|
||||
"the cell should contains the number of record: 1");
|
||||
|
||||
form.$buttons.find('.o_form_button_save').click();
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('*_view_ref in context are passed correctly', function (assert) {
|
||||
var done = assert.async();
|
||||
assert.expect(4);
|
||||
|
||||
@@ -136,11 +136,12 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('basic grouped rendering with active field', function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(6);
|
||||
|
||||
// add active field on partner model and make all records active
|
||||
this.data.partner.fields.active = {string: 'Active', type: 'char', default: true};
|
||||
|
||||
var envIDs = [1, 2, 3, 4]; // the ids that should be in the environment during this test
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -152,6 +153,12 @@ QUnit.module('Views', {
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates></kanban>',
|
||||
groupBy: ['bar'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// check available actions in kanban header's config dropdown
|
||||
@@ -163,6 +170,7 @@ QUnit.module('Views', {
|
||||
// archive the records of the first column
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 3,
|
||||
"last column should contain 3 records");
|
||||
envIDs = [4];
|
||||
kanban.$('.o_kanban_group:last .o_column_archive').click(); // click on 'Archive'
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:last .o_kanban_record').length, 0,
|
||||
"last column should contain no record");
|
||||
@@ -493,8 +501,9 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('can drag and drop a record from one column to the next', function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(9);
|
||||
|
||||
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
|
||||
this.data.partner.fields.sequence = {type: 'number', string: "Sequence"};
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
@@ -516,6 +525,12 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 2,
|
||||
"column should contain 2 record(s)");
|
||||
@@ -525,6 +540,7 @@ QUnit.module('Views', {
|
||||
assert.strictEqual(kanban.$('.thisiseditable').length, 4, "all records should be editable");
|
||||
var $record = kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record:first');
|
||||
var $group = kanban.$('.o_kanban_group:nth-child(2)');
|
||||
envIDs = [3, 2, 4, 1]; // first record of first column moved to the bottom of second column
|
||||
testUtils.dragAndDrop($record, $group);
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:nth-child(1) .o_kanban_record').length, 1,
|
||||
@@ -623,6 +639,42 @@ QUnit.module('Views', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('environment is updated when (un)folding groups', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<kanban>' +
|
||||
'<field name="product_id"/>' +
|
||||
'<templates><t t-name="kanban-box">' +
|
||||
'<div><field name="foo"/></div>' +
|
||||
'</t></templates>' +
|
||||
'</kanban>',
|
||||
groupBy: ['product_id'],
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(envIDs, event.data.ids,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// fold the second group and check that the res_ids it contains are no
|
||||
// longer in the environment
|
||||
envIDs = [1, 3];
|
||||
kanban.$('.o_kanban_group:last .o_kanban_toggle_fold').click();
|
||||
|
||||
// re-open the second group and check that the res_ids it contains are
|
||||
// back in the environment
|
||||
envIDs = [1, 3, 2, 4];
|
||||
kanban.$('.o_kanban_group:last .o_kanban_toggle_fold').click();
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('create a column in grouped on m2o', function (assert) {
|
||||
assert.expect(13);
|
||||
|
||||
@@ -1229,8 +1281,9 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('resequence columns in grouped by m2o', function (assert) {
|
||||
assert.expect(4);
|
||||
assert.expect(7);
|
||||
|
||||
var envIDs = [1, 3, 2, 4]; // the ids that should be in the environment during this test
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -1248,6 +1301,12 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(kanban.$('.o_kanban_view').hasClass('ui-sortable'),
|
||||
@@ -1260,6 +1319,7 @@ QUnit.module('Views', {
|
||||
// there is a 100ms delay on the d&d feature (jquery sortable) for
|
||||
// kanban columns, making it hard to test. So we rather bypass the d&d
|
||||
// for this test, and directly call the event handler
|
||||
envIDs = [2, 4, 1, 3]; // the columns will be inverted
|
||||
kanban._onResequenceColumn({data: {ids: [5, 3]}});
|
||||
kanban.update({}, {reload: false}); // re-render without reloading
|
||||
|
||||
@@ -1388,8 +1448,9 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('load more records in column', function (assert) {
|
||||
assert.expect(9);
|
||||
assert.expect(12);
|
||||
|
||||
var envIDs = [1, 2, 4]; // the ids that should be in the environment during this test
|
||||
var kanban = createView({
|
||||
View: KanbanView,
|
||||
model: 'partner',
|
||||
@@ -1409,12 +1470,19 @@ QUnit.module('Views', {
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts:{
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2,
|
||||
"there should be 2 records in the column");
|
||||
|
||||
// load more
|
||||
envIDs = [1, 2, 3, 4]; // id 3 will be loaded
|
||||
kanban.$('.o_kanban_group:eq(1)').find('.o_kanban_load_more').click();
|
||||
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 3,
|
||||
@@ -1423,6 +1491,8 @@ QUnit.module('Views', {
|
||||
assert.verifySteps([[2, undefined], [2, undefined], [2, 2]],
|
||||
"the records should be correctly fetched");
|
||||
|
||||
// reload
|
||||
envIDs = [1, 2, 4]; // first group is limited again to 2 records
|
||||
kanban.reload();
|
||||
assert.strictEqual(kanban.$('.o_kanban_group:eq(1) .o_kanban_record').length, 2,
|
||||
"there should be 2 records in the column after reload");
|
||||
|
||||
@@ -1271,7 +1271,9 @@ QUnit.module('Views', {
|
||||
data: this.data,
|
||||
arch: '<tree><field name="date"/><field name="datetime"/></tree>',
|
||||
session: {
|
||||
tzOffset: 120
|
||||
getTZOffset: function () {
|
||||
return 120;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1322,12 +1324,13 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
QUnit.test('list view with nested groups', function (assert) {
|
||||
assert.expect(37);
|
||||
assert.expect(42);
|
||||
|
||||
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1});
|
||||
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 2});
|
||||
|
||||
var nbRPCs = {readGroup: 0, searchRead: 0};
|
||||
var envIDs = []; // the ids that should be in the environment during this test
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
@@ -1352,10 +1355,14 @@ QUnit.module('Views', {
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
intercepts: {
|
||||
switch_view: function (event) {
|
||||
switch_view: function (event) {
|
||||
assert.strictEqual(event.data.res_id, 4,
|
||||
"'switch_view' event has been triggered");
|
||||
},
|
||||
env_updated: function (event) {
|
||||
assert.deepEqual(event.data.ids, envIDs,
|
||||
"should notify the environment with the correct ids");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1398,6 +1405,7 @@ QUnit.module('Views', {
|
||||
|
||||
// open subgroup
|
||||
nbRPCs = {readGroup: 0, searchRead: 0};
|
||||
envIDs = [4, 5]; // the opened subgroup contains these two records
|
||||
$openGroup.find('.o_group_header:nth(2)').click();
|
||||
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
|
||||
@@ -1414,6 +1422,7 @@ QUnit.module('Views', {
|
||||
|
||||
// sort by int_field (ASC) and check that open groups are still open
|
||||
nbRPCs = {readGroup: 0, searchRead: 0};
|
||||
envIDs = [5, 4]; // order of the records changed
|
||||
list.$('thead th:last').click();
|
||||
assert.strictEqual(nbRPCs.readGroup, 2, "should have done two read_groups");
|
||||
assert.strictEqual(nbRPCs.searchRead, 1, "should have done one search_read");
|
||||
@@ -1427,6 +1436,7 @@ QUnit.module('Views', {
|
||||
|
||||
// close first level group
|
||||
nbRPCs = {readGroup: 0, searchRead: 0};
|
||||
envIDs = []; // the group being closed, there is no more record in the environment
|
||||
list.$('.o_group_header:first').click();
|
||||
assert.strictEqual(nbRPCs.readGroup, 0, "should have done no read_group");
|
||||
assert.strictEqual(nbRPCs.searchRead, 0, "should have done no search_read");
|
||||
@@ -2519,6 +2529,55 @@ QUnit.module('Views', {
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('grouped list view, indentation for empty group', function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
this.data.foo.fields.priority = {
|
||||
string: "Priority",
|
||||
type: "selection",
|
||||
selection: [[1, "Low"], [2, "Medium"], [3, "High"]],
|
||||
default: 1,
|
||||
};
|
||||
this.data.foo.records.push({id: 5, foo: "blip", int_field: -7, m2o: 1, priority: 2});
|
||||
this.data.foo.records.push({id: 6, foo: "blip", int_field: 5, m2o: 1, priority: 3});
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="id"/></tree>',
|
||||
groupBy: ['priority', 'm2o'],
|
||||
mockRPC: function (route, args) {
|
||||
// Override of the read_group to display the row even if there is no record in it,
|
||||
// to mock the behavihour of some fields e.g stage_id on the sale order.
|
||||
if (args.method === 'read_group' && args.kwargs.groupby[0] === "m2o") {
|
||||
return $.when([
|
||||
{
|
||||
id: 8,
|
||||
m2o:[1,"Value 1"],
|
||||
m2o_count: 0
|
||||
}, {
|
||||
id: 2,
|
||||
m2o:[2,"Value 2"],
|
||||
m2o_count: 1
|
||||
}
|
||||
]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
// open the first group
|
||||
list.$('.o_group_header:first').click();
|
||||
assert.strictEqual(list.$('th.o_group_name').eq(1).children().length, 1,
|
||||
"There should be an empty element creating the indentation for the subgroup.");
|
||||
assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).hasClass('fa'), true,
|
||||
"The first element of the row name should have the fa class");
|
||||
assert.strictEqual(list.$('th.o_group_name').eq(1).children().eq(0).is('span'), true,
|
||||
"The first element of the row name should be a span");
|
||||
list.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -140,6 +140,35 @@ QUnit.module('Views', {
|
||||
dialog.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('SelectCreateDialog list view in readonly', function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
var parent = createParent({
|
||||
data: this.data,
|
||||
archs: {
|
||||
'partner,false,list':
|
||||
'<tree string="Partner" editable="bottom">' +
|
||||
'<field name="display_name"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
'partner,false,search':
|
||||
'<search/>'
|
||||
},
|
||||
});
|
||||
|
||||
var dialog = new dialogs.SelectCreateDialog(parent, {
|
||||
res_model: 'partner',
|
||||
}).open();
|
||||
|
||||
// click on the first row to see if the list is editable
|
||||
dialog.$('.o_list_view tbody tr:first td:not(.o_list_record_selector):first').click();
|
||||
|
||||
assert.equal(dialog.$('.o_list_view tbody tr:first td:not(.o_list_record_selector):first input').length, 0,
|
||||
"list view should not be editable in a SelectCreateDialog");
|
||||
|
||||
dialog.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ odoo.define('web.data_export_tests', function (require) {
|
||||
var framework = require('web.framework');
|
||||
var ListView = require('web.ListView');
|
||||
var testUtils = require('web.test_utils');
|
||||
var data = require('web.data');
|
||||
|
||||
var createView = testUtils.createView;
|
||||
|
||||
@@ -127,6 +128,89 @@ QUnit.module('widgets', {
|
||||
]);
|
||||
});
|
||||
|
||||
QUnit.test('saving fields list when exporting data', function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
var create = data.DataSet.prototype.create;
|
||||
|
||||
data.DataSet.prototype.create = function (data, options) {
|
||||
assert.step('create');
|
||||
return $.when([]);
|
||||
};
|
||||
|
||||
var list = createView({
|
||||
View: ListView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<tree><field name="foo"/></tree>',
|
||||
viewOptions: {
|
||||
sidebar: true,
|
||||
},
|
||||
mockRPC: function (route) {
|
||||
if (route === '/web/export/formats') {
|
||||
return $.when([
|
||||
{tag: 'csv', label: 'CSV'},
|
||||
{tag: 'xls', label: 'Excel'},
|
||||
]);
|
||||
}
|
||||
if (route === '/web/export/get_fields') {
|
||||
return $.when([
|
||||
{
|
||||
field_type: "one2many",
|
||||
string: "Activities",
|
||||
required: false,
|
||||
value: "activity_ids/id",
|
||||
id: "activity_ids",
|
||||
params: {"model": "mail.activity", "prefix": "activity_ids", "name": "Activities"},
|
||||
relation_field: "res_id",
|
||||
children: true,
|
||||
},
|
||||
]);
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
});
|
||||
|
||||
list.getParent().searchview = {
|
||||
build_search_data: function () {
|
||||
assert.step('build_search_data');
|
||||
return {
|
||||
contexts: [],
|
||||
domains: [],
|
||||
groupbys: [],
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Open the export modal
|
||||
list.$('thead th.o_list_record_selector input').click();
|
||||
list.sidebar.$('a:contains(Export)').click();
|
||||
assert.strictEqual($('.modal').length, 1,
|
||||
"a modal dialog should be open");
|
||||
|
||||
// Select 'Activities' in fields to export
|
||||
assert.strictEqual($('.modal select.o_fields_list option').length, 0,
|
||||
"the fields list should be empty");
|
||||
$('.modal .o_export_tree_item:contains(Activities)').click();
|
||||
$('.modal button:contains(Add)').click();
|
||||
assert.strictEqual($('.modal select.o_fields_list option').length, 1,
|
||||
"there should be one item in the fields list");
|
||||
|
||||
// Save fields list
|
||||
$('.modal a:contains(Save fields list)').click();
|
||||
$('.modal .o_save_list > input').val('fields list').trigger('input');
|
||||
$('.modal .o_save_list > button').click();
|
||||
assert.verifySteps(['build_search_data', 'create'],
|
||||
"create should have been called");
|
||||
|
||||
// Close the modal and destroy list
|
||||
$('.modal button span:contains(Close)').click();
|
||||
list.destroy();
|
||||
|
||||
// restore create function
|
||||
data.DataSet.prototype.create = create;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -204,7 +204,15 @@ var ViewEditor = Widget.extend({
|
||||
initType = "less";
|
||||
}
|
||||
if (!this.options.doNotLoadViews) {
|
||||
initResID = (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
|
||||
if (typeof this.viewKey === "number") {
|
||||
initResID = this.viewKey;
|
||||
} else {
|
||||
var view = _.findWhere(this.views, {xml_id: this.viewKey});
|
||||
if (!view) {
|
||||
view = _.findWhere(this.views, {key: this.viewKey});
|
||||
}
|
||||
initResID = view.id;
|
||||
}
|
||||
initType = "xml";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,9 +318,14 @@ class Website(Home):
|
||||
|
||||
def get_view_ids(self, xml_ids):
|
||||
ids = []
|
||||
View = request.env["ir.ui.view"].with_context(active_test=False)
|
||||
for xml_id in xml_ids:
|
||||
if "." in xml_id:
|
||||
record_id = request.env.ref(xml_id).id
|
||||
# Get website-specific view if possible
|
||||
record_id = View.search([
|
||||
("website_id", "=", request.website.id),
|
||||
("key", "=", xml_id),
|
||||
]).id or request.env.ref(xml_id).id
|
||||
else:
|
||||
record_id = int(xml_id)
|
||||
ids.append(record_id)
|
||||
|
||||
@@ -70,6 +70,11 @@ class View(models.Model):
|
||||
# assume it's already a view object (WTF?)
|
||||
return view_id
|
||||
|
||||
@api.model
|
||||
def _get_inheriting_views_arch_domain(self, view_id, model):
|
||||
domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model)
|
||||
return ['|', ('website_id', '=', False), ('website_id', '=', self.env.context.get('website_id'))] + domain
|
||||
|
||||
@api.model
|
||||
@tools.ormcache_context('self._uid', 'xml_id', keys=('website_id',))
|
||||
def get_view_id(self, xml_id):
|
||||
|
||||
@@ -136,7 +136,7 @@ class BlogPost(models.Model):
|
||||
default='{"background-image": "none", "background-color": "oe_black", "opacity": "0.2", "resize_class": ""}')
|
||||
blog_id = fields.Many2one('blog.blog', 'Blog', required=True, ondelete='cascade')
|
||||
tag_ids = fields.Many2many('blog.tag', string='Tags')
|
||||
content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize_attributes=False)
|
||||
content = fields.Html('Content', default=_default_content, translate=html_translate, sanitize=False)
|
||||
teaser = fields.Text('Teaser', compute='_compute_teaser', inverse='_set_teaser')
|
||||
teaser_manual = fields.Text(string='Teaser Content')
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
|
||||
<p>Thanks,</p>
|
||||
|
||||
${ctx['partner_id'].user_id and ctx['partner_id'].user_id.signature or '' | safe}
|
||||
${ctx['partner_id'].user_id and ctx['partner_id'].user_id.signature | safe or ''}
|
||||
|
||||
% if not ctx['partner_id'].user_id:
|
||||
PS: It looks like you do not have an account manager assigned to you, please contact us.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</group>
|
||||
<field name="assignation_lines" attrs="{'invisible': [('forward_type', 'in', ['single',False])]}">
|
||||
<tree create="false" editable="bottom">
|
||||
<field name="lead_id" readonly="1"/>
|
||||
<field name="lead_id" readonly="1" force_save="1" />
|
||||
<field name="lead_location" readonly="1"/>
|
||||
<field name="partner_assigned_id"/>
|
||||
<field name="partner_location" readonly="1"/>
|
||||
|
||||
@@ -49,9 +49,9 @@ class WebsiteHrRecruitment(http.Controller):
|
||||
countries = set(o.country_id for o in offices if o.country_id)
|
||||
|
||||
if department:
|
||||
jobs = (j for j in jobs if j.department_id and j.department_id.id == department.id)
|
||||
jobs = [j for j in jobs if j.department_id and j.department_id.id == department.id]
|
||||
if office_id and office_id in [x.id for x in offices]:
|
||||
jobs = (j for j in jobs if j.address_id and j.address_id.id == office_id)
|
||||
jobs = [j for j in jobs if j.address_id and j.address_id.id == office_id]
|
||||
else:
|
||||
office_id = False
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<record id="hr_department_public" model="ir.rule">
|
||||
<field name="name">Job department: Public</field>
|
||||
<field name="model_id" ref="hr.model_hr_department"/>
|
||||
<field name="domain_force">[('jobs_ids.website_published', '=', True)]</field>
|
||||
<field name="domain_force">['|', ('jobs_ids.website_published', '=', True), ('child_ids', 'not in', [])]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
|
||||
@@ -70,11 +70,13 @@ class website_account(http.Controller):
|
||||
|
||||
@http.route(['/my/account'], type='http', auth='user', website=True)
|
||||
def details(self, redirect=None, **post):
|
||||
values = self._prepare_portal_layout_values()
|
||||
partner = request.env.user.partner_id
|
||||
values = {
|
||||
values.update({
|
||||
'error': {},
|
||||
'error_message': []
|
||||
}
|
||||
'error_message': [],
|
||||
'hide_current_user': True,
|
||||
})
|
||||
|
||||
if post:
|
||||
error, error_message = self.details_form_validate(post)
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
<div class="o_my_contact" t-if="sales_rep">
|
||||
<t t-call="website_portal.contact"/>
|
||||
</div>
|
||||
<div class="o_my_details">
|
||||
<div class="o_my_details" t-if="not hide_current_user and user">
|
||||
<h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3>
|
||||
<div t-field="user.partner_id" t-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<field name="user_ids">
|
||||
<tree string="Contacts" editable="bottom" create="false" delete="false">
|
||||
<field name="partner_id"/>
|
||||
<field name="partner_id" force_save="1"/>
|
||||
<field name="email"/>
|
||||
<field name="in_portal"/>
|
||||
</tree>
|
||||
|
||||
@@ -497,7 +497,7 @@ class WebsiteSale(http.Controller):
|
||||
def _checkout_form_save(self, mode, checkout, all_values):
|
||||
Partner = request.env['res.partner']
|
||||
if mode[0] == 'new':
|
||||
partner_id = Partner.sudo().create(checkout)
|
||||
partner_id = Partner.sudo().create(checkout).id
|
||||
elif mode[0] == 'edit':
|
||||
partner_id = int(all_values.get('partner_id', 0))
|
||||
if partner_id:
|
||||
|
||||
@@ -187,7 +187,9 @@ class Product(models.Model):
|
||||
def _website_price(self):
|
||||
qty = self._context.get('quantity', 1.0)
|
||||
partner = self.env.user.partner_id
|
||||
pricelist = self.env['website'].get_current_website().get_current_pricelist()
|
||||
current_website = self.env['website'].get_current_website()
|
||||
pricelist = current_website.get_current_pricelist()
|
||||
company_id = current_website.company_id
|
||||
|
||||
context = dict(self._context, pricelist=pricelist.id, partner=partner)
|
||||
self2 = self.with_context(context) if self._context != context else self
|
||||
@@ -195,7 +197,7 @@ class Product(models.Model):
|
||||
ret = self.env.user.has_group('sale.group_show_price_subtotal') and 'total_excluded' or 'total_included'
|
||||
|
||||
for p, p2 in pycompat.izip(self, self2):
|
||||
taxes = partner.property_account_position_id.map_tax(p.taxes_id)
|
||||
taxes = partner.property_account_position_id.map_tax(p.taxes_id.filtered(lambda x: x.company_id == company_id))
|
||||
p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret]
|
||||
p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
|
||||
|
||||
|
||||
@@ -100,6 +100,6 @@ class WebsiteSaleDigital(website_account):
|
||||
return request.not_found()
|
||||
elif attachment["datas"]:
|
||||
data = io.BytesIO(base64.standard_b64decode(attachment["datas"]))
|
||||
return http.send_file(data, filename=attachment['name'], as_attachment=True)
|
||||
return http.send_file(data, filename=attachment['name'].encode('utf-8'), as_attachment=True)
|
||||
else:
|
||||
return request.not_found()
|
||||
|
||||
@@ -36,4 +36,5 @@ Luis González lgonzalez@vauxoo.com https://github.com/luisg123v
|
||||
Oriana Maita oriana@vauxoo.com https://github.com/maitaoriana
|
||||
Gabriela Mogollon gmogollon@vauxoo.com https://github.com/GavyMG
|
||||
Jesus Zapata jesus@vauxoo.com https://github.com/JesusZapata
|
||||
Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
|
||||
Germana Oliveira germana@vauxoo.com https://github.com/goliveirab
|
||||
Mariano Fernandez mariano@vauxoo.com https://github.com/Batuto
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Vietnam, 2017-08-15
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this declaration.
|
||||
|
||||
Signed,
|
||||
|
||||
Duong Bao Thang nothingctrl@gmail.com https://github.com/NothingCtrl
|
||||
@@ -0,0 +1,10 @@
|
||||
Indonesia, 2017-08-12
|
||||
|
||||
I hereby agree to the terms of the Odoo Individual Contributor License
|
||||
Agreement v1.0.
|
||||
|
||||
I declare that I am authorized and able to make this agreement and sign this
|
||||
declaration.
|
||||
|
||||
Signed,
|
||||
Edi Santoso repodevs@gmail.com https://github.com/repodevs
|
||||
@@ -159,7 +159,11 @@ database
|
||||
- ``%h`` is replaced by the whole hostname the request is made on.
|
||||
- ``%d`` is replaced by the subdomain the request is made on, with the
|
||||
exception of ``www`` (so domain ``odoo.com`` and ``www.odoo.com`` both
|
||||
match the database ``odoo``)
|
||||
match the database ``odoo``).
|
||||
|
||||
These operations are case sensitive. Add option ``(?i)`` to match all
|
||||
databases (so domain ``odoo.com`` using ``(?i)%d`` matches the database
|
||||
``Odoo``).
|
||||
|
||||
.. option:: --db-template <template>
|
||||
|
||||
|
||||
@@ -846,17 +846,13 @@ attributes:
|
||||
Possible children of the view element are:
|
||||
|
||||
``field``
|
||||
declares fields to aggregate or to use in kanban *logic*. If the field is
|
||||
simply displayed in the kanban view, it does not need to be pre-declared.
|
||||
declares fields to use in kanban *logic*. If the field is simply displayed in
|
||||
the kanban view, it does not need to be pre-declared.
|
||||
|
||||
Possible attributes are:
|
||||
|
||||
``name`` (required)
|
||||
the name of the field to fetch
|
||||
``sum``, ``avg``, ``min``, ``max``, ``count``
|
||||
displays the corresponding aggregation at the top of a kanban column, the
|
||||
field's value is the label of the aggregation (a string). Only one
|
||||
aggregate operation per field is supported.
|
||||
|
||||
``templates``
|
||||
defines a list of :ref:`reference/qweb` templates. Cards definition may be
|
||||
|
||||
@@ -519,7 +519,8 @@ class IrModelFields(models.Model):
|
||||
if field.state == 'manual' and field.ttype == 'many2many':
|
||||
rel_name = field.relation_table or model._fields[field.name].relation
|
||||
tables_to_drop.add(rel_name)
|
||||
model._pop_field(field.name)
|
||||
if field.state == 'manual':
|
||||
model._pop_field(field.name)
|
||||
|
||||
if tables_to_drop:
|
||||
# drop the relation tables that are not used by other fields
|
||||
@@ -620,7 +621,7 @@ class IrModelFields(models.Model):
|
||||
if not self.search([('model_id', '=', vals['relation']), ('name', '=', vals['relation_field']), ('ttype', '=', 'many2one')]):
|
||||
raise UserError(_("Many2one %s on model %s does not exist!") % (vals['relation_field'], vals['relation']))
|
||||
|
||||
self.clear_caches()
|
||||
self.clear_caches() # for _existing_field_data()
|
||||
|
||||
if vals['model'] in self.pool:
|
||||
# setup models; this re-initializes model in registry
|
||||
@@ -679,7 +680,7 @@ class IrModelFields(models.Model):
|
||||
|
||||
res = super(IrModelFields, self).write(vals)
|
||||
|
||||
self.clear_caches()
|
||||
self.clear_caches() # for _existing_field_data()
|
||||
|
||||
if column_rename:
|
||||
# rename column in database, and its corresponding index if present
|
||||
|
||||
@@ -66,7 +66,7 @@ class IrQWeb(models.AbstractModel, QWeb):
|
||||
# assume cache will be invalidated by third party on write to ir.ui.view
|
||||
def _get_template_cache_keys(self):
|
||||
""" Return the list of context keys to use for caching ``_get_template``. """
|
||||
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
|
||||
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id']
|
||||
|
||||
# apply ormcache_context decorator unless in dev mode...
|
||||
@tools.conditional(
|
||||
@@ -189,14 +189,14 @@ class IrQWeb(models.AbstractModel, QWeb):
|
||||
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
|
||||
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
|
||||
'xml' not in tools.config['dev_mode'],
|
||||
tools.ormcache('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async'),
|
||||
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
|
||||
)
|
||||
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
|
||||
files, remains = self._get_asset_content(xmlid, options)
|
||||
asset = AssetsBundle(xmlid, files, remains, env=self.env)
|
||||
return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url))
|
||||
|
||||
@tools.ormcache('xmlid', 'options.get("lang", "en_US")')
|
||||
@tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
|
||||
def _get_asset_content(self, xmlid, options):
|
||||
options = dict(options,
|
||||
inherit_branding=False, inherit_branding_auto=False,
|
||||
|
||||
@@ -431,6 +431,15 @@ actual arch.
|
||||
#------------------------------------------------------
|
||||
# Inheritance mecanism
|
||||
#------------------------------------------------------
|
||||
@api.model
|
||||
def _get_inheriting_views_arch_domain(self, view_id, model):
|
||||
return [
|
||||
['inherit_id', '=', view_id],
|
||||
['model', '=', model],
|
||||
['mode', '=', 'extension'],
|
||||
['active', '=', True],
|
||||
]
|
||||
|
||||
@api.model
|
||||
def get_inheriting_views_arch(self, view_id, model):
|
||||
"""Retrieves the architecture of views that inherit from the given view, from the sets of
|
||||
@@ -446,13 +455,8 @@ actual arch.
|
||||
:return: [(view_arch,view_id), ...]
|
||||
"""
|
||||
user_groups = self.env.user.groups_id
|
||||
conditions = self._get_inheriting_views_arch_domain(view_id, model)
|
||||
|
||||
conditions = [
|
||||
['inherit_id', '=', view_id],
|
||||
['model', '=', model],
|
||||
['mode', '=', 'extension'],
|
||||
['active', '=', True],
|
||||
]
|
||||
if self.pool._init and not self._context.get('load_all_views'):
|
||||
# Module init currently in progress, only consider views from
|
||||
# modules whose code is already loaded
|
||||
@@ -928,7 +932,7 @@ actual arch.
|
||||
|
||||
def _read_template_keys(self):
|
||||
""" Return the list of context keys to use for caching ``_read_template``. """
|
||||
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
|
||||
return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations', 'website_id']
|
||||
|
||||
# apply ormcache_context decorator unless in dev mode...
|
||||
@api.model
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<field name="header_line" />
|
||||
<field name="header_spacing" />
|
||||
<field name="dpi" />
|
||||
<field name="report_ids" widget="many2many_tags"/>
|
||||
<field name="report_ids" widget="many2many_tags" options="{'not_delete': True}"/>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user