[MERGE] forward port branch saas-16 up to 4ca3d9cadf

This commit is contained in:
Christophe Simonis
2017-07-14 15:51:59 +02:00
37 changed files with 670 additions and 196 deletions
@@ -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 &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">
@@ -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);
+1 -1
View File
@@ -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:
+112 -34
View File
@@ -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);
});
},
});
+2 -2
View File
@@ -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);
+147
View File
@@ -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();
});
});
});
+1 -1
View File
@@ -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'})
#------------------------------------------------------
+1 -1
View File
@@ -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
+5 -1
View File
@@ -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)
+1 -1
View File
@@ -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:
+6
View File
@@ -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"
+7
View File
@@ -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
View File
@@ -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:
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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,
+4 -3
View File
@@ -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',
+14 -1
View File
@@ -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',
-55
View File
@@ -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">
+2 -2
View File
@@ -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:
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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>
-1
View File
@@ -24,7 +24,6 @@ exercise-searchview
exercise-gantt
exercise-graph
exercise-kanban
exercise-state-basic
exercise-access-rights
exercise-access-rules
exercise-wizard
+1 -1
View File
@@ -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