summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2017-07-14 15:51:59 +0200
committerChristophe Simonis <chs@odoo.com>2017-07-14 15:51:59 +0200
commitdb2f82d5f1a8c4fdbbd61a0aa7daef2def0ef06e (patch)
tree4dd6a0d816180b06963a245d01a817ebcdaf7868 /addons
parent55d72e1ab4a64f52e040911a8b4b4c8ce9443f65 (diff)
parent4ca3d9cadf9607c95880bb5222041bced7b13ec0 (diff)
[MERGE] forward port branch saas-16 up to 4ca3d9cadf9
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_bank_statement.py5
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_action.js23
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_model.js97
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js65
-rw-r--r--addons/account/static/src/xml/account_reconciliation.xml10
-rw-r--r--addons/account/static/tests/reconciliation_tests.js64
-rw-r--r--addons/base_import/models/base_import.py2
-rw-r--r--addons/mail/static/src/js/many2many_tags_email.js144
-rw-r--r--addons/mail/static/src/js/thread_field.js4
-rw-r--r--addons/mail/static/tests/chatter_tests.js147
-rw-r--r--addons/mass_mailing/models/mass_mailing.py2
-rw-r--r--addons/mrp/models/mrp_workcenter.py2
-rw-r--r--addons/point_of_sale/i18n/point_of_sale.pot12
-rw-r--r--addons/point_of_sale/wizard/pos_details.py6
-rw-r--r--addons/product/models/product_pricelist.py2
-rw-r--r--addons/resource/i18n/resource.pot6
-rw-r--r--addons/resource/models/resource.py7
-rw-r--r--addons/sale/models/sale.py30
-rw-r--r--addons/sale_timesheet/tests/__init__.py4
-rw-r--r--addons/stock_account/models/stock.py8
-rw-r--r--addons/web/static/src/js/chrome/action_manager.js18
-rw-r--r--addons/web/static/src/js/chrome/view_manager.js13
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js12
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js3
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js80
-rw-r--r--addons/website/static/src/js/website.backendDashboard.js4
-rw-r--r--addons/website_crm_partner_assign/report/crm_lead_report.py2
28 files changed, 645 insertions, 129 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 4d7add6879a..85bed6a05ad 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -335,6 +335,7 @@ class AccountBankStatement(models.Model):
'st_lines_ids': st_lines_left.ids,
'notifications': [],
'statement_name': len(statements) == 1 and statements[0].name or False,
+ 'journal_id': statements and statements[0].journal_id.id or False,
'num_already_reconciled_lines': 0,
}
@@ -470,7 +471,7 @@ class AccountBankStatementLine(models.Model):
'details': {
'name': _("Automatically reconciled items"),
'model': 'account.move',
- 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').ids
+ 'ids': automatic_reconciliation_entries.mapped('journal_entry_ids').mapped('move_id').ids
}
}]
return {
@@ -586,7 +587,7 @@ class AccountBankStatementLine(models.Model):
if str:
str_domain = self.env['account.move.line'].domain_move_lines_for_reconciliation(str=str)
if not partner_id:
- str_domain = expression.OR([str_domain, ('partner_id.name', 'ilike', str)])
+ str_domain = expression.OR([str_domain, [('partner_id.name', 'ilike', str)]])
domain = expression.AND([domain, str_domain])
if excluded_ids:
domain = expression.AND([[('id', 'not in', excluded_ids)], domain])
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index b2c79471929..51175be7794 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -29,6 +29,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
validate: '_onValidate',
validate_all_balanced: '_onValidate',
change_name: '_onChangeName',
+ close_statement: '_onCloseStatement',
},
config: {
// used to instanciate the model
@@ -202,7 +203,26 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
});
});
},
-
+ /**
+ * call 'closeStatement' model method
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onCloseStatement: function (event) {
+ var self = this;
+ return this.model.closeStatement().then(function (result) {
+ self.do_action({
+ name: 'Bank Statements',
+ res_model: 'account.bank.statement',
+ res_id: result,
+ views: [[false, 'form']],
+ type: 'ir.actions.act_window',
+ view_type: 'form',
+ view_mode: 'form',
+ });
+ });
+ },
/**
* call 'validate' or 'autoReconciliation' model method then destroy the
* validated lines and update the action renderer with the new status bar
@@ -222,6 +242,7 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
'title': self.title,
'time': Date.now()-self.time,
'notifications': result.notifications,
+ 'context': self.model.getContext(),
});
_.each(result.handles, function (handle) {
self._getWidget(handle).destroy();
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 11e4f4bb55f..e12f8f21009 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -230,11 +230,31 @@ 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._performMoveLine(handle).then(function () {
- if (line.mode === 'create') {
- return self.createProposition(handle);
- }
- });
+ return this._changePartner(handle, partner.id)
+ .then(function() {
+ line.reconciliation_proposition = [];
+ return self._performMoveLine(handle)
+ })
+ .then(function () {
+ if (line.mode === 'create') {
+ return self.createProposition(handle);
+ }
+ });
+ },
+ /**
+ * close the statement
+ * @returns {Deferred<number>} resolves to the res_id of the closed statements
+ */
+ closeStatement: function () {
+ var self = this;
+ return this._rpc({
+ model: 'account.bank.statement',
+ method: 'button_confirm_bank',
+ args: [self.bank_statement_id.id],
+ })
+ .then(function () {
+ return self.bank_statement_id.id
+ });
},
/**
*
@@ -257,6 +277,13 @@ var StatementModel = BasicModel.extend({
return this._computeLine(line);
},
/**
+ * Return context information and journal_id
+ * @returns {Object} context
+ */
+ getContext: function () {
+ return this.context;
+ },
+ /**
* get the line data for this handle
*
* @param {OdooEvent} event
@@ -282,6 +309,8 @@ var StatementModel = BasicModel.extend({
if (!statement_ids) {
return $.when();
}
+ this.context = context;
+
var def_statement = this._rpc({
model: 'account.bank.statement',
method: 'reconciliation_widget_preprocess',
@@ -292,6 +321,7 @@ var StatementModel = BasicModel.extend({
self.bank_statement_id = statement_ids.length === 1 ? {id: statement_ids[0], display_name: statement.statement_name} : false;
self.valuenow = 0;
self.valuemax = statement.st_lines_ids.length;
+ self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
'id': id,
@@ -484,14 +514,19 @@ var StatementModel = BasicModel.extend({
_.each(handles, function (handle) {
var line = self.getLine(handle);
var props = _.filter(line.reconciliation_proposition, function (prop) {return !prop.is_tax && !prop.invalid;});
+ if (props.length === 0) {
+ props.push(self._formatQuickCreate(line, {
+ account_id: [line.st_line.open_balance_account_id, self.accounts[line.st_line.open_balance_account_id]],
+ }));
+ }
ids.push(line.id);
values.push({
"partner_id": line.st_line.partner_id,
"counterpart_aml_dicts": _.map(_.filter(props, function (prop) {
- return !isNaN(prop.id) && prop.partial_reconcile;
+ return !isNaN(prop.id) && !prop.already_paid;
}), self._formatToProcessReconciliation.bind(self, line)),
"payment_aml_ids": _.pluck(_.filter(props, function (prop) {
- return !isNaN(prop.id) && !prop.partial_reconcile;
+ return !isNaN(prop.id) && prop.already_paid;
}), 'id'),
"new_aml_dicts": _.map(_.filter(props, function (prop) {
return isNaN(prop.id);
@@ -558,6 +593,29 @@ var StatementModel = BasicModel.extend({
return this._computeLine(line);
},
/**
+ * When changing partner, read property_account_receivable and payable
+ * of that partner because the counterpart account might cahnge depending
+ * on the partner
+ *
+ * @private
+ * @param {string} handle
+ * @param {integer} partner_id
+ * @returns {Deferred}
+ */
+ _changePartner: function (handle, partner_id) {
+ var self = this;
+ return this._rpc({
+ model: 'res.partner',
+ method: 'read',
+ args: [partner_id, ["property_account_receivable_id", "property_account_payable_id"]],
+ }).then(function (result) {
+ if (result.length > 0) {
+ var line = self.getLine(handle);
+ self.lines[handle].st_line.open_balance_account_id = line.amount < 0 ? result[0]['property_account_payable_id'][0] : result[0]['property_account_receivable_id'][0];
+ }
+ });
+ },
+ /**
* Calculates the balance; format each proposition amount_str and mark as
* invalid the line with empty account_id, amount or label
* Check the taxes server side for each updated propositions with tax_id
@@ -573,7 +631,7 @@ var StatementModel = BasicModel.extend({
// compute taxes
var tax_defs = [];
var reconciliation_proposition = [];
- var format_options = {
+ var formatOptions = {
currency_id: line.st_line.currency_id,
};
_.each(line.reconciliation_proposition, function (prop) {
@@ -610,10 +668,10 @@ var StatementModel = BasicModel.extend({
});
prop.amount = tax.base;
- prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, format_options);
+ prop.amount_str = field_utils.format.monetary(Math.abs(prop.amount), {}, formatOptions);
prop.invalid = !self._isValid(prop);
- tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, format_options);
+ tax_prop.amount_str = field_utils.format.monetary(Math.abs(tax_prop.amount), {}, formatOptions);
tax_prop.invalid = prop.invalid;
reconciliation_proposition.push(tax_prop);
@@ -644,7 +702,7 @@ var StatementModel = BasicModel.extend({
total = Math.round(total*1000)/1000 || 0;
line.balance = {
'amount': total,
- 'amount_str': field_utils.format.monetary(Math.abs(total), {}, format_options),
+ 'amount_str': field_utils.format.monetary(Math.abs(total), {}, formatOptions),
'account_code': self.accounts[line.st_line.open_balance_account_id],
};
line.balance.type = line.balance.amount ? (line.balance.amount > 0 && line.st_line.partner_id ? 0 : -1) : 1;
@@ -759,6 +817,9 @@ var StatementModel = BasicModel.extend({
_formatQuickCreate: function (line, values) {
values = values || {};
var account = this._formatNameGet(values.account_id);
+ var formatOptions = {
+ currency_id: line.st_line.currency_id,
+ };
var prop = {
'id': _.uniqueId('createLine'),
'label': values.label || line.st_line.name,
@@ -771,7 +832,7 @@ var StatementModel = BasicModel.extend({
'credit': 0,
'base_amount': values.amount_type !== "percentage" ?
(values.amount || line.balance.amount) :
- line.st_line.amount * values.amount / 100,
+ line.balance.amount * values.amount / 100,
'percent': values.amount_type === "percentage" ? values.amount : null,
'link': values.link,
'display': true,
@@ -781,9 +842,10 @@ var StatementModel = BasicModel.extend({
'__focus': '__focus' in values ? values.__focus : true,
};
if (prop.base_amount) {
+ // Call to format and parse needed to round the value to the currency precision
var sign = prop.base_amount < 0 ? -1 : 1;
- var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, line);
- prop.base_amount = sign * field_utils.parse.monetary(amount);
+ var amount = field_utils.format.monetary(Math.abs(prop.base_amount), {}, formatOptions);
+ prop.base_amount = sign * field_utils.parse.monetary(amount, {}, formatOptions);
}
prop.amount = prop.base_amount;
return prop;
@@ -843,7 +905,10 @@ var StatementModel = BasicModel.extend({
* @returns {object}
*/
_formatToProcessReconciliation: function (line, prop) {
- var amount = prop.partial_reconcile ? -line.st_line.amount : prop.amount;
+ var amount = -prop.amount;
+ if (prop.partial_reconcile === true) {
+ amount = -line.st_line.amount;
+ }
var result = {
name : prop.label,
debit : amount > 0 ? amount : 0,
@@ -854,7 +919,7 @@ var StatementModel = BasicModel.extend({
} else {
result.account_id = prop.account_id.id;
if (prop.journal_id) {
- result.journal_id = prop.account_id.id;
+ result.journal_id = prop.journal_id.id;
}
}
if (!isNaN(prop.id)) result.counterpart_aml_id = prop.id;
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index bd8d8e52982..e0ecf61d354 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -22,6 +22,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
'click div:first h1.statement_name': '_onClickStatementName',
'click div:first h1.statement_name_edition button': '_onValidateName',
"click *[rel='do_action']": "_onDoAction",
+ 'click button.button_back_to_statement': '_onGoToBankStatement',
+ 'click button.button_close_statement': '_onCloseBankStatement',
},
/**
* @override
@@ -107,7 +109,8 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
var $done = $(qweb.render("reconciliation.done", {
'duration': moment(dt).utc().format(time.strftime_to_moment_format(_t.database.parameters.time_format)),
'number': state.valuenow,
- 'timePerTransaction': Math.round(dt/1000/state.valuemax)
+ 'timePerTransaction': Math.round(dt/1000/state.valuemax),
+ 'context': state.context,
}));
this.$el.children().hide();
// display rainbowman after full reconciliation
@@ -165,11 +168,12 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
},
/**
* @private
+ * Click on close bank statement button, this will
+ * close and then open form view of bank statement
+ * @param {MouseEvent} event
*/
- _onValidateName: function () {
- var name = this.model.get(this.handleNameRecord).data.name;
- this.trigger_up('change_name', {'data': name});
- this.$('.statement_name, .statement_name_edition').toggle();
+ _onCloseBankStatement: function (e) {
+ this.trigger_up('close_statement');
},
/**
* @private
@@ -190,6 +194,34 @@ var StatementRenderer = Widget.extend(FieldManagerMixin, {
view_mode: "list"
});
},
+ /**
+ * Open the list view for account.bank.statement model
+ * @private
+ * @param {MouseEvent} event
+ */
+ _onGoToBankStatement: function (e) {
+ var journalId = $(e.target).attr('data_journal_id');
+ if (journalId) {
+ journalId = parseInt(journalId);
+ }
+ this.do_action({
+ name: 'Bank Statements',
+ res_model: 'account.bank.statement',
+ views: [[false, 'list'], [false, 'form']],
+ type: 'ir.actions.act_window',
+ context: {search_default_journal_id: journalId},
+ view_type: 'list',
+ view_mode: 'form',
+ });
+ },
+ /**
+ * @private
+ */
+ _onValidateName: function () {
+ var name = this.model.get(this.handleNameRecord).data.name;
+ this.trigger_up('change_name', {'data': name});
+ this.$('.statement_name, .statement_name_edition').toggle();
+ },
});
@@ -312,6 +344,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
if ((state.balance.amount !== 0 || line.partial_reconcile) && props.length === 1 &&
+ line.already_paid === false &&
(
(state.st_line.amount > 0 && state.st_line.amount < props[0].amount) ||
(state.st_line.amount < 0 && state.st_line.amount > props[0].amount))
@@ -374,6 +407,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
/**
* @private
+ * @param {jQueryElement} $el
+ */
+ _destroyPopover: function ($el) {
+ var popover = $el.data('bs.popover');
+ if (popover) {
+ popover.destroy();
+ }
+ },
+ /**
+ * @private
* @param {integer} partnerID
* @param {string} partnerName
* @returns {string} local id of the dataPoint
@@ -557,16 +600,20 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @param {MouseEvent} event
*/
_onSelectMoveLine: function (event) {
- var mv_line_id = $(event.target).closest('.mv_line').data('line-id');
- this.trigger_up('add_proposition', {'data': mv_line_id});
+ var $el = $(event.target)
+ this._destroyPopover($el);
+ var moveLineId = $el.closest('.mv_line').data('line-id');
+ this.trigger_up('add_proposition', {'data': moveLineId});
},
/**
* @private
* @param {MouseEvent} event
*/
_onSelectProposition: function (event) {
- var mv_line_id = $(event.target).closest('.mv_line').data('line-id');
- this.trigger_up('remove_proposition', {'data': mv_line_id});
+ var $el = $(event.target)
+ this._destroyPopover($el);
+ var moveLineId = $el.closest('.mv_line').data('line-id');
+ this.trigger_up('remove_proposition', {'data': moveLineId});
},
/**
* @private
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index 9f6ec95142a..e73a42c785b 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -48,6 +48,16 @@
<div t-name="reconciliation.done" class="done_message">
<h2>Congrats, you're all done!</h2>
<p>You reconciled <strong><t t-esc="number"/></strong> transactions in <strong><t t-esc="duration"/></strong>.<br/>That's on average <t t-esc="timePerTransaction"/> seconds per transaction.</p>
+ <t t-if="context &amp;&amp; context.active_model">
+ <p t-if="context['active_model'] === 'account.journal' || context['active_model'] === 'account.bank.statement'" class="actions_buttons">
+ <t t-if="context.journal_id">
+ <button class="button_back_to_statement btn btn-default btn-sm" t-att-data_journal_id='context.journal_id'>Go to bank statement(s)</button>
+ </t>
+ <t t-if="context['active_model'] === 'account.bank.statement'">
+ <button class="button_close_statement btn btn-primary btn-sm" style="display: inline-block;">Close statement</button>
+ </t>
+ </p>
+ </t>
</div>
<t t-name="reconciliation.line">
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 79841262c4f..0eeb805112c 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -9,6 +9,9 @@ var data = {
image: {string: "image", type: 'integer'},
customer: {string: "customer", type: 'boolean'},
supplier: {string: "supplier", type: 'boolean'},
+ parent_id: {string: "Parent", type: 'boolean'},
+ property_account_receivable_id: {string: 'Account receivable', type: 'many2one', relation: 'account.account'},
+ property_account_payable_id: {string: 'Account payable', type: 'many2one', relation: 'account.account'},
},
records: [
{id: 1, display_name: "partner 1", image: 'AAA', customer: true},
@@ -16,7 +19,7 @@ var data = {
{id: 3, display_name: "partner 3", image: 'CCC', customer: true},
{id: 4, display_name: "partner 4", image: 'DDD', customer: true},
{id: 8, display_name: "Agrolait", image: 'EEE', customer: true},
- {id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true},
+ {id: 12, display_name: "Camptocamp", image: 'FFF', supplier: true, property_account_receivable_id: 287, property_account_payable_id: 287},
],
mark_as_reconciled: function () {
return $.when();
@@ -339,7 +342,10 @@ var mv_lines = {
'[]': [],
'[5,"",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': 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-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false}
+ {'account_type': "receivable", 'amount_currency_str': "", 'currency_id': false, 'date_maturity': "2017-02-07", 'date': "2017-01-08", 'total_amount_str': "$ 525.00", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'name': "INV/2017/0003", 'partner_name': "Agrolait", 'total_amount_currency_str': "", 'id': 112, 'credit': 0.0, 'journal_id': [1, "Customer Invoices"], 'amount_str': "$ 525.00", 'debit': 525.0, 'account_code': "101200", 'ref': "", 'already_paid': false},
+ {'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': 134, '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}
],
'[5,"b",0,6]': [
{'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},
@@ -602,7 +608,19 @@ QUnit.module('account', {
testUtils.intercept(clientAction, 'call_service', function (event) {
assert.deepEqual(event.data.args[1].args,
- [[5],[{partner_id: 8, counterpart_aml_dicts: [], payment_aml_ids: [109,112], new_aml_dicts: []}]],
+ [[5],[{partner_id: 8, counterpart_aml_dicts: [{
+ "counterpart_aml_id": 109,
+ "credit": 650,
+ "debit": 0,
+ "name": "INV/2017/0002"
+ },
+ {
+ "counterpart_aml_id": 112,
+ "credit": 525,
+ "debit": 0,
+ "name": "INV/2017/0003"
+ }],
+ payment_aml_ids: [], new_aml_dicts: []}]],
"Should call process_reconciliations with ids");
});
@@ -625,13 +643,8 @@ QUnit.module('account', {
[6],
[{
partner_id: false,
- counterpart_aml_dicts:[{
- name: "BNK1/2017/0002: SUPP.OUT/2017/0002",
- debit: 32.58,
- credit: 0,
- counterpart_aml_id: 392
- }],
- payment_aml_ids: [],
+ counterpart_aml_dicts:[],
+ payment_aml_ids: [392],
new_aml_dicts: []
}]
], "should call process_reconciliations with partial reconcile values");
@@ -652,14 +665,14 @@ QUnit.module('account', {
assert.notOk( widget.$('.cell_left .line_info_button').length, "should not display the partial reconciliation alert");
widget.$('.accounting_view thead td:first').trigger('click');
widget.$('.match .cell_account_code:first').trigger('click');
- assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 1, "should display the partial reconciliation alert");
+ assert.equal( widget.$('.accounting_view tbody .cell_left .line_info_button').length, 0, "should not display the partial reconciliation alert");
assert.ok( widget.$('button.btn-primary:not(hidden)').length, "should not display the reconcile button");
assert.ok( widget.$('.text-danger:not(hidden)').length, "should display counterpart alert");
widget.$('.accounting_view .cell_left .line_info_button').trigger('click');
- assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 1, "should display a partial reconciliation alert");
- assert.ok(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should display the partial reconciliation information");
+ assert.strictEqual(widget.$('.accounting_view .cell_left .line_info_button').length, 0, "should not display a partial reconciliation alert");
+ assert.notOk(widget.$('.accounting_view .cell_left .line_info_button').hasClass('do_partial_reconcile_false'), "should not display the partial reconciliation information");
assert.ok( widget.$('button.btn-default:not(hidden)').length, "should display the validate button");
- assert.strictEqual( widget.$el.data('mode'), "inactive", "should be inactive mode");
+ assert.strictEqual( widget.$el.data('mode'), "match", "should be inactive mode");
widget.$('button.btn-default:not(hidden)').trigger('click');
clientAction.destroy();
@@ -722,6 +735,29 @@ QUnit.module('account', {
clientAction.destroy();
});
+ QUnit.test('Reconciliation change partner', function (assert) {
+ assert.expect(4);
+
+ var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
+
+ testUtils.addMockEnvironment(clientAction, {
+ data: this.params.data,
+ });
+
+ clientAction.appendTo($('#qunit-fixture'));
+ var widget = clientAction.widgets[0];
+ assert.strictEqual(widget.$('.o_input_dropdown input').val(), "Agrolait", "the partner many2one should display agrolait");
+ assert.strictEqual(widget.$('.match table tr').length, 2, "agrolait should have 2 propositions for reconciliation");
+
+ // Simulate changing partner
+ widget.$('.o_input_dropdown input').trigger('click');
+ $('.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");
+
+ clientAction.destroy();
+ });
QUnit.test('Reconciliation create line', function (assert) {
assert.expect(23);
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 0a763810604..85fb49924f6 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -615,7 +615,7 @@ class Import(models.TransientModel):
for num, line in enumerate(data):
if line[index]:
try:
- line[index] = dt.strftime(dt.strptime(ustr(line[index]).encode('utf-8'), user_format), server_format)
+ line[index] = dt.strftime(dt.strptime(ustr(line[index].strip()).encode('utf-8'), user_format), server_format)
except ValueError as e:
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e))
except Exception as e:
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
index 52e213ddb7d..d266147bcc7 100644
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ b/addons/mail/static/src/js/many2many_tags_email.js
@@ -1,53 +1,131 @@
odoo.define('mail.many2manytags', function (require) {
"use strict";
-// FIXME: apply https://github.com/odoo/odoo/commit/1217ae914b313df7fe8511c138871e585bb21c81
-
+var BasicModel = require('web.BasicModel');
var core = require('web.core');
var form_common = require('web.view_dialogs');
var field_registry = require('web.field_registry');
var relational_fields = require('web.relational_fields');
+var M2MTags = relational_fields.FieldMany2ManyTags;
var _t = core._t;
-var FieldMany2ManyTags = relational_fields.FieldMany2ManyTags;
+BasicModel.include({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
-var FieldMany2ManyTagsEmail = FieldMany2ManyTags.extend({
- init: function() {
- this.values_checking = [];
- this._super.apply(this, arguments);
+ /**
+ * @private
+ * @param {Object} record - an element from the localData
+ * @param {string} fieldName
+ * @return {Deferred<Object>} the deferred is resolved with the
+ * invalidPartnerIds
+ */
+ _setInvalidMany2ManyTagsEmail: function (record, fieldName) {
+ var self = this;
+ var localID = (record._changes && fieldName in record._changes) ?
+ record._changes[fieldName] :
+ record.data[fieldName];
+ var list = this.localData[localID];
+ var invalidPartnerIds = [];
+ _.each(list.data, function (id) {
+ var record = self.localData[id];
+ if (!record.data.email) {
+ invalidPartnerIds.push(record);
+ }
+ });
+ var def;
+ if (invalidPartnerIds) {
+ // remove invalid partners
+ var changes = {operation: 'REMOVE', ids: _.pluck(invalidPartnerIds, 'id')};
+ def = this._applyX2ManyChange(record, fieldName, changes);
+ }
+ return $.when(def).then(function () {
+ return $.when({
+ invalidPartnerIds: _.pluck(invalidPartnerIds, 'res_id'),
+ });
+ });
},
+});
+
+var FieldMany2ManyTagsEmail = M2MTags.extend({
+ fieldsToFetch: _.extend({}, M2MTags.prototype.fieldsToFetch, {
+ email: {type: 'char'},
+ }),
+ specialData: "_setInvalidMany2ManyTagsEmail",
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
- add_id: function (id) {
+ /**
+ * Open a popup for each invalid partners (without email) to fill the email.
+ *
+ * @private
+ * @returns {Deferred}
+ */
+ _checkEmailPopup: function () {
var self = this;
- var _super = this._super.bind(this);
- // check partner has email
- this.trigger_up('perform_model_rpc', {
- model: 'res.partner',
- method: 'search',
- args: [[
- ["id", "=", id],
- ["email", "=", false],
- ]],
- on_success: function(partner_id) {
- if (partner_id.length) {
- // invalid partner
- var pop = new form_common.FormViewDialog(self, {
- res_model: 'res.partner',
- res_id: partner_id[0],
- title: _t("Please complete partner's informations and Email"),
- }).open();
- pop.on('write_completed', self, function () {
- // self.values_checking = _.without(self.values_checking, id);
- _super.apply(self, [id]);
- });
- } else {
- // valid partner
- _super.apply(self, [id]);
- }
+
+ var popupDefs = [];
+ var validPartners = [];
+
+ // propose the user to correct invalid partners
+ _.each(this.record.specialData[this.name].invalidPartnerIds, function (resID) {
+ var def = $.Deferred();
+ popupDefs.push(def);
+
+ var pop = new form_common.FormViewDialog(self, {
+ res_model: self.field.relation,
+ res_id: resID,
+ title: _t("Please complete partner's informations and Email"),
+ on_saved: function (record) {
+ if (record.data.email) {
+ validPartners.push(record.res_id);
+ }
+ },
+ }).open();
+ pop.on('closed', self, function () {
+ def.resolve();
+ });
+ });
+ return $.when.apply($, popupDefs).then(function() {
+ // All popups have been processed for the given ids
+ // It is now time to set the final value with valid partners ids.
+ validPartners = _.uniq(validPartners);
+ if (validPartners.length) {
+ var values = _.map(validPartners, function (id) {
+ return {id: id};
+ });
+ self._setValue({
+ operation: 'ADD_M2M',
+ ids: values,
+ });
}
});
},
+ /**
+ * Override to check if all many2many values have an email set before
+ * rendering the widget.
+ *
+ * @override
+ * @private
+ */
+ _render: function () {
+ var self = this;
+ var def = $.Deferred();
+ var _super = this._super.bind(this);
+ if (this.record.specialData[this.name].invalidPartnerIds.length) {
+ def = this._checkEmailPopup();
+ } else {
+ def.resolve();
+ }
+ return def.then(function () {
+ return _super.apply(self, arguments);
+ });
+ },
});
field_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail);
diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js
index 481dc49af36..0d7635cae8b 100644
--- a/addons/mail/static/src/js/thread_field.js
+++ b/addons/mail/static/src/js/thread_field.js
@@ -21,7 +21,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
this.msgIDs = this.value.res_ids;
},
willStart: function () {
- return this._chatReady();
+ return this.alive(this._chatReady());
},
start: function () {
var self = this;
@@ -43,7 +43,7 @@ var ThreadField = AbstractField.extend(chat_mixin, {
var def1 = this.thread.appendTo(this.$el);
var def2 = this._super.apply(this, arguments);
- return $.when(def1, def2).then(function () {
+ return this.alive($.when(def1, def2)).then(function () {
// unwrap the thread to remove an unnecessary level on div
self.setElement(self.thread.$el);
diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js
index 2bb5742e378..f268851463d 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -11,7 +11,9 @@ var testUtils = require('web.test_utils');
var BasicComposer = Composers.BasicComposer;
+var createAsyncView = testUtils.createAsyncView;
var createView = testUtils.createView;
+var createAsyncView = testUtils.createAsyncView;
QUnit.module('mail', {}, function () {
@@ -862,5 +864,150 @@ QUnit.test('followers widget: do not display follower duplications', function (a
form.destroy();
});
+QUnit.test('does not render and crash when destroyed before chat system is ready', function (assert) {
+ assert.expect(0);
+
+ var def = $.Deferred();
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form string="Partners">' +
+ '<sheet>' +
+ '<field name="foo"/>' +
+ '</sheet>' +
+ '<div class="oe_chatter">' +
+ '<field name="message_follower_ids" widget="mail_followers"/>' +
+ '<field name="message_ids" widget="mail_thread"/>' +
+ '<field name="activity_ids" widget="mail_activity"/>' +
+ '</div>' +
+ '</form>',
+ res_id: 2,
+ mockRPC: function (route, args) {
+ if (route === '/mail/read_followers') {
+ return $.when({
+ followers: [],
+ subtypes: [],
+ });
+ }
+ return this._super(route, args);
+ },
+ intercepts: {
+ chat_manager_ready: function (event) {
+ // we delay the return of the chat_manager ready event
+ event.data.callback(def);
+ },
+ get_messages: function (event) {
+ event.stopPropagation();
+ event.data.callback($.when([{
+ attachment_ids: [],
+ body: "",
+ date: moment("2016-12-20 09:35:40"),
+ id: 34,
+ res_id: 3,
+ author_id: ["3", "Fu Ck Mil Grom"],
+ }]));
+ },
+ get_bus: function (event) {
+ event.stopPropagation();
+ event.data.callback(new Bus());
+ },
+ get_session: function (event) {
+ event.stopPropagation();
+ event.data.callback({uid: 1});
+ },
+ },
+ });
+
+ form.destroy();
+ // here, the chat_manager system is ready, and the chatter can try to render
+ // itself. We simply make sure here that no crashes occur (since the form
+ // view is destroyed, all rpcs will be dropped, and many other mechanisms
+ // relying on events will not work, such as _getBus)
+ def.resolve();
+});
+
+QUnit.module('FieldMany2ManyTagsEmail', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ display_name: { string: "Displayed name", type: "char" },
+ timmy: { string: "pokemon", type: "many2many", relation: 'partner_type'},
+ },
+ records: [{
+ id: 1,
+ display_name: "first record",
+ timmy: [],
+ }],
+ },
+ partner_type: {
+ fields: {
+ name: {string: "Partner Type", type: "char"},
+ email: {string: "Email", type: "char"},
+ },
+ records: [
+ {id: 12, display_name: "gold", email: 'coucou@petite.perruche'},
+ {id: 14, display_name: "silver", email: ''},
+ ]
+ },
+ };
+ },
+});
+
+QUnit.test('fieldmany2many tags email', function (assert) {
+ assert.expect(11);
+ var done = assert.async();
+
+ this.data.partner.records[0].timmy = [12, 14];
+
+ // the modals need to be closed before the form view rendering
+ createAsyncView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ res_id: 1,
+ arch:'<form string="Partners">' +
+ '<sheet>' +
+ '<field name="display_name"/>' +
+ '<field name="timmy" widget="many2many_tags_email"/>' +
+ '</sheet>' +
+ '</form>',
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ if (args.method ==='read' && args.model === 'partner_type') {
+ assert.step(args.args[0]);
+ assert.deepEqual(args.args[1] , ['display_name', 'email'], "should read the email");
+ }
+ return this._super.apply(this, arguments);
+ },
+ archs: {
+ 'partner_type,false,form': '<form string="Types"><field name="display_name"/><field name="email"/></form>',
+ },
+ }).then(function (form) {
+ // should read it 3 times (1 with the form view, one with the form dialog and one after save)
+ assert.verifySteps([[12, 14], [14], [14]]);
+ assert.strictEqual(form.$('.o_field_many2manytags[name="timmy"] span.o_tag_color_10').length, 2,
+ "the second tag should be present");
+
+ form.destroy();
+ done();
+ });
+
+ assert.strictEqual($('.modal-body.o_act_window').length, 1,
+ "there should be one modal opened to edit the empty email");
+ assert.strictEqual($('.modal-body.o_act_window input[name="display_name"]').val(), "silver",
+ "the opened modal should be a form view dialog with the partner_type 14");
+ assert.strictEqual($('.modal-body.o_act_window input[name="email"]').length, 1,
+ "there should be an email field in the modal");
+
+ // set the email and save the modal (will render the form view)
+ $('.modal-body.o_act_window input[name="email"]').val('coucou@petite.perruche').trigger('input');
+ $('.modal-footer .btn-primary').click();
+});
+
});
});
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index 7142cb5967c..44993bdc907 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -559,7 +559,7 @@ class MassMailing(models.Model):
def retry_failed_mail(self):
failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')])
failed_mails.mapped('statistics_ids').unlink()
- failed_mails.unlink()
+ failed_mails.sudo().unlink()
self.write({'state': 'in_queue'})
#------------------------------------------------------
diff --git a/addons/mrp/models/mrp_workcenter.py b/addons/mrp/models/mrp_workcenter.py
index 49d87c2fe3c..58226b2e9e8 100644
--- a/addons/mrp/models/mrp_workcenter.py
+++ b/addons/mrp/models/mrp_workcenter.py
@@ -16,7 +16,7 @@ class MrpWorkcenter(models.Model):
# resource
name = fields.Char(related='resource_id.name', store=True)
- time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', store=True)
+ time_efficiency = fields.Float('Time Efficiency', related='resource_id.time_efficiency', default=100, store=True)
active = fields.Boolean('Active', related='resource_id.active', default=True, store=True)
code = fields.Char('Code', copy=False)
diff --git a/addons/point_of_sale/i18n/point_of_sale.pot b/addons/point_of_sale/i18n/point_of_sale.pot
index 77c4a996ec8..0d7ea224acf 100644
--- a/addons/point_of_sale/i18n/point_of_sale.pot
+++ b/addons/point_of_sale/i18n/point_of_sale.pot
@@ -3513,6 +3513,18 @@ msgid "You have to select a pricelist in the sale form !\n"
msgstr ""
#. module: point_of_sale
+#: code:addons/point_of_sale/wizard/pos_details.py:48
+#, python-format
+msgid "You have to set a logo for your company."
+msgstr ""
+
+#. module: point_of_sale
+#: code:addons/point_of_sale/wizard/pos_details.py:50
+#, python-format
+msgid "You have to set your reports's header and footer layout."
+msgstr ""
+
+#. module: point_of_sale
#: code:addons/point_of_sale/models/pos_session.py:24
#, python-format
msgid "You have to set a Sale Journal for the POS:%s"
diff --git a/addons/point_of_sale/wizard/pos_details.py b/addons/point_of_sale/wizard/pos_details.py
index c4691daec3a..1e8fed01a25 100644
--- a/addons/point_of_sale/wizard/pos_details.py
+++ b/addons/point_of_sale/wizard/pos_details.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import api, fields, models
+from odoo import api, fields, models, _
from odoo.exceptions import UserError
@@ -44,5 +44,9 @@ class PosDetails(models.TransientModel):
@api.multi
def generate_report(self):
+ if (not self.env.user.company_id.logo):
+ raise UserError(_("You have to set a logo or a layout for your company."))
+ elif (not self.env.user.company_id.external_report_layout):
+ raise UserError(_("You have to set your reports's header and footer layout."))
data = {'date_start': self.start_date, 'date_stop': self.end_date, 'config_ids': self.pos_config_ids.ids}
return self.env.ref('point_of_sale.sale_details_report').report_action([], data=data)
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index 49ea8241f95..41e9b79110b 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -108,7 +108,7 @@ class Pricelist(models.Model):
"""
self.ensure_one()
if not date:
- date = self._context.get('date', fields.Date.today())
+ date = self._context.get('date') or fields.Date.today()
if not uom_id and self._context.get('uom'):
uom_id = self._context['uom']
if uom_id:
diff --git a/addons/resource/i18n/resource.pot b/addons/resource/i18n/resource.pot
index 30b510ade92..aa093e5292e 100644
--- a/addons/resource/i18n/resource.pot
+++ b/addons/resource/i18n/resource.pot
@@ -442,6 +442,12 @@ msgid "This field is used to calculate the the expected duration of a work order
msgstr ""
#. module: resource
+#: code:addons/resource/models/resource.py:667
+#, python-format
+msgid "The efficiency factor cannot be equal to 0."
+msgstr ""
+
+#. module: resource
#: selection:resource.calendar.attendance,dayofweek:0
msgid "Thursday"
msgstr ""
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 7c86cfd232c..cd26abaabc8 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -673,6 +673,13 @@ class ResourceResource(models.Model):
('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'),
]
+ @api.multi
+ @api.constrains('time_efficiency')
+ def _check_time_efficiency(self):
+ for record in self:
+ if record.time_efficiency == 0:
+ raise ValidationError(_('The efficiency factor cannot be equal to 0.'))
+
@api.model
def create(self, values):
if values.get('company_id') and not values.get('calendar_id'):
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index a22b6e7c42c..4ff43dd1b35 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -923,16 +923,7 @@ class SaleOrderLine(models.Model):
uom=self.product_uom.id
)
- name = product.name_get()[0][1]
- if product.description_sale:
- name += '\n' + product.description_sale
- vals['name'] = name
-
- self._compute_tax_id()
-
- if self.order_id.pricelist_id and self.order_id.partner_id:
- vals['price_unit'] = self.env['account.tax']._fix_tax_included_price(self._get_display_price(product), product.taxes_id, self.tax_id)
- self.update(vals)
+ result = {'domain': domain}
title = False
message = False
@@ -942,14 +933,27 @@ class SaleOrderLine(models.Model):
message = product.sale_line_warn_msg
warning['title'] = title
warning['message'] = message
+ result = {'warning': warning}
if product.sale_line_warn == 'block':
self.product_id = False
- return {'warning': warning}
- return {'domain': domain}
+ return result
+
+ name = product.name_get()[0][1]
+ if product.description_sale:
+ name += '\n' + product.description_sale
+ vals['name'] = name
+
+ self._compute_tax_id()
+
+ if self.order_id.pricelist_id and self.order_id.partner_id:
+ vals['price_unit'] = self.env['account.tax']._fix_tax_included_price(self._get_display_price(product), product.taxes_id, self.tax_id)
+ self.update(vals)
+
+ return result
@api.onchange('product_uom', 'product_uom_qty')
def product_uom_change(self):
- if not self.product_uom:
+ if not self.product_uom or not self.product_id:
self.price_unit = 0.0
return
if self.order_id.pricelist_id and self.order_id.partner_id:
diff --git a/addons/sale_timesheet/tests/__init__.py b/addons/sale_timesheet/tests/__init__.py
index 502409b7144..9b3932ef5fb 100644
--- a/addons/sale_timesheet/tests/__init__.py
+++ b/addons/sale_timesheet/tests/__init__.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-#from . import test_sale_timesheet
-#from . import test_sale_service
+from . import test_sale_timesheet
+from . import test_sale_service
from . import test_timesheet_revenue
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 9f05f2d3861..fc2bcdc5b53 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -315,8 +315,8 @@ class StockMove(models.Model):
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
- 'debit': debit_value,
- 'credit': 0,
+ 'debit': debit_value if debit_value > 0 else 0,
+ 'credit': -debit_value if debit_value < 0 else 0,
'account_id': debit_account_id,
}
credit_line_vals = {
@@ -326,8 +326,8 @@ class StockMove(models.Model):
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
- 'credit': credit_value,
- 'debit': 0,
+ 'credit': credit_value if credit_value > 0 else 0,
+ 'debit': -credit_value if credit_value < 0 else 0,
'account_id': credit_account_id,
}
res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 280a90cec8d..5d4a44ba420 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -420,16 +420,20 @@ var ActionManager = Widget.extend({
return this.inner_widget;
},
history_back: function() {
- var nb_views = this.inner_action.get_nb_views();
- if (nb_views > 1) {
+ var nbViews = this.inner_action.get_nb_views();
+ if (nbViews > 1) {
// Stay on this action, but select the previous view
- return this.select_action(this.inner_action, nb_views - 2);
+ return this.select_action(this.inner_action, nbViews - 2);
}
- if (this.action_stack.length > 1) {
+ var nbActions = this.action_stack.length;
+ if (nbActions > 1) {
// Select the previous action
- var action = this.action_stack[this.action_stack.length - 2];
- nb_views = action.get_nb_views();
- return this.select_action(action, nb_views - 1);
+ var action = this.action_stack[nbActions - 2];
+ nbViews = action.get_nb_views();
+ return this.select_action(action, nbViews - 1);
+ }
+ else if (nbActions === 1 && nbViews === 1) {
+ return this.select_action(this.action_stack[0], 0);
}
return $.Deferred().reject();
},
diff --git a/addons/web/static/src/js/chrome/view_manager.js b/addons/web/static/src/js/chrome/view_manager.js
index 14ed4bdebc7..9b431d3e57a 100644
--- a/addons/web/static/src/js/chrome/view_manager.js
+++ b/addons/web/static/src/js/chrome/view_manager.js
@@ -561,7 +561,8 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
var self = this;
var result_handler = on_closed || function () {};
var context = new Context(this.env.context, action_data.context || {});
- var record_id = res_ids && res_ids[0];
+ // OR NULL hereunder: pyeval waits specifically for a null value, different from undefined
+ var record_id = res_ids && res_ids[0] || null;
// response handler
var handler = function (action) {
@@ -651,18 +652,10 @@ var ViewManager = Widget.extend(ControlPanelMixin, {
* will call this very view manager to activate the previous view.
* @todo: directly switch to previous view
*
- * A special case is done in the case that there is no previous view. When
- * that happens, we simply rerender the current view.
- *
* @private
*/
_onSwitchToPreviousView: function () {
- if (this.view_stack.length === 1) {
- var currentView = this.view_stack[0].controller;
- currentView.update({}, {reload: false});
- } else {
- this.do_action('history_back');
- }
+ this.trigger_up('history_back');
}
});
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 567c54abcb1..14512c02912 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -485,8 +485,16 @@ var FieldMany2One = AbstractField.extend({
},
/**
* @private
+ *
+ * @param {OdooEvent} ev
*/
- _onInputKeyup: function () {
+ _onInputKeyup: function (ev) {
+ if (ev.which === $.ui.keyCode.ENTER) {
+ // If we pressed enter, we want to prevent _onInputFocusout from
+ // executing since it would open a M2O dialog to request
+ // confirmation that the many2one is not properly set.
+ return;
+ }
if (this.$input.val() === "") {
this.reinitialize(false);
} else if (this._getDisplayName(this.m2o_value) !== this.$input.val()) {
@@ -1909,7 +1917,7 @@ var FieldRadio = FieldSelection.extend({
this._super.apply(this, arguments);
if (this.mode === 'edit') {
this.tagName = 'div';
- this.className += this.nodeOptions.horizontal ? 'o_horizontal' : 'o_vertical';
+ this.className += this.nodeOptions.horizontal ? ' o_horizontal' : ' o_vertical';
}
this.unique_id = _.uniqueId("radio");
this._setValues();
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 82dc6f90d26..cc72816a85d 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -250,7 +250,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
on_fail: function (reason) {
reload().always(function() {
def.reject(reason);
- })
+ });
},
on_success: def.resolve.bind(def),
});
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 906464a5a26..c562fa388f0 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1187,7 +1187,8 @@ var BasicModel = AbstractModel.extend({
}
var self = this;
- var list = this.localData[record._changes[fieldName] || record.data[fieldName]];
+ var localID = (record._changes && record._changes[fieldName]) || record.data[fieldName];
+ var list = this.localData[localID];
var field = record.fields[fieldName];
var fieldInfo = record.fieldsInfo[viewType || record.viewType][fieldName];
var view = fieldInfo.views && fieldInfo.views[fieldInfo.mode];
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index d4d3a6307f9..aacbb744252 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -30,6 +30,7 @@ QUnit.module('relational_fields', {
type: "selection",
selection: [['red', "Red"], ['black', "Black"]],
default: 'red',
+ string: "Color",
},
date: {string: "Some Date", type: "date"},
datetime: {string: "Datetime Field", type: 'datetime'},
@@ -1140,6 +1141,52 @@ QUnit.module('relational_fields', {
});
});
+ QUnit.test('pressing ENTER on a \'no_quick_create\' many2one should not trigger M2ODialog', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ var M2O_DELAY = relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY;
+ relationalFields.FieldMany2One.prototype.AUTOCOMPLETE_DELAY = 0;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="trululu" options="{\'no_quick_create\': True}"/>' +
+ '<field name="foo"/>' +
+ '</form>',
+ archs: {
+ 'partner,false,form': '<form string="Partners"><field name="display_name"/></form>',
+ },
+ });
+
+ var $input = form.$('.o_field_many2one input');
+ $input.val("Something that does not exist").trigger('input');
+ $('.ui-autocomplete .ui-menu-item a:contains(Create and)').trigger('mouseenter');
+ concurrency.delay(0).then(function() {
+ $input.trigger($.Event('keydown', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+ $input.trigger($.Event('keypress', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+ $input.trigger($.Event('keyup', {
+ which: $.ui.keyCode.ENTER,
+ keyCode: $.ui.keyCode.ENTER,
+ }));
+ concurrency.delay(0).then(function() {
+ $input.blur();
+ assert.strictEqual($('.modal').length, 1,
+ "should have one modal in body");
+ form.destroy();
+ done();
+ });
+ });
+ });
+
QUnit.test('many2one in editable list + onchange, with enter [REQUIRE FOCUS]', function (assert) {
assert.expect(6);
var done = assert.async();
@@ -1472,11 +1519,11 @@ QUnit.module('relational_fields', {
}, true);
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
- "should have the 3 rows in the correct order")
+ "should have the 3 rows in the correct order");
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "My little Foo Valueblipyop",
- "should still have the 3 rows in the correct order")
+ "should still have the 3 rows in the correct order");
// Drag and drop the fourth line in second position
testUtils.dragAndDrop(
@@ -1489,11 +1536,11 @@ QUnit.module('relational_fields', {
"sequences values should be incremental starting from the previous minimum one");
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
- "should have the 3 rows in the new order")
+ "should have the 3 rows in the new order");
form.$buttons.find('.o_form_button_save').click();
assert.strictEqual(form.$('td.o_data_cell:not(.o_handle_cell)').text(), "blipMy little Foo Valueyop",
- "should still have the 3 rows in the new order")
+ "should still have the 3 rows in the new order");
form.destroy();
});
@@ -6412,6 +6459,31 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('fieldradio widget has o_horizontal or o_vertical class', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.color2 = this.data.partner.fields.color;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<group>' +
+ '<field name="color" widget="radio"/>' +
+ '<field name="color2" widget="radio" options="{\'horizontal\': True}"/>' +
+ '</group>' +
+ '</form>',
+ });
+
+ var btn1 = form.$('div.o_field_radio.o_vertical');
+ var btn2 = form.$('div.o_field_radio.o_horizontal');
+
+ assert.strictEqual(btn1.length, 1, "should have o_vertical class");
+ assert.strictEqual(btn2.length, 1, "should have o_horizontal class");
+ form.destroy();
+ });
+
QUnit.test('fieldradio widget with numerical keys encoded as strings', function (assert) {
assert.expect(5);
diff --git a/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js
index f1dcdf6a6ec..16b1281f222 100644
--- a/addons/website/static/src/js/website.backendDashboard.js
+++ b/addons/website/static/src/js/website.backendDashboard.js
@@ -59,8 +59,8 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
fetch_data: function() {
var self = this;
return ajax.jsonRpc('/website/fetch_dashboard_data', 'call', {
- 'date_from': this.date_from.format('YYYY-MM-DD'),
- 'date_to': this.date_to.format('YYYY-MM-DD'),
+ 'date_from': this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
+ 'date_to': this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
}).done(function(result) {
self.data = result;
self.dashboards_data = result.dashboards;
diff --git a/addons/website_crm_partner_assign/report/crm_lead_report.py b/addons/website_crm_partner_assign/report/crm_lead_report.py
index e72a1b3dedf..07676117473 100644
--- a/addons/website_crm_partner_assign/report/crm_lead_report.py
+++ b/addons/website_crm_partner_assign/report/crm_lead_report.py
@@ -67,7 +67,7 @@ class CrmLeadReportAssign(models.Model):
p.grade_id,
p.date as partner_date,
c.planned_revenue*(c.probability/100) as probable_revenue,
- 1 as nbr,
+ 1 as nbr_cases,
c.create_date as create_date,
extract('epoch' from (c.write_date-c.create_date))/(3600*24) as delay_close,
extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as delay_expected,