[MERGE] forward port branch saas-16 up to 4ca3d9cadf
This commit is contained in:
@@ -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])
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
});
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -256,6 +276,13 @@ var StatementModel = BasicModel.extend({
|
||||
line.createForm = _.pick(prop, this.quickCreateFields);
|
||||
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
|
||||
*
|
||||
@@ -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);
|
||||
@@ -557,6 +592,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
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
@@ -372,6 +405,16 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {jQueryElement} $el
|
||||
*/
|
||||
_destroyPopover: function ($el) {
|
||||
var popover = $el.data('bs.popover');
|
||||
if (popover) {
|
||||
popover.destroy();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} partnerID
|
||||
@@ -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
|
||||
|
||||
@@ -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 && 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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,52 +1,130 @@
|
||||
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({
|
||||
|
||||
var FieldMany2ManyTagsEmail = FieldMany2ManyTags.extend({
|
||||
init: function() {
|
||||
this.values_checking = [];
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
add_id: function (id) {
|
||||
/**
|
||||
* @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 _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 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
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Open a popup for each invalid partners (without email) to fill the email.
|
||||
*
|
||||
* @private
|
||||
* @returns {Deferred}
|
||||
*/
|
||||
_checkEmailPopup: function () {
|
||||
var self = this;
|
||||
|
||||
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);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'})
|
||||
|
||||
#------------------------------------------------------
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3512,6 +3512,18 @@ msgid "You have to select a pricelist in the sale form !\n"
|
||||
"Please set one before choosing a product."
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -441,6 +441,12 @@ msgstr ""
|
||||
msgid "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes."
|
||||
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"
|
||||
|
||||
@@ -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'):
|
||||
|
||||
+17
-13
@@ -923,6 +923,21 @@ class SaleOrderLine(models.Model):
|
||||
uom=self.product_uom.id
|
||||
)
|
||||
|
||||
result = {'domain': domain}
|
||||
|
||||
title = False
|
||||
message = False
|
||||
warning = {}
|
||||
if product.sale_line_warn != 'no-message':
|
||||
title = _("Warning for %s") % product.name
|
||||
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 result
|
||||
|
||||
name = product.name_get()[0][1]
|
||||
if product.description_sale:
|
||||
name += '\n' + product.description_sale
|
||||
@@ -934,22 +949,11 @@ class SaleOrderLine(models.Model):
|
||||
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)
|
||||
|
||||
title = False
|
||||
message = False
|
||||
warning = {}
|
||||
if product.sale_line_warn != 'no-message':
|
||||
title = _("Warning for %s") % product.name
|
||||
message = product.sale_line_warn_msg
|
||||
warning['title'] = title
|
||||
warning['message'] = message
|
||||
if product.sale_line_warn == 'block':
|
||||
self.product_id = False
|
||||
return {'warning': warning}
|
||||
return {'domain': domain}
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,12 +9,13 @@ Index: addons/openacademy/__manifest__.py
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
- 'depends': ['base'],
|
||||
+ 'depends': ['base', 'board'],
|
||||
- 'depends': ['base', 'report'],
|
||||
+ 'depends': ['base', 'report', 'board'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
@@ -31,6 +31,7 @@
|
||||
@@ -30,6 +30,7 @@
|
||||
'templates.xml',
|
||||
'views/openacademy.xml',
|
||||
'views/partner.xml',
|
||||
+ 'views/session_board.xml',
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
# HG changeset patch
|
||||
# Parent c140f0a861a08881d8737bca0ffb83904a2059a3
|
||||
|
||||
Index: addons/openacademy/__manifest__.py
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/__manifest__.py 2014-08-26 17:26:21.535783052 +0200
|
||||
+++ addons/openacademy/__manifest__.py 2014-08-26 17:26:21.531783052 +0200
|
||||
@@ -21,7 +21,7 @@
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
- 'depends': ['base'],
|
||||
+ 'depends': ['base', 'report'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
Index: addons/openacademy/__manifest__.py
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/__manifest__.py 2014-08-29 08:39:43.975536806 +0200
|
||||
+++ addons/openacademy/__manifest__.py 2014-08-29 08:39:52.000000000 +0200
|
||||
@@ -31,6 +31,7 @@
|
||||
@@ -30,6 +30,7 @@
|
||||
'templates.xml',
|
||||
'views/openacademy.xml',
|
||||
'views/partner.xml',
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
# HG changeset patch
|
||||
# Parent 8c721171aa16a41e94059f53d6780c67b5ef2dfc
|
||||
# Parent 8d2ca42b5be2031ea9624896df53f09f7ca131be
|
||||
|
||||
diff --git a/openacademy/models.py b/openacademy/models.py
|
||||
--- a/openacademy/models.py
|
||||
+++ b/openacademy/models.py
|
||||
@@ -66,6 +66,24 @@ class Session(models.Model):
|
||||
attendees_count = fields.Integer(
|
||||
string="Attendees count", compute='_get_attendees_count', store=True)
|
||||
|
||||
+ state = fields.Selection([
|
||||
+ ('draft', "Draft"),
|
||||
+ ('confirmed', "Confirmed"),
|
||||
+ ('done', "Done"),
|
||||
+ ], default='draft')
|
||||
+
|
||||
+ @api.multi
|
||||
+ def action_draft(self):
|
||||
+ self.state = 'draft'
|
||||
+
|
||||
+ @api.multi
|
||||
+ def action_confirm(self):
|
||||
+ self.state = 'confirmed'
|
||||
+
|
||||
+ @api.multi
|
||||
+ def action_done(self):
|
||||
+ self.state = 'done'
|
||||
+
|
||||
@api.depends('seats', 'attendee_ids')
|
||||
def _taken_seats(self):
|
||||
for r in self:
|
||||
diff --git a/openacademy/views/openacademy.xml b/openacademy/views/openacademy.xml
|
||||
--- a/openacademy/views/openacademy.xml
|
||||
+++ b/openacademy/views/openacademy.xml
|
||||
@@ -96,6 +96,19 @@
|
||||
<field name="model">openacademy.session</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Session Form">
|
||||
+ <header>
|
||||
+ <button name="action_draft" type="object"
|
||||
+ string="Reset to draft"
|
||||
+ states="confirmed,done"/>
|
||||
+ <button name="action_confirm" type="object"
|
||||
+ string="Confirm" states="draft"
|
||||
+ class="oe_highlight"/>
|
||||
+ <button name="action_done" type="object"
|
||||
+ string="Mark as done" states="confirmed"
|
||||
+ class="oe_highlight"/>
|
||||
+ <field name="state" widget="statusbar"/>
|
||||
+ </header>
|
||||
+
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="General">
|
||||
@@ -28,7 +28,7 @@ diff --git a/openacademy/models.py b/openacademy/models.py
|
||||
|
||||
default['name'] = new_name
|
||||
return super(Course, self).copy(default)
|
||||
@@ -97,15 +97,15 @@ class Session(models.Model):
|
||||
@@ -79,15 +79,15 @@ class Session(models.Model):
|
||||
if self.seats < 0:
|
||||
return {
|
||||
'warning': {
|
||||
@@ -48,7 +48,7 @@ diff --git a/openacademy/models.py b/openacademy/models.py
|
||||
},
|
||||
}
|
||||
|
||||
@@ -151,4 +151,4 @@ class Session(models.Model):
|
||||
@@ -133,4 +133,4 @@ class Session(models.Model):
|
||||
def _check_instructor_not_in_attendees(self):
|
||||
for r in self:
|
||||
if r.instructor_id and r.instructor_id in r.attendee_ids:
|
||||
|
||||
@@ -2,7 +2,7 @@ Index: addons/openacademy/views/openacademy.xml
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:19.255004611 +0200
|
||||
+++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:19.251004612 +0200
|
||||
@@ -248,6 +248,12 @@
|
||||
@@ -234,6 +234,12 @@
|
||||
<field name="session_id"/>
|
||||
<field name="attendee_ids"/>
|
||||
</group>
|
||||
|
||||
@@ -17,7 +17,7 @@ Index: addons/openacademy/views/openacademy.xml
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:07.879004780 +0200
|
||||
+++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:07.871004780 +0200
|
||||
@@ -237,5 +237,26 @@
|
||||
@@ -224,5 +224,26 @@
|
||||
<menuitem id="session_menu" name="Sessions"
|
||||
parent="openacademy_menu"
|
||||
action="session_list_action"/>
|
||||
|
||||
@@ -2,7 +2,7 @@ Index: addons/openacademy/views/openacademy.xml
|
||||
===================================================================
|
||||
--- addons.orig/openacademy/views/openacademy.xml 2014-08-28 14:34:28.583004473 +0200
|
||||
+++ addons/openacademy/views/openacademy.xml 2014-08-28 14:34:28.579004473 +0200
|
||||
@@ -245,7 +245,7 @@
|
||||
@@ -231,7 +231,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Add Attendees">
|
||||
<group>
|
||||
|
||||
@@ -24,7 +24,6 @@ exercise-searchview
|
||||
exercise-gantt
|
||||
exercise-graph
|
||||
exercise-kanban
|
||||
exercise-state-basic
|
||||
exercise-access-rights
|
||||
exercise-access-rules
|
||||
exercise-wizard
|
||||
|
||||
@@ -546,7 +546,7 @@ default db to serve on localhost:8069
|
||||
http://www.enterprisedb.com/products-services-training/pgdownload
|
||||
.. _Quilt: http://en.wikipedia.org/wiki/Quilt_(software)
|
||||
.. _saas: https://www.odoo.com/page/start
|
||||
.. _the wkhtmltopdf download page: http://wkhtmltopdf.org/downloads.html
|
||||
.. _the wkhtmltopdf download page: https://github.com/wkhtmltopdf/wkhtmltopdf/releases/tag/0.12.1
|
||||
.. _UAC: http://en.wikipedia.org/wiki/User_Account_Control
|
||||
.. _wkhtmltopdf: http://wkhtmltopdf.org
|
||||
.. _pip: https://pip.pypa.io
|
||||
|
||||
Reference in New Issue
Block a user