171c92c9aab62198fcecfbce25705dfb5f40539e
[odoo/odoo.git] / addons / account / static / src / js / account_widgets.js
1 openerp.account = function (instance) {
2     openerp.account.quickadd(instance);
3     var _t = instance.web._t,
4         _lt = instance.web._lt;
5     var QWeb = instance.web.qweb;
6     
7     instance.web.account = instance.web.account || {};
8     
9     instance.web.client_actions.add('bank_statement_reconciliation_view', 'instance.web.account.bankStatementReconciliation');
10     instance.web.account.bankStatementReconciliation = instance.web.Widget.extend({
11         className: 'oe_bank_statement_reconciliation',
12
13         events: {
14             "click .statement_name span": "statementNameClickHandler",
15             "keyup .change_statement_name_field": "changeStatementNameFieldHandler",
16             "click .change_statement_name_button": "changeStatementButtonClickHandler",
17         },
18     
19         init: function(parent, context) {
20             this._super(parent);
21             this.max_reconciliations_displayed = 10;
22             if (context.context.statement_id) this.statement_ids = [context.context.statement_id];
23             if (context.context.statement_ids) this.statement_ids = context.context.statement_ids;
24             this.single_statement = this.statement_ids !== undefined && this.statement_ids.length === 1;
25             this.multiple_statements = this.statement_ids !== undefined && this.statement_ids.length > 1;
26             this.title = context.context.title || _t("Reconciliation");
27             this.st_lines = [];
28             this.last_displayed_reconciliation_index = undefined; // Flow control
29             this.reconciled_lines = 0; // idem
30             this.already_reconciled_lines = 0; // Number of lines of the statement which were already reconciled
31             this.model_bank_statement = new instance.web.Model("account.bank.statement");
32             this.model_bank_statement_line = new instance.web.Model("account.bank.statement.line");
33             this.reconciliation_menu_id = false; // Used to update the needaction badge
34             this.formatCurrency; // Method that formats the currency ; loaded from the server
35     
36             // Only for statistical purposes
37             this.lines_reconciled_with_ctrl_enter = 0;
38             this.time_widget_loaded = Date.now();
39     
40             // Stuff used by the children bankStatementReconciliationLine
41             this.max_move_lines_displayed = 5;
42             this.animation_speed = 100; // "Blocking" animations
43             this.aestetic_animation_speed = 300; // eye candy
44             this.map_currency_id_rounding = {};
45             this.map_tax_id_amount = {};
46             this.presets = {};
47             // We'll need to get the code of an account selected in a many2one (whose value is the id)
48             this.map_account_id_code = {};
49             // The same move line cannot be selected for multiple resolutions
50             this.excluded_move_lines_ids = {};
51             // Description of the fields to initialize in the "create new line" form
52             // NB : for presets to work correctly, a field id must be the same string as a preset field
53             this.create_form_fields = {
54                 account_id: {
55                     id: "account_id",
56                     index: 0,
57                     corresponding_property: "account_id", // a account.move field name
58                     label: _t("Account"),
59                     required: true,
60                     tabindex: 10,
61                     constructor: instance.web.form.FieldMany2One,
62                     field_properties: {
63                         relation: "account.account",
64                         string: _t("Account"),
65                         type: "many2one",
66                         domain: [['type','not in',['view', 'closed', 'consolidation']]],
67                     },
68                 },
69                 label: {
70                     id: "label",
71                     index: 1,
72                     corresponding_property: "label",
73                     label: _t("Label"),
74                     required: true,
75                     tabindex: 11,
76                     constructor: instance.web.form.FieldChar,
77                     field_properties: {
78                         string: _t("Label"),
79                         type: "char",
80                     },
81                 },
82                 tax_id: {
83                     id: "tax_id",
84                     index: 2,
85                     corresponding_property: "tax_id",
86                     label: _t("Tax"),
87                     required: false,
88                     tabindex: 12,
89                     constructor: instance.web.form.FieldMany2One,
90                     field_properties: {
91                         relation: "account.tax",
92                         string: _t("Tax"),
93                         type: "many2one",
94                         domain: [['type_tax_use','in',['purchase', 'all']], ['parent_id', '=', false]],
95                     },
96                 },
97                 amount: {
98                     id: "amount",
99                     index: 3,
100                     corresponding_property: "amount",
101                     label: _t("Amount"),
102                     required: true,
103                     tabindex: 13,
104                     constructor: instance.web.form.FieldFloat,
105                     field_properties: {
106                         string: _t("Amount"),
107                         type: "float",
108                     },
109                 },
110                 analytic_account_id: {
111                     id: "analytic_account_id",
112                     index: 4,
113                     corresponding_property: "analytic_account_id",
114                     label: _t("Analytic Acc."),
115                     required: false,
116                     tabindex: 14,
117                     group:"analytic.group_analytic_accounting",
118                     constructor: instance.web.form.FieldMany2One,
119                     field_properties: {
120                         relation: "account.analytic.account",
121                         string: _t("Analytic Acc."),
122                         type: "many2one",
123                     },
124                 },
125             };
126         },
127     
128         start: function() {
129             this._super();
130             var self = this;
131             // Retreive statement infos and reconciliation data from the model
132             var lines_filter = [['journal_entry_id', '=', false], ['account_id', '=', false]];
133             var deferred_promises = [];
134             
135             // Working on specified statement(s)
136             if (self.statement_ids && self.statement_ids.length > 0) {
137                 lines_filter.push(['statement_id', 'in', self.statement_ids]);
138
139                 // If only one statement, display its name as title and allow to modify it
140                 if (self.single_statement) {
141                     deferred_promises.push(self.model_bank_statement
142                         .query(["name"])
143                         .filter([['id', '=', self.statement_ids[0]]])
144                         .first()
145                         .then(function(title){
146                             self.title = title.name;
147                         })
148                     );
149                 }
150                 // Anyway, find out how many statement lines are reconciled (for the progressbar)
151                 deferred_promises.push(self.model_bank_statement
152                     .call("number_of_lines_reconciled", [self.statement_ids])
153                     .then(function(num) {
154                         self.already_reconciled_lines = num;
155                     })
156                 );
157             }
158             
159             // Get operation templates
160             deferred_promises.push(new instance.web.Model("account.statement.operation.template")
161                 .query(['id','name','account_id','label','amount_type','amount','tax_id','analytic_account_id'])
162                 .all().then(function (data) {
163                     _(data).each(function(preset){
164                         self.presets[preset.id] = preset;
165                     });
166                 })
167             );
168
169             // Get the function to format currencies
170             deferred_promises.push(new instance.web.Model("res.currency")
171                 .call("get_format_currencies_js_function")
172                 .then(function(data) {
173                     self.formatCurrency = new Function("amount, currency_id", data);
174                 })
175             );
176     
177             // Get statement lines
178             deferred_promises.push(self.model_bank_statement_line
179                 .query(['id'])
180                 .filter(lines_filter)
181                 .order_by('statement_id, id')
182                 .all().then(function (data) {
183                     self.st_lines = _(data).map(function(o){ return o.id });
184                 })
185             );
186     
187             // When queries are done, render template and reconciliation lines
188             return $.when.apply($, deferred_promises).then(function(){
189     
190                 // If there is no statement line to reconcile, stop here
191                 if (self.st_lines.length === 0) {
192                     self.$el.prepend(QWeb.render("bank_statement_nothing_to_reconcile"));
193                     return;
194                 }
195     
196                 // Create a dict account id -> account code for display facilities
197                 new instance.web.Model("account.account")
198                     .query(['id', 'code'])
199                     .all().then(function(data) {
200                         _.each(data, function(o) { self.map_account_id_code[o.id] = o.code });
201                     });
202
203                 // Create a dict currency id -> rounding factor
204                 new instance.web.Model("res.currency")
205                     .query(['id', 'rounding'])
206                     .all().then(function(data) {
207                         _.each(data, function(o) { self.map_currency_id_rounding[o.id] = o.rounding });
208                     });
209
210                 // Create a dict tax id -> amount
211                 new instance.web.Model("account.tax")
212                     .query(['id', 'amount'])
213                     .all().then(function(data) {
214                         _.each(data, function(o) { self.map_tax_id_amount[o.id] = o.amount });
215                     });
216             
217                 new instance.web.Model("ir.model.data")
218                     .call("xmlid_to_res_id", ["account.menu_bank_reconcile_bank_statements"])
219                     .then(function(data) {
220                         self.reconciliation_menu_id = data;
221                         self.doReloadMenuReconciliation();
222                     });
223
224                 // Bind keyboard events TODO : méthode standard ?
225                 $("body").on("keypress", function (e) {
226                     self.keyboardShortcutsHandler(e);
227                 });
228     
229                 // Render and display
230                 self.$el.prepend(QWeb.render("bank_statement_reconciliation", {
231                     title: self.title,
232                     single_statement: self.single_statement,
233                     total_lines: self.already_reconciled_lines+self.st_lines.length
234                 }));
235                 self.updateProgressbar();
236                 var reconciliations_to_show = self.st_lines.slice(0, self.max_reconciliations_displayed);
237                 self.last_displayed_reconciliation_index = reconciliations_to_show.length;
238                 self.$(".reconciliation_lines_container").css("opacity", 0);
239     
240                 // Display the reconciliations
241                 return self.model_bank_statement_line
242                     .call("get_data_for_reconciliations", [reconciliations_to_show])
243                     .then(function (data) {
244                         var child_promises = [];
245                         while ((datum = data.shift()) !== undefined)
246                             child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition));
247                         $.when.apply($, child_promises).then(function(){
248                             self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed);
249                             self.getChildren()[0].set("mode", "match");
250                         });
251                     });
252             });
253         },
254
255         statementNameClickHandler: function() {
256             if (! this.single_statement) return;
257             this.$(".statement_name span").hide();
258             this.$(".change_statement_name_field").attr("value", this.title);
259             this.$(".change_statement_name_container").show();
260             this.$(".change_statement_name_field").focus();
261         },
262
263         changeStatementNameFieldHandler: function(e) {
264             var name = this.$(".change_statement_name_field").val();
265             if (name === "") this.$(".change_statement_name_button").attr("disabled", "disabled");
266             else this.$(".change_statement_name_button").removeAttr("disabled");
267             
268             if (name !== "" && e.which === 13) // Enter
269                 this.$(".change_statement_name_button").trigger("click");
270             if (e.which === 27) { // Escape
271                 this.$(".statement_name span").show();
272                 this.$(".change_statement_name_container").hide();
273             }
274         },
275
276         changeStatementButtonClickHandler: function() {
277             var self = this;
278             if (! self.single_statement) return;
279             var name = self.$(".change_statement_name_field").val();
280             if (name === "") return;
281             self.$(".change_statement_name_button").attr("disabled", "disabled");
282             return self.model_bank_statement
283                 .call("write", [[self.statement_ids[0]], {'name': name}])
284                 .done(function () {
285                     self.title = name;
286                     self.$(".statement_name span").text(name).show();
287                     self.$(".change_statement_name_container").hide();
288                 }).always(function() {
289                     self.$(".change_statement_name_button").removeAttr("disabled");
290                 });
291         },
292     
293         keyboardShortcutsHandler: function(e) {
294             var self = this;
295             if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) {
296                 self.persistReconciliations(_.filter(self.getChildren(), function(o) { return o.is_valid; }));
297             }
298         },
299
300         persistReconciliations: function(reconciliations) {
301             if (reconciliations.length === 0) return;
302             var self = this;
303             // Prepare data
304             var data = [];
305             for (var i=0; i<reconciliations.length; i++) {
306                 var child = reconciliations[i];
307                 data.push([child.st_line_id, child.makeMoveLineDicts()]);
308             }
309             var deferred_animation = self.$(".reconciliation_lines_container").fadeOut(self.aestetic_animation_speed);
310             deferred_rpc = self.model_bank_statement_line.call("process_reconciliations", [data]);
311             return $.when(deferred_animation, deferred_rpc)
312                 .done(function() {
313                     // Remove children
314                     for (var i=0; i<reconciliations.length; i++) {
315                         var child = reconciliations[i];
316                         self.unexcludeMoveLines(child, child.partner_id, child.get("mv_lines_selected"));
317                         $.each(child.$(".bootstrap_popover"), function(){ $(this).popover('destroy') });
318                         child.destroy();
319                     }
320                     // Update interface
321                     self.lines_reconciled_with_ctrl_enter += reconciliations.length;
322                     self.reconciled_lines += reconciliations.length;
323                     self.updateProgressbar();
324                     self.doReloadMenuReconciliation();
325
326                     // Display new line if there are left
327                     if (self.last_displayed_reconciliation_index < self.st_lines.length) {
328                         var begin = self.last_displayed_reconciliation_index;
329                         var end = Math.min((begin+self.max_reconciliations_displayed), self.st_lines.length);
330                         var reconciliations_to_show = self.st_lines.slice(begin, end);
331
332                         return self.model_bank_statement_line
333                             .call("get_data_for_reconciliations", [reconciliations_to_show])
334                             .then(function (data) {
335                                 var child_promises = [];
336                                 var datum;
337                                 while ((datum = data.shift()) !== undefined) {
338                                     var context = {
339                                         st_line_id: datum.st_line.id,
340                                         mode: 'inactive',
341                                         animate_entrance: false,
342                                         initial_data_provided: true,
343                                         st_line: datum.st_line,
344                                         reconciliation_proposition: datum.reconciliation_proposition,
345                                     };
346                                     var widget = new instance.web.account.bankStatementReconciliationLine(self, context);
347                                     child_promises.push(widget.appendTo(self.$(".reconciliation_lines_container")));
348                                 }
349                                 self.last_displayed_reconciliation_index += reconciliations_to_show.length;
350                                 return $.when.apply($, child_promises).then(function() {
351                                     // Put the first line in match mode
352                                     if (self.reconciled_lines !== self.st_lines.length) {
353                                         var first_child = self.getChildren()[0];
354                                         if (first_child.get("mode") === "inactive") {
355                                             first_child.set("mode", "match");
356                                         }
357                                     }
358                                     self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
359                                 });
360                             });
361                     } else if (self.reconciled_lines === self.st_lines.length) {
362                         // Congratulate the user if the work is done
363                         self.displayDoneMessage();
364                     } else {
365                         // Some lines weren't persisted because they were't valid
366                         self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
367                     }
368                 }).fail(function() {
369                     self.$(".reconciliation_lines_container").fadeIn(self.aestetic_animation_speed);
370                 });
371         },
372
373         // Adds move line ids to the list of move lines not to fetch for a given partner
374         // This is required because the same move line cannot be selected for multiple reconciliation
375         // and because for a partial reconciliation only one line can be fetched)
376         excludeMoveLines: function(source_child, partner_id, lines) {
377             var self = this;
378             var line_ids = _.collect(lines, function(o) { return o.id });
379         
380             var excluded_ids = this.excluded_move_lines_ids[partner_id];
381             var excluded_move_lines_changed = false;
382             _.each(line_ids, function(line_id){
383                 if (excluded_ids.indexOf(line_id) === -1) {
384                     excluded_ids.push(line_id);
385                     excluded_move_lines_changed = true;
386                 }
387             });
388             if (! excluded_move_lines_changed)
389                 return;
390         
391             // Function that finds if an array of line objects contains at least a line identified by its id
392             var contains_lines = function(lines_array, line_ids) {
393                 for (var i = 0; i < lines_array.length; i++)
394                     for (var j = 0; j < line_ids.length; j++)
395                         if (lines_array[i].id === line_ids[j])
396                             return true;
397                 return false;
398             };
399         
400             // Update children if needed
401             _.each(self.getChildren(), function(child){
402                 if ((child.partner_id === partner_id || child.st_line.has_no_partner) && child !== source_child) {
403                     if (contains_lines(child.get("mv_lines_selected"), line_ids)) {
404                         child.set("mv_lines_selected", _.filter(child.get("mv_lines_selected"), function(o){ return line_ids.indexOf(o.id) === -1 }));
405                     } else if (contains_lines(child.mv_lines_deselected, line_ids)) {
406                         child.mv_lines_deselected = _.filter(child.mv_lines_deselected, function(o){ return line_ids.indexOf(o.id) === -1 });
407                         child.updateMatches();
408                     } else if (contains_lines(child.get("mv_lines"), line_ids)) {
409                         child.updateMatches();
410                     }
411                 }
412             });
413         },
414         
415         unexcludeMoveLines: function(source_child, partner_id, lines) {
416             var self = this;
417             var line_ids = _.collect(lines, function(o) { return o.id });
418
419             var initial_excluded_lines_num = this.excluded_move_lines_ids[partner_id].length;
420             this.excluded_move_lines_ids[partner_id] = _.difference(this.excluded_move_lines_ids[partner_id], line_ids);
421             if (this.excluded_move_lines_ids[partner_id].length === initial_excluded_lines_num)
422                 return;
423         
424             // Update children if needed
425             _.each(self.getChildren(), function(child){
426                 if (child.partner_id === partner_id && child !== source_child && (child.get("mode") === "match" || child.$el.hasClass("no_match")))
427                     child.updateMatches();
428                 if (child.st_line.has_no_partner && child.get("mode") === "match" || child.$el.hasClass("no_match"))
429                     child.updateMatches();
430             });
431         },
432     
433         displayReconciliation: function(st_line_id, mode, animate_entrance, initial_data_provided, st_line, reconciliation_proposition) {
434             var self = this;
435             animate_entrance = (animate_entrance === undefined ? true : animate_entrance);
436             initial_data_provided = (initial_data_provided === undefined ? false : initial_data_provided);
437     
438             var context = {
439                 st_line_id: st_line_id,
440                 mode: mode,
441                 animate_entrance: animate_entrance,
442                 initial_data_provided: initial_data_provided,
443                 st_line: initial_data_provided ? st_line : undefined,
444                 reconciliation_proposition: initial_data_provided ? reconciliation_proposition : undefined,
445             };
446             var widget = new instance.web.account.bankStatementReconciliationLine(self, context);
447             return widget.appendTo(self.$(".reconciliation_lines_container"));
448         },
449     
450         childValidated: function(child) {
451             var self = this;
452     
453             self.reconciled_lines++;
454             self.updateProgressbar();
455             self.doReloadMenuReconciliation();
456     
457             // Display new line if there are left
458             if (self.last_displayed_reconciliation_index < self.st_lines.length) {
459                 self.displayReconciliation(self.st_lines[self.last_displayed_reconciliation_index++], 'inactive');
460             }
461             // Congratulate the user if the work is done
462             if (self.reconciled_lines === self.st_lines.length) {
463                 self.displayDoneMessage();
464             }
465         
466             // Put the first line in match mode
467             if (self.reconciled_lines !== self.st_lines.length) {
468                 var first_child = self.getChildren()[0];
469                 if (first_child.get("mode") === "inactive") {
470                     first_child.set("mode", "match");
471                 }
472             }
473         },
474
475         goBackToStatementsTreeView: function() {
476             var self = this;
477             new instance.web.Model("ir.model.data")
478                 .call("get_object_reference", ['account', 'action_bank_statement_tree'])
479                 .then(function (result) {
480                     var action_id = result[1];
481                     // Warning : altough I don't see why this widget wouldn't be directly instanciated by the
482                     // action manager, if it wasn't, this code wouldn't work. You'd have to do something like :
483                     // var action_manager = self;
484                     // while (! action_manager instanceof ActionManager)
485                     //    action_manager = action_manager.getParent();
486                     var action_manager = self.getParent();
487                     var breadcrumbs = action_manager.breadcrumbs;
488                     var found = false;
489                     for (var i=breadcrumbs.length-1; i>=0; i--) {
490                         if (breadcrumbs[i].action && breadcrumbs[i].action.id === action_id) {
491                             var title = breadcrumbs[i].get_title();
492                             action_manager.select_breadcrumb(i, _.isArray(title) ? i : undefined);
493                             found = true;
494                         }
495                     }
496                     if (!found)
497                         instance.web.Home(self);
498                 });
499         },
500     
501         displayDoneMessage: function() {
502             var self = this;
503     
504             var sec_taken = Math.round((Date.now()-self.time_widget_loaded)/1000);
505             var sec_per_item = Math.round(sec_taken/self.reconciled_lines);
506             var achievements = [];
507     
508             var time_taken;
509             if (sec_taken/60 >= 1) time_taken = Math.floor(sec_taken/60) +"' "+ sec_taken%60 +"''";
510             else time_taken = sec_taken%60 +" seconds";
511     
512             var title;
513             if (sec_per_item < 5) title = _t("Whew, that was fast !") + " <i class='fa fa-trophy congrats_icon'></i>";
514             else title = _t("Congrats, you're all done !") + " <i class='fa fa-thumbs-o-up congrats_icon'></i>";
515     
516             if (self.lines_reconciled_with_ctrl_enter === self.reconciled_lines)
517                 achievements.push({
518                     title: _t("Efficiency at its finest"),
519                     desc: _t("Only use the ctrl-enter shortcut to validate reconciliations."),
520                     icon: "fa-keyboard-o"}
521                 );
522     
523             if (sec_per_item < 5)
524                 achievements.push({
525                     title: _t("Fast reconciler"),
526                     desc: _t("Take on average less than 5 seconds to reconcile a transaction."),
527                     icon: "fa-bolt"}
528                 );
529     
530             // Render it
531             self.$(".protip").hide();
532             self.$(".oe_form_sheet").append(QWeb.render("bank_statement_reconciliation_done_message", {
533                 title: title,
534                 time_taken: time_taken,
535                 sec_per_item: sec_per_item,
536                 transactions_done: self.reconciled_lines,
537                 done_with_ctrl_enter: self.lines_reconciled_with_ctrl_enter,
538                 achievements: achievements,
539                 single_statement: self.single_statement,
540                 multiple_statements: self.multiple_statements,
541             }));
542     
543             // Animate it
544             var container = $("<div style='overflow: hidden;' />");
545             self.$(".done_message").wrap(container).css("opacity", 0).css("position", "relative").css("left", "-50%");
546             self.$(".done_message").animate({opacity: 1, left: 0}, self.aestetic_animation_speed*2, "easeOutCubic");
547             self.$(".done_message").animate({opacity: 1}, self.aestetic_animation_speed*3, "easeOutCubic");
548     
549             // Make it interactive
550             self.$(".achievement").popover({'placement': 'top', 'container': self.el, 'trigger': 'hover'});
551
552             if (self.$(".button_back_to_statement").length !== 0) {
553                 self.$(".button_back_to_statement").click(function() {
554                     self.goBackToStatementsTreeView();
555                 });
556             }
557
558             if (self.$(".button_close_statement").length !== 0) {
559                 self.$(".button_close_statement").hide();
560                 self.model_bank_statement
561                     .query(["balance_end_real", "balance_end"])
562                     .filter([['id', 'in', self.statement_ids]])
563                     .all()
564                     .then(function(data){
565                         if (_.all(data, function(o) { return o.balance_end_real === o.balance_end })) {
566                             self.$(".button_close_statement").show();
567                             self.$(".button_close_statement").click(function() {
568                                 self.$(".button_close_statement").attr("disabled", "disabled");
569                                 self.model_bank_statement
570                                     .call("button_confirm_bank", [self.statement_ids])
571                                     .then(function () {
572                                         self.goBackToStatementsTreeView();
573                                     }, function() {
574                                         self.$(".button_close_statement").removeAttr("disabled");
575                                     });
576                             });
577                         }
578                     });
579             }
580         },
581     
582         updateProgressbar: function() {
583             var self = this;
584             var done = self.already_reconciled_lines + self.reconciled_lines;
585             var total = self.already_reconciled_lines + self.st_lines.length;
586             var prog_bar = self.$(".progress .progress-bar");
587             prog_bar.attr("aria-valuenow", done);
588             prog_bar.css("width", (done/total*100)+"%");
589             self.$(".progress .progress-text .valuenow").text(done);
590         },
591     
592         /* reloads the needaction badge */
593         doReloadMenuReconciliation: function () {
594             var menu = instance.webclient.menu;
595             if (!menu || !this.reconciliation_menu_id) {
596                 return $.when();
597             }
598             return menu.rpc("/web/menu/load_needaction", {'menu_ids': [this.reconciliation_menu_id]}).done(function(r) {
599                 menu.on_needaction_loaded(r);
600             }).then(function () {
601                 menu.trigger("need_action_reloaded");
602             });
603         },
604     });
605     
606     instance.web.account.bankStatementReconciliationLine = instance.web.Widget.extend({
607         className: 'oe_bank_statement_reconciliation_line',
608     
609         events: {
610             "click .change_partner": "changePartnerClickHandler",
611             "click .button_ok": "persistAndDestroy",
612             "click .mv_line": "moveLineClickHandler",
613             "click .initial_line": "initialLineClickHandler",
614             "click .line_open_balance": "lineOpenBalanceClickHandler",
615             "click .pager_control_left:not(.disabled)": "pagerControlLeftHandler",
616             "click .pager_control_right:not(.disabled)": "pagerControlRightHandler",
617             "keyup .filter": "filterHandler",
618             "click .line_info_button": function(e){e.stopPropagation()}, // small usability hack
619             "click .add_line": "addLineBeingEdited",
620             "click .preset": "presetClickHandler",
621             "click .do_partial_reconcile_button": "doPartialReconcileButtonClickHandler",
622             "click .undo_partial_reconcile_button": "undoPartialReconcileButtonClickHandler",
623         },
624     
625         init: function(parent, context) {
626             this._super(parent);
627     
628             this.formatCurrency = this.getParent().formatCurrency;
629             if (context.initial_data_provided) {
630                 // Process data
631                 _.each(context.reconciliation_proposition, function(line) {
632                     this.decorateMoveLine(line, context.st_line.currency_id);
633                 }, this);
634                 this.set("mv_lines_selected", context.reconciliation_proposition);
635                 this.st_line = context.st_line;
636                 this.partner_id = context.st_line.partner_id;
637                 this.decorateStatementLine(this.st_line);
638     
639                 // Exclude selected move lines
640                 if (this.getParent().excluded_move_lines_ids[this.partner_id] === undefined)
641                     this.getParent().excluded_move_lines_ids[this.partner_id] = [];
642                 this.getParent().excludeMoveLines(this, this.partner_id, context.reconciliation_proposition);
643             } else {
644                 this.set("mv_lines_selected", []);
645                 this.st_line = undefined;
646                 this.partner_id = undefined;
647             }
648     
649             this.context = context;
650             this.st_line_id = context.st_line_id;
651             this.max_move_lines_displayed = this.getParent().max_move_lines_displayed;
652             this.animation_speed = this.getParent().animation_speed;
653             this.aestetic_animation_speed = this.getParent().aestetic_animation_speed;
654             this.model_bank_statement_line = new instance.web.Model("account.bank.statement.line");
655             this.model_res_users = new instance.web.Model("res.users");
656             this.model_tax = new instance.web.Model("account.tax");
657             this.map_currency_id_rounding = this.getParent().map_currency_id_rounding;
658             this.map_account_id_code = this.getParent().map_account_id_code;
659             this.map_tax_id_amount = this.getParent().map_tax_id_amount;
660             this.presets = this.getParent().presets;
661             this.is_valid = true;
662             this.is_consistent = true; // Used to prevent bad server requests
663             this.can_fetch_more_move_lines; // Tell if we can show more move lines
664             this.filter = "";
665             // In rare cases like when deleting a statement line's partner we don't want the server to
666             // look for a reconciliation proposition (in this particular case it might find a move line
667             // matching the statement line and decide to set the statement line's partner accordingly)
668             this.do_load_reconciliation_proposition = true;
669     
670             this.set("mode", undefined);
671             this.on("change:mode", this, this.modeChanged);
672             this.set("balance", undefined); // Debit is +, credit is -
673             this.on("change:balance", this, this.balanceChanged);
674             this.set("pager_index", 0);
675             this.on("change:pager_index", this, this.pagerChanged);
676             // NB : mv_lines represent the counterpart that will be created to reconcile existing move lines, so debit and credit are inverted
677             this.set("mv_lines", []);
678             this.on("change:mv_lines", this, this.mvLinesChanged);
679             this.mv_lines_deselected = []; // deselected lines are displayed on top of the match table
680             this.on("change:mv_lines_selected", this, this.mvLinesSelectedChanged);
681             this.set("lines_created", []);
682             this.set("line_created_being_edited", [{'id': 0}]);
683             this.on("change:lines_created", this, this.createdLinesChanged);
684             this.on("change:line_created_being_edited", this, this.createdLinesChanged);
685         },
686     
687         start: function() {
688             var self = this;
689             return self._super().then(function() {
690                 // no animation while loading
691                 self.animation_speed = 0;
692                 self.aestetic_animation_speed = 0;
693     
694                 self.is_consistent = false;
695                 if (self.context.animate_entrance) {
696                     self.$el.fadeOut(0);
697                     self.$el.slideUp(0);
698                 }
699                 return $.when(self.loadData()).then(function(){
700                     return $.when(self.render()).then(function(){
701                         self.is_consistent = true;
702                         // Make an entrance
703                         self.animation_speed = self.getParent().animation_speed;
704                         self.aestetic_animation_speed = self.getParent().aestetic_animation_speed;
705                         if (self.context.animate_entrance) {
706                             return self.$el.stop(true, true).fadeIn({ duration: self.aestetic_animation_speed, queue: false }).css('display', 'none').slideDown(self.aestetic_animation_speed); 
707                         }
708                     });
709                 });
710             });
711         },
712
713         loadData: function() {
714             var self = this;
715             if (self.context.initial_data_provided)
716                 return;
717
718             // Get ids of selected move lines (to exclude them from reconciliation proposition)
719             var excluded_move_lines_ids = [];
720             if (self.do_load_reconciliation_proposition) {
721                 _.each(self.getParent().excluded_move_lines_ids, function(o){
722                     excluded_move_lines_ids = excluded_move_lines_ids.concat(o);
723                 });
724             }
725             // Load statement line
726             return self.model_bank_statement_line
727                 .call("get_data_for_reconciliations", [[self.st_line_id], excluded_move_lines_ids, self.do_load_reconciliation_proposition])
728                 .then(function (data) {
729                     self.st_line = data[0].st_line;
730                     self.decorateStatementLine(self.st_line);
731                     self.partner_id = data[0].st_line.partner_id;
732                     if (self.getParent().excluded_move_lines_ids[self.partner_id] === undefined)
733                         self.getParent().excluded_move_lines_ids[self.partner_id] = [];
734                     var mv_lines = [];
735                     _.each(data[0].reconciliation_proposition, function(line) {
736                         self.decorateMoveLine(line, self.st_line.currency_id);
737                         mv_lines.push(line);
738                     }, self);
739                     self.set("mv_lines_selected", self.get("mv_lines_selected").concat(mv_lines));
740                 });
741         },
742
743         render: function() {
744             var self = this;
745             var presets_array = [];
746             for (var id in self.presets)
747                 if (self.presets.hasOwnProperty(id))
748                     presets_array.push(self.presets[id]);
749             self.$el.prepend(QWeb.render("bank_statement_reconciliation_line", {
750                 line: self.st_line,
751                 mode: self.context.mode,
752                 presets: presets_array
753             }));
754             
755             // Stuff that require the template to be rendered
756             self.$(".match").slideUp(0);
757             self.$(".create").slideUp(0);
758             if (self.st_line.no_match) self.$el.addClass("no_match");
759             self.bindPopoverTo(self.$(".line_info_button"));
760             self.createFormWidgets();
761             // Special case hack : no identified partner
762             if (self.st_line.has_no_partner) {
763                 self.$el.css("opacity", "0");
764                 self.updateBalance();
765                 self.$(".change_partner_container").show(0);
766                 self.$(".match").slideUp(0);
767                 self.$el.addClass("no_partner");
768                 self.set("mode", self.context.mode);
769                 self.balanceChanged();
770                 self.updateAccountingViewMatchedLines();
771                 self.animation_speed = self.getParent().animation_speed;
772                 self.aestetic_animation_speed = self.getParent().aestetic_animation_speed;
773                 self.$el.animate({opacity: 1}, self.aestetic_animation_speed);
774                 return;
775             }
776             
777             // TODO : the .on handler's returned deferred is lost
778             return $.when(self.set("mode", self.context.mode)).then(function(){
779                 // Make sure the display is OK
780                 self.balanceChanged();
781                 self.createdLinesChanged();
782                 self.updateAccountingViewMatchedLines();
783             });
784         },
785
786         restart: function(mode) {
787             var self = this;
788             mode = (mode === undefined ? 'inactive' : mode);
789             self.context.animate_entrance = false;
790             self.$el.css("height", self.$el.outerHeight());
791             // Destroy everything
792             _.each(self.getChildren(), function(o){ o.destroy() });
793             self.is_consistent = false;
794             return $.when(self.$el.animate({opacity: 0}, self.animation_speed)).then(function() {
795                 self.getParent().unexcludeMoveLines(self, self.partner_id, self.get("mv_lines_selected"));
796                 $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') });
797                 self.$el.empty();
798                 self.$el.removeClass("no_partner");
799                 self.context.mode = mode;
800                 self.context.initial_data_provided = false;
801                 self.is_valid = true;
802                 self.is_consistent = true;
803                 self.filter = "";
804                 self.set("balance", undefined, {silent: true});
805                 self.set("mode", undefined, {silent: true});
806                 self.set("pager_index", 0, {silent: true});
807                 self.set("mv_lines", [], {silent: true});
808                 self.set("mv_lines_selected", [], {silent: true});
809                 self.mv_lines_deselected = [];
810                 self.set("lines_created", [], {silent: true});
811                 self.set("line_created_being_edited", [{'id': 0}], {silent: true});
812                 // Rebirth
813                 return $.when(self.start()).then(function() {
814                     self.$el.css("height", "auto");
815                     self.is_consistent = true;
816                     self.$el.animate({opacity: 1}, self.animation_speed);
817                 });
818             });
819         },
820     
821         /* create form widgets, append them to the dom and bind their events handlers */
822         createFormWidgets: function() {
823             var self = this;
824             var create_form_fields = self.getParent().create_form_fields;
825             var create_form_fields_arr = [];
826             for (var key in create_form_fields)
827                 if (create_form_fields.hasOwnProperty(key))
828                     create_form_fields_arr.push(create_form_fields[key]);
829             create_form_fields_arr.sort(function(a, b){ return b.index - a.index });
830     
831             // field_manager
832             var dataset = new instance.web.DataSet(this, "account.account", self.context);
833             dataset.ids = [];
834             dataset.arch = {
835                 attrs: { string: "Stéphanie de Monaco", version: "7.0", class: "oe_form_container" },
836                 children: [],
837                 tag: "form"
838             };
839     
840             var field_manager = new instance.web.FormView (
841                 this, dataset, false, {
842                     initial_mode: 'edit',
843                     disable_autofocus: false,
844                     $buttons: $(),
845                     $pager: $()
846             });
847     
848             field_manager.load_form(dataset);
849     
850             // fields default properties
851             var Default_field = function() {
852                 this.context = {};
853                 this.domain = [];
854                 this.help = "";
855                 this.readonly = false;
856                 this.required = true;
857                 this.selectable = true;
858                 this.states = {};
859                 this.views = {};
860             };
861             var Default_node = function(field_name) {
862                 this.tag = "field";
863                 this.children = [];
864                 this.required = true;
865                 this.attrs = {
866                     invisible: "False",
867                     modifiers: '{"required":true}',
868                     name: field_name,
869                     nolabel: "True",
870                 };
871             };
872     
873             // Append fields to the field_manager
874             field_manager.fields_view.fields = {};
875             for (var i=0; i<create_form_fields_arr.length; i++) {
876                 field_manager.fields_view.fields[create_form_fields_arr[i].id] = _.extend(new Default_field(), create_form_fields_arr[i].field_properties);
877             }
878             field_manager.fields_view.fields["change_partner"] = _.extend(new Default_field(), {
879                 relation: "res.partner",
880                 string: _t("Partner"),
881                 type: "many2one",
882                 domain: [['parent_id','=',false], '|', ['customer','=',true], ['supplier','=',true]],
883             });
884     
885             // Returns a function that serves as a xhr response handler
886             var hideGroupResponseClosureFactory = function(field_widget, $container, obj_key){
887                 return function(has_group){
888                     if (has_group) $container.show();
889                     else {
890                         field_widget.destroy();
891                         $container.remove();
892                         delete self[obj_key];
893                     }
894                 };
895             };
896     
897             // generate the create "form"
898             self.create_form = [];
899             for (var i=0; i<create_form_fields_arr.length; i++) {
900                 var field_data = create_form_fields_arr[i];
901     
902                 // create widgets
903                 var node = new Default_node(field_data.id);
904                 if (! field_data.required) node.attrs.modifiers = "";
905                 var field = new field_data.constructor(field_manager, node);
906                 self[field_data.id+"_field"] = field;
907                 self.create_form.push(field);
908     
909                 // on update : change the last created line
910                 field.corresponding_property = field_data.corresponding_property;
911                 field.on("change:value", self, self.formCreateInputChanged);
912     
913                 // append to DOM
914                 var $field_container = $(QWeb.render("form_create_field", {id: field_data.id, label: field_data.label}));
915                 field.appendTo($field_container.find("td"));
916                 self.$(".create_form").prepend($field_container);
917     
918                 // now that widget's dom has been created (appendTo does that), bind events and adds tabindex
919                 if (field_data.field_properties.type != "many2one") {
920                     // Triggers change:value TODO : moche bind ?
921                     field.$el.find("input").keyup(function(e, field){ field.commit_value(); }.bind(null, null, field));
922                 }
923                 field.$el.find("input").attr("tabindex", field_data.tabindex);
924     
925                 // Hide the field if group not OK
926                 if (field_data.group !== undefined) {
927                     var target = $field_container;
928                     target.hide();
929                     self.model_res_users
930                         .call("has_group", [field_data.group])
931                         .then(hideGroupResponseClosureFactory(field, target, (field_data.id+"_field")));
932                 }
933             }
934     
935             // generate the change partner "form"
936             var change_partner_node = new Default_node("change_partner"); change_partner_node.attrs.modifiers = "";
937             self.change_partner_field = new instance.web.form.FieldMany2One(field_manager, change_partner_node);
938             self.change_partner_field.appendTo(self.$(".change_partner_container"));
939             self.change_partner_field.on("change:value", self.change_partner_field, function() {
940                 self.changePartner(this.get_value());
941             });
942             self.change_partner_field.$el.find("input").attr("placeholder", self.st_line.communication_partner_name || _t("Select Partner"));
943     
944             field_manager.do_show();
945         },
946     
947         /** Utils */
948     
949         /* TODO : if t-call for attr, all in qweb */
950         decorateStatementLine: function(line){
951             line.q_popover = QWeb.render("bank_statement_reconciliation_line_details", {line: line});
952         },
953     
954         // adds fields, prefixed with q_, to the move line for qweb rendering
955         decorateMoveLine: function(line, currency_id) {
956             line.partial_reconcile = false;
957             line.propose_partial_reconcile = false;
958             line['credit'] = [line['debit'], line['debit'] = line['credit']][0];
959             line.q_due_date = (line.date_maturity === false ? line.date : line.date_maturity);
960             line.q_amount = (line.debit !== 0 ? "- "+line.q_debit : "") + (line.credit !== 0 ? line.q_credit : "");
961             line.q_label = line.name;
962             line.debit_str = this.formatCurrency(line.debit, currency_id);
963             line.credit_str = this.formatCurrency(line.credit, currency_id);
964             line.q_popover = QWeb.render("bank_statement_reconciliation_move_line_details", {line: line});
965             if (line.has_no_partner)
966                 line.q_label = line.partner_name + ': ' + line.q_label;
967             if (line.ref && line.ref !== line.name)
968                 line.q_label += " : " + line.ref;
969         },
970     
971         bindPopoverTo: function(el) {
972             var self = this;
973             $(el).addClass("bootstrap_popover");
974             el.popover({
975                 'placement': 'left',
976                 'container': self.el,
977                 'html': true,
978                 'trigger': 'hover',
979                 'animation': false,
980                 'toggle': 'popover'
981             });
982         },
983     
984         islineCreatedBeingEditedValid: function() {
985             var line = this.get("line_created_being_edited")[0];
986             return line.amount // must be defined and not 0
987                 && line.account_id // must be defined (and will never be 0)
988                 && line.label; // must be defined and not empty
989         },
990     
991         /* returns the created lines, plus the ones being edited if valid */
992         getCreatedLines: function() {
993             var self = this;
994             var created_lines = self.get("lines_created").slice();
995             if (self.islineCreatedBeingEditedValid())
996                 return created_lines.concat(self.get("line_created_being_edited"));
997             else
998                 return created_lines;
999         },
1000     
1001         /** Matching */
1002     
1003         moveLineClickHandler: function(e) {
1004             var self = this;
1005             if (e.currentTarget.dataset.selected === "true") self.deselectMoveLine(e.currentTarget);
1006             else self.selectMoveLine(e.currentTarget);
1007         },
1008
1009         selectMoveLine: function(mv_line) {
1010             var self = this;
1011             var line_id = mv_line.dataset.lineid;
1012
1013             // find the line in mv_lines or mv_lines_deselected
1014             var line = _.find(self.get("mv_lines"), function(o){ return o.id == line_id});
1015             if (! line) {
1016                 line = _.find(self.mv_lines_deselected, function(o){ return o.id == line_id });
1017                 self.mv_lines_deselected = _.filter(self.mv_lines_deselected, function(o) { return o.id != line_id });
1018             }
1019             if (! line) return; // If no line found, we've got a syncing problem (let's turn a deaf ear)
1020
1021             // Warn the user if he's selecting lines from both a payable and a receivable account
1022             var last_selected_line = _.last(self.get("mv_lines_selected"));
1023             if (last_selected_line && last_selected_line.account_type != line.account_type) {
1024                 new instance.web.Dialog(this, {
1025                     title: _t("Warning"),
1026                     size: 'medium',
1027                 }, $("<div />").text(_.str.sprintf(_t("You are selecting transactions from both a payable and a receivable account.\n\nIn order to proceed, you first need to deselect the %s transactions."), last_selected_line.account_type))).open();
1028                 return;
1029             }
1030
1031             self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line));
1032         },
1033
1034         deselectMoveLine: function(mv_line) {
1035             var self = this;
1036             var line_id = mv_line.dataset.lineid;
1037             var line = _.find(self.get("mv_lines_selected"), function(o){ return o.id == line_id});
1038             if (! line) return; // If no line found, we've got a syncing problem (let's turn a deaf ear)
1039
1040             // add the line to mv_lines_deselected and remove it from mv_lines_selected
1041             self.mv_lines_deselected.unshift(line);
1042             var mv_lines_selected = _.filter(self.get("mv_lines_selected"), function(o) { return o.id != line_id });
1043             
1044             // remove partial reconciliation stuff if necessary
1045             if (line.partial_reconcile === true) self.unpartialReconcileLine(line);
1046             if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false;
1047             
1048             self.$el.removeClass("no_match");
1049             self.set("mode", "match");
1050             self.set("mv_lines_selected", mv_lines_selected);
1051         },
1052     
1053         /** Matches pagination */
1054     
1055         pagerControlLeftHandler: function() {
1056             var self = this;
1057             if (self.$(".pager_control_left").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ }
1058             if (self.get("pager_index") === 0) { return; }
1059             self.set("pager_index", self.get("pager_index")-1 );
1060         },
1061         
1062         pagerControlRightHandler: function() {
1063             var self = this;
1064             if (self.$(".pager_control_right").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ }
1065             if (! self.can_fetch_more_move_lines) { return; }
1066             self.set("pager_index", self.get("pager_index")+1 );
1067         },
1068     
1069         filterHandler: function() {
1070             var self = this;
1071             self.set("pager_index", 0);
1072             self.filter = self.$(".filter").val();
1073             window.clearTimeout(self.apply_filter_timeout);
1074             self.apply_filter_timeout = window.setTimeout(self.proxy('updateMatches'), 200);
1075         },
1076
1077     
1078         /** Creating */
1079     
1080         initializeCreateForm: function() {
1081             var self = this;
1082     
1083             _.each(self.create_form, function(field) {
1084                 field.set("value", false);
1085             });
1086             self.label_field.set("value", self.st_line.name);
1087             self.amount_field.set("value", -1*self.get("balance"));
1088             self.account_id_field.focus();
1089         },
1090     
1091         addLineBeingEdited: function() {
1092             var self = this;
1093             if (! self.islineCreatedBeingEditedValid()) return;
1094             
1095             self.set("lines_created", self.get("lines_created").concat(self.get("line_created_being_edited")));
1096             // Add empty created line
1097             var new_id = self.get("line_created_being_edited")[0].id + 1;
1098             self.set("line_created_being_edited", [{'id': new_id}]);
1099     
1100             self.initializeCreateForm();
1101         },
1102     
1103         removeLine: function($line) {
1104             var self = this;
1105             var line_id = $line.data("lineid");
1106     
1107             // if deleting the created line that is being edited, validate it before
1108             if (line_id === self.get("line_created_being_edited")[0].id) {
1109                 self.addLineBeingEdited();
1110             }
1111             self.set("lines_created", _.filter(self.get("lines_created"), function(o) { return o.id != line_id }));
1112             self.amount_field.set("value", -1*self.get("balance"));
1113         },
1114     
1115         presetClickHandler: function(e) {
1116             var self = this;
1117             self.initializeCreateForm();
1118             var preset = self.presets[e.currentTarget.dataset.presetid];
1119             // Hack : set_value of a field calls a handler that returns a deferred because it could make a RPC call
1120             // to compute the tax before it updates the line being edited. Unfortunately this deferred is lost.
1121             // Hence this ugly hack to avoid concurrency problem that arose when setting amount (in initializeCreateForm), then tax, then another amount
1122             if (preset.tax && self.tax_field) self.tax_field.set_value(false);
1123             if (preset.amount && self.amount_field) self.amount_field.set_value(false);
1124
1125             for (var key in preset) {
1126                 if (! preset.hasOwnProperty(key) || key === "amount") continue;
1127                 if (preset[key] && self.hasOwnProperty(key+"_field"))
1128                     self[key+"_field"].set_value(preset[key]);
1129             }
1130             if (preset.amount && self.amount_field) {
1131                 if (preset.amount_type === "fixed")
1132                     self.amount_field.set_value(preset.amount);
1133                 else if (preset.amount_type === "percentage_of_total")
1134                     self.amount_field.set_value(self.st_line.amount * preset.amount / 100);
1135                 else if (preset.amount_type === "percentage_of_balance") {
1136                     self.amount_field.set_value(0);
1137                     self.updateBalance();
1138                     self.amount_field.set_value(-1 * self.get("balance") * preset.amount / 100);
1139                 }
1140             }
1141         },
1142     
1143
1144         /** Display */
1145     
1146         initialLineClickHandler: function() {
1147             var self = this;
1148             if (self.get("mode") === "match") {
1149                 self.set("mode", "inactive");
1150             } else {
1151                 self.set("mode", "match");
1152             }
1153         },
1154     
1155         lineOpenBalanceClickHandler: function() {
1156             var self = this;
1157             if (self.get("mode") === "create") {
1158                 self.set("mode", "match");
1159             } else {
1160                 self.set("mode", "create");
1161             }
1162         },
1163     
1164         changePartnerClickHandler: function() {
1165             var self = this;
1166             self.$(".change_partner_container").find("input").attr("placeholder", self.st_line.partner_name);
1167             self.$(".change_partner_container").show();
1168             self.$(".partner_name").hide();
1169             self.change_partner_field.$drop_down.trigger("click");
1170         },
1171     
1172     
1173         /** Views updating */
1174     
1175         updateAccountingViewMatchedLines: function() {
1176             var self = this;
1177             $.each(self.$(".tbody_matched_lines .bootstrap_popover"), function(){ $(this).popover('destroy') });
1178             self.$(".tbody_matched_lines").empty();
1179     
1180             _(self.get("mv_lines_selected")).each(function(line){
1181                 var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: true}));
1182                 self.bindPopoverTo($line.find(".line_info_button"));
1183                 if (line.propose_partial_reconcile) self.bindPopoverTo($line.find(".do_partial_reconcile_button"));
1184                 if (line.partial_reconcile) self.bindPopoverTo($line.find(".undo_partial_reconcile_button"));
1185                 self.$(".tbody_matched_lines").append($line);
1186             });
1187         },
1188     
1189         updateAccountingViewCreatedLines: function() {
1190             var self = this;
1191             $.each(self.$(".tbody_created_lines .bootstrap_popover"), function(){ $(this).popover('destroy') });
1192             self.$(".tbody_created_lines").empty();
1193     
1194             _(self.getCreatedLines()).each(function(line){
1195                 var $line = $(QWeb.render("bank_statement_reconciliation_created_line", {line: line}));
1196                 $line.find(".line_remove_button").click(function(){ self.removeLine($(this).closest(".created_line")) });
1197                 self.$(".tbody_created_lines").append($line);
1198                 if (line.no_remove_action) {
1199                     // Then the previous line's remove button deletes this line too
1200                     $line.hover(function(){ $(this).prev().addClass("active") },function(){ $(this).prev().removeClass("active") });
1201                 }
1202             });
1203         },
1204     
1205         updateMatchView: function() {
1206             var self = this;
1207             var table = self.$(".match table");
1208             var nothing_displayed = true;
1209         
1210             // Display move lines
1211             $.each(self.$(".match table .bootstrap_popover"), function(){ $(this).popover('destroy') });
1212             table.empty();
1213             var slice_start = self.get("pager_index") * self.max_move_lines_displayed;
1214             var slice_end = (self.get("pager_index")+1) * self.max_move_lines_displayed;
1215             _( _.filter(self.mv_lines_deselected, function(o){
1216                     return o.name.indexOf(self.filter) !== -1 || o.ref.indexOf(self.filter) !== -1 })
1217                 .slice(slice_start, slice_end)).each(function(line){
1218                 var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false}));
1219                 self.bindPopoverTo($line.find(".line_info_button"));
1220                 table.append($line);
1221                 nothing_displayed = false;
1222             });
1223             _(self.get("mv_lines")).each(function(line){
1224                 var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false}));
1225                 self.bindPopoverTo($line.find(".line_info_button"));
1226                 table.append($line);
1227                 nothing_displayed = false;
1228             });
1229             if (nothing_displayed && this.filter !== "")
1230                 table.append(QWeb.render("filter_no_match", {filter_str: self.filter}));
1231         },
1232     
1233         updatePagerControls: function() {
1234             var self = this;
1235         
1236             if (self.get("pager_index") === 0)
1237                 self.$(".pager_control_left").addClass("disabled");
1238             else
1239                 self.$(".pager_control_left").removeClass("disabled");
1240             if (! self.can_fetch_more_move_lines)
1241                 self.$(".pager_control_right").addClass("disabled");
1242             else
1243                 self.$(".pager_control_right").removeClass("disabled");
1244         },
1245     
1246         /** Properties changed */
1247     
1248         // Updates the validation button and the "open balance" line
1249         balanceChanged: function() {
1250             var self = this;
1251             var balance = self.get("balance");
1252             self.$(".tbody_open_balance").empty();
1253             // Special case hack : no identified partner
1254             if (self.st_line.has_no_partner) {
1255                 if (Math.abs(balance).toFixed(3) === "0.000") {
1256                     self.$(".button_ok").addClass("oe_highlight");
1257                     self.$(".button_ok").removeAttr("disabled");
1258                     self.$(".button_ok").text("OK");
1259                     self.is_valid = true;
1260                 } else {
1261                     self.$(".button_ok").removeClass("oe_highlight");
1262                     self.$(".button_ok").attr("disabled", "disabled");
1263                     self.$(".button_ok").text("OK");
1264                     self.is_valid = false;
1265                     var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : "");
1266                     var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : "");
1267                     var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {
1268                         debit: debit,
1269                         credit: credit,
1270                         account_code: self.map_account_id_code[self.st_line.open_balance_account_id]
1271                     }));
1272                     $line.find('.js_open_balance')[0].innerHTML = _t("Choose counterpart");
1273                     self.$(".tbody_open_balance").append($line);
1274                 }
1275                 return;
1276             }
1277     
1278             if (Math.abs(balance).toFixed(3) === "0.000") {
1279                 self.$(".button_ok").addClass("oe_highlight");
1280                 self.$(".button_ok").text("OK");
1281             } else {
1282                 self.$(".button_ok").removeClass("oe_highlight");
1283                 self.$(".button_ok").text("Keep open");
1284                 var debit = (balance > 0 ? self.formatCurrency(balance, self.st_line.currency_id) : "");
1285                 var credit = (balance < 0 ? self.formatCurrency(-1*balance, self.st_line.currency_id) : "");
1286                 var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", {
1287                     debit: debit,
1288                     credit: credit,
1289                     account_code: self.map_account_id_code[self.st_line.open_balance_account_id]
1290                 }));
1291                 self.$(".tbody_open_balance").append($line);
1292             }
1293         },
1294     
1295         modeChanged: function(o, val) {
1296             var self = this;
1297     
1298             self.$(".action_pane.active").removeClass("active");
1299
1300             if (val.oldValue === "create")
1301                 self.addLineBeingEdited();
1302     
1303             if (self.get("mode") === "inactive") {
1304                 self.$(".match").slideUp(self.animation_speed);
1305                 self.$(".create").slideUp(self.animation_speed);
1306                 self.el.dataset.mode = "inactive";
1307     
1308             } else if (self.get("mode") === "match") {
1309                 // TODO : remove this old_animation_speed / new_animation_speed hack
1310                 // when .on handler's returned deferred's no longer lost
1311                 var old_animation_speed = self.animation_speed;
1312                 return $.when(self.updateMatches()).then(function() {
1313                     var new_animation_speed = self.animation_speed;
1314                     self.animation_speed = old_animation_speed;
1315                     if (self.$el.hasClass("no_match")) {
1316                         self.animation_speed = 0;
1317                         self.set("mode", "create");
1318                         return;
1319                     }
1320                     self.$(".match").slideDown(self.animation_speed);
1321                     self.$(".create").slideUp(self.animation_speed);
1322                     self.el.dataset.mode = "match";
1323                     self.animation_speed = new_animation_speed;
1324                 });
1325     
1326             } else if (self.get("mode") === "create") {
1327                 self.initializeCreateForm();
1328                 self.$(".match").slideUp(self.animation_speed);
1329                 self.$(".create").slideDown(self.animation_speed);
1330                 self.el.dataset.mode = "create";
1331             }
1332         },
1333     
1334         pagerChanged: function() {
1335             this.updateMatches();
1336         },
1337     
1338         mvLinesChanged: function() {
1339             var self = this;
1340             // If pager_index is out of range, set it to display the last page
1341             if (self.get("pager_index") !== 0 && self.get("mv_lines").length === 0 && ! self.can_fetch_more_move_lines) {
1342                 self.set("pager_index", 0);
1343             }
1344         
1345             // If there is no match to display, disable match view and pass in mode inactive
1346             if (self.get("mv_lines").length + self.mv_lines_deselected.length === 0 && !self.can_fetch_more_move_lines && self.filter === "") {
1347                 self.$el.addClass("no_match");
1348                 if (self.get("mode") === "match") {
1349                     self.set("mode", "inactive");
1350                 }
1351             } else {
1352                 self.$el.removeClass("no_match");
1353             }
1354
1355             _.each(self.get("mv_lines"), function(line) {
1356                 if (line.partial_reconciliation_siblings_ids.length > 0) {
1357                     var correct_format = _.collect(line.partial_reconciliation_siblings_ids, function(o) { return {'id': o} });
1358                     self.getParent().excludeMoveLines(self, self.partner_id, correct_format);
1359                 }
1360             });
1361
1362             self.updateMatchView();
1363             self.updatePagerControls();
1364         },
1365     
1366         mvLinesSelectedChanged: function(elt, val) {
1367             var self = this;
1368         
1369             var added_lines = _.difference(val.newValue, val.oldValue);
1370             var removed_lines = _.difference(val.oldValue, val.newValue);
1371         
1372             self.getParent().excludeMoveLines(self, self.partner_id, added_lines);
1373             self.getParent().unexcludeMoveLines(self, self.partner_id, removed_lines);
1374         
1375             $.when(self.updateMatches()).then(function(){
1376                 self.updateAccountingViewMatchedLines();
1377                 self.updateBalance();
1378             });
1379         },
1380
1381         // Generic function for updating the line_created_being_edited
1382         formCreateInputChanged: function(elt, val) {
1383             var self = this;
1384             var line_created_being_edited = self.get("line_created_being_edited");
1385             line_created_being_edited[0][elt.corresponding_property] = val.newValue;
1386             line_created_being_edited[0].currency_id = self.st_line.currency_id;
1387     
1388             // Specific cases
1389             if (elt === self.account_id_field)
1390                 line_created_being_edited[0].account_num = self.map_account_id_code[elt.get("value")];
1391     
1392             // Update tax line
1393             var deferred_tax = new $.Deferred();
1394             if (elt === self.tax_id_field || elt === self.amount_field) {
1395                 var amount = self.amount_field.get("value");
1396                 var tax = self.map_tax_id_amount[self.tax_id_field.get("value")];
1397                 if (amount && tax) {
1398                     deferred_tax = $.when(self.model_tax
1399                         .call("compute_for_bank_reconciliation", [self.tax_id_field.get("value"), amount]))
1400                         .then(function(data){
1401                             line_created_being_edited[0].amount_with_tax = line_created_being_edited[0].amount;
1402                             line_created_being_edited[0].amount = (data.total.toFixed(3) === amount.toFixed(3) ? amount : data.total);
1403                             var current_line_cursor = 1;
1404                             $.each(data.taxes, function(index, tax){
1405                                 if (tax.amount !== 0.0) {
1406                                     var tax_account_id = (amount > 0 ? tax.account_collected_id : tax.account_paid_id);
1407                                     tax_account_id = tax_account_id !== false ? tax_account_id: line_created_being_edited[0].account_id;
1408                                     line_created_being_edited[current_line_cursor] = {
1409                                         id: line_created_being_edited[0].id,
1410                                         account_id: tax_account_id,
1411                                         account_num: self.map_account_id_code[tax_account_id],
1412                                         label: tax.name,
1413                                         amount: tax.amount,
1414                                         no_remove_action: true,
1415                                         currency_id: self.st_line.currency_id,
1416                                         is_tax_line: true
1417                                     };
1418                                     current_line_cursor = current_line_cursor + 1;
1419                                 }
1420                             });
1421                         }
1422                     );
1423                 } else {
1424                     line_created_being_edited.length = 1;
1425                     deferred_tax.resolve();
1426                 }
1427             } else { deferred_tax.resolve(); }
1428     
1429             $.when(deferred_tax).then(function(){
1430                 // Format amounts
1431                 var rounding = 1/self.map_currency_id_rounding[self.st_line.currency_id];
1432                 $.each(line_created_being_edited, function(index, val) {
1433                     if (val.amount) {
1434                         line_created_being_edited[index].amount = Math.round(val.amount*rounding)/rounding;
1435                         line_created_being_edited[index].amount_str = self.formatCurrency(Math.abs(val.amount), val.currency_id);
1436                     }
1437                 });
1438                 self.set("line_created_being_edited", line_created_being_edited);
1439                 self.createdLinesChanged(); // TODO For some reason, previous line doesn't trigger change handler
1440             });
1441         },
1442     
1443         createdLinesChanged: function() {
1444             var self = this;
1445             self.updateAccountingViewCreatedLines();
1446             self.updateBalance();
1447     
1448             if (self.islineCreatedBeingEditedValid()) self.$(".add_line").show();
1449             else self.$(".add_line").hide();
1450         },
1451     
1452     
1453         /** Model */
1454     
1455         doPartialReconcileButtonClickHandler: function(e) {
1456             var self = this;
1457     
1458             var line_id = $(e.currentTarget).closest("tr").data("lineid");
1459             var line = _.find(self.get("mv_lines_selected"), function(o) { return o.id == line_id });
1460             self.partialReconcileLine(line);
1461     
1462             $(e.currentTarget).popover('destroy');
1463             self.updateAccountingViewMatchedLines();
1464             self.updateBalance();
1465             e.stopPropagation();
1466         },
1467     
1468         partialReconcileLine: function(line) {
1469             var self = this;
1470             var balance = self.get("balance");
1471             line.initial_amount = line.debit !== 0 ? line.debit : -1 * line.credit;
1472             if (balance < 0) {
1473                 line.debit += balance;
1474                 line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id);
1475             } else {
1476                 line.credit -= balance;
1477                 line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id);
1478             }
1479             line.propose_partial_reconcile = false;
1480             line.partial_reconcile = true;
1481         },
1482     
1483         undoPartialReconcileButtonClickHandler: function(e) {
1484             var self = this;
1485     
1486             var line_id = $(e.currentTarget).closest("tr").data("lineid");
1487             var line = _.find(self.get("mv_lines_selected"), function(o) { return o.id == line_id });
1488             self.unpartialReconcileLine(line);
1489     
1490             $(e.currentTarget).popover('destroy');
1491             self.updateAccountingViewMatchedLines();
1492             self.updateBalance();
1493             e.stopPropagation();
1494         },
1495     
1496         unpartialReconcileLine: function(line) {
1497             var self = this;
1498             if (line.initial_amount > 0) {
1499                 line.debit = line.initial_amount;
1500                 line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id);
1501             } else {
1502                 line.credit = -1 * line.initial_amount;
1503                 line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id);
1504             }
1505             line.propose_partial_reconcile = true;
1506             line.partial_reconcile = false;
1507         },
1508     
1509         updateBalance: function() {
1510             var self = this;
1511             var mv_lines_selected = self.get("mv_lines_selected");
1512             var lines_selected_num = mv_lines_selected.length;
1513
1514             // Undo partial reconciliation if necessary
1515             if (lines_selected_num !== 1) {
1516                 _.each(mv_lines_selected, function(line) {
1517                     if (line.partial_reconcile === true) self.unpartialReconcileLine(line);
1518                     if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false;
1519                 });
1520                 self.updateAccountingViewMatchedLines();
1521             }
1522
1523             // Compute balance
1524             var balance = 0;
1525             balance -= self.st_line.amount;
1526             _.each(mv_lines_selected, function(o) {
1527                 balance = balance - o.debit + o.credit;
1528             });
1529             _.each(self.getCreatedLines(), function(o) {
1530                 balance += o.amount;
1531             });
1532             // Dealing with floating-point
1533             balance = Math.round(balance*1000)/1000;
1534             self.set("balance", balance);
1535     
1536             // Propose partial reconciliation if necessary
1537             if (lines_selected_num === 1 &&
1538                 self.st_line.amount * balance > 0 &&
1539                 self.st_line.amount * (mv_lines_selected[0].debit - mv_lines_selected[0].credit) < 0 &&
1540                 ! mv_lines_selected[0].partial_reconcile) {
1541                 
1542                 mv_lines_selected[0].propose_partial_reconcile = true;
1543                 self.updateAccountingViewMatchedLines();
1544             } else if (lines_selected_num === 1) {
1545                 mv_lines_selected[0].propose_partial_reconcile = false;
1546                 self.updateAccountingViewMatchedLines();
1547             }
1548         },
1549
1550         // Loads move lines according to the widget's state
1551         updateMatches: function() {
1552             var self = this;
1553             var deselected_lines_num = self.mv_lines_deselected.length;
1554             var offset = self.get("pager_index") * self.max_move_lines_displayed - deselected_lines_num;
1555             if (offset < 0) offset = 0;
1556             var limit = (self.get("pager_index")+1) * self.max_move_lines_displayed - deselected_lines_num;
1557             if (limit > self.max_move_lines_displayed) limit = self.max_move_lines_displayed;
1558             var excluded_ids = _.collect(self.get("mv_lines_selected").concat(self.mv_lines_deselected), function(o) { return o.id; });
1559             var globally_excluded_ids = [];
1560             if (self.st_line.has_no_partner)
1561                 _.each(self.getParent().excluded_move_lines_ids, function(o) { globally_excluded_ids = globally_excluded_ids.concat(o) });
1562             else
1563                 globally_excluded_ids = self.getParent().excluded_move_lines_ids[self.partner_id];
1564             if (globally_excluded_ids !== undefined)
1565                 for (var i=0; i<globally_excluded_ids.length; i++)
1566                     if (excluded_ids.indexOf(globally_excluded_ids[i]) === -1)
1567                         excluded_ids.push(globally_excluded_ids[i]);
1568             
1569             limit += 1; // Let's fetch 1 more item than requested
1570             if (limit > 0) {
1571                 return self.model_bank_statement_line
1572                     .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, offset, limit])
1573                     .then(function (lines) {
1574                         _.each(lines, function(line) { self.decorateMoveLine(line, self.st_line.currency_id) }, self);
1575                         // If we could fetch 1 more item than what we'll display, that means there are move lines left to be displayed (so we enable the pager)
1576                         self.can_fetch_more_move_lines = (lines.length === limit);
1577                         self.set("mv_lines", lines.slice(0, limit-1));
1578                     });
1579             } else {
1580                 self.set("mv_lines", []);
1581             }
1582         },
1583
1584         // Changes the partner_id of the statement_line in the DB and reloads the widget
1585         changePartner: function(partner_id) {
1586             var self = this;
1587             self.is_consistent = false;
1588             return self.model_bank_statement_line
1589                 // Update model
1590                 .call("write", [[self.st_line_id], {'partner_id': partner_id}])
1591                 .then(function () {
1592                     self.do_load_reconciliation_proposition = false; // of the server might set the statement line's partner
1593                     self.animation_speed = 0;
1594                     return $.when(self.restart(self.get("mode"))).then(function(){
1595                         self.do_load_reconciliation_proposition = true;
1596                         self.is_consistent = true;
1597                         self.set("mode", "match");
1598                     });
1599                 });
1600         },
1601     
1602         // Returns an object that can be passed to process_reconciliation()
1603         prepareSelectedMoveLineForPersisting: function(line) {
1604             return {
1605                 name: line.name,
1606                 debit: line.debit,
1607                 credit: line.credit,
1608                 counterpart_move_line_id: line.id,
1609             };
1610         },
1611     
1612         // idem
1613         prepareCreatedMoveLineForPersisting: function(line) {
1614             var dict = {};
1615             if (dict['account_id'] === undefined)
1616                 dict['account_id'] = line.account_id;
1617             dict['name'] = line.label;
1618             var amount = line.tax_id ? line.amount_with_tax: line.amount;
1619             if (amount > 0) dict['credit'] = amount;
1620             if (amount < 0) dict['debit'] = -1 * amount;
1621             if (line.tax_id) dict['account_tax_id'] = line.tax_id;
1622             if (line.is_tax_line) dict['is_tax_line'] = line.is_tax_line;
1623             if (line.analytic_account_id) dict['analytic_account_id'] = line.analytic_account_id;
1624     
1625             return dict;
1626         },
1627     
1628         // idem
1629         prepareOpenBalanceForPersisting: function() {
1630             var balance = this.get("balance");
1631             var dict = {};
1632     
1633             dict['account_id'] = this.st_line.open_balance_account_id;
1634             dict['name'] = _t("Open balance");
1635             if (balance > 0) dict['debit'] = balance;
1636             if (balance < 0) dict['credit'] = -1*balance;
1637     
1638             return dict;
1639         },
1640
1641         makeMoveLineDicts: function() {
1642             var self = this;
1643             var mv_line_dicts = [];
1644             _.each(self.get("mv_lines_selected"), function(o) { mv_line_dicts.push(self.prepareSelectedMoveLineForPersisting(o)) });
1645             _.each(self.getCreatedLines(), function(o) { mv_line_dicts.push(self.prepareCreatedMoveLineForPersisting(o)) });
1646             if (Math.abs(self.get("balance")).toFixed(3) !== "0.000") mv_line_dicts.push(self.prepareOpenBalanceForPersisting());
1647             return mv_line_dicts;
1648         },
1649     
1650         // Persist data, notify parent view and terminate widget
1651         persistAndDestroy: function(speed) {
1652             var self = this;
1653             speed = (isNaN(speed) ? self.animation_speed : speed);
1654             if (! self.is_consistent) return;
1655
1656             // Sliding animation
1657             var height = self.$el.outerHeight();
1658             var container = $("<div />");
1659             container.css("height", height)
1660                      .css("marginTop", self.$el.css("marginTop"))
1661                      .css("marginBottom", self.$el.css("marginBottom"));
1662             self.$el.wrap(container);
1663             var deferred_animation = self.$el.parent().slideUp(speed*height/150);
1664     
1665             // RPC
1666             self.$(".button_ok").attr("disabled", "disabled");
1667             return self.model_bank_statement_line
1668                 .call("process_reconciliation", [self.st_line_id, self.makeMoveLineDicts()])
1669                 .done(function () {
1670                     self.getParent().unexcludeMoveLines(self, self.partner_id, self.get("mv_lines_selected"));
1671                     $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') });
1672                     return $.when(deferred_animation).then(function(){
1673                         self.$el.parent().remove();
1674                         var parent = self.getParent();
1675                         return $.when(self.destroy()).then(function() {
1676                             parent.childValidated(self);
1677                         });
1678                     });
1679                 }).fail(function(){
1680                     self.$el.parent().slideDown(speed*height/150, function(){
1681                         self.$el.unwrap();
1682                     });
1683                 }).always(function() {
1684                     self.$(".button_ok").removeAttr("disabled");
1685                 });
1686         },
1687     });
1688
1689     instance.web.views.add('tree_account_reconciliation', 'instance.web.account.ReconciliationListView');
1690     instance.web.account.ReconciliationListView = instance.web.ListView.extend({
1691         init: function() {
1692             this._super.apply(this, arguments);
1693             var self = this;
1694             this.current_partner = null;
1695             this.on('record_selected', this, function() {
1696                 if (self.get_selected_ids().length === 0) {
1697                     self.$(".oe_account_recon_reconcile").attr("disabled", "");
1698                 } else {
1699                     self.$(".oe_account_recon_reconcile").removeAttr("disabled");
1700                 }
1701             });
1702         },
1703         load_list: function() {
1704             var self = this;
1705             var tmp = this._super.apply(this, arguments);
1706             if (this.partners) {
1707                 this.$el.prepend(QWeb.render("AccountReconciliation", {widget: this}));
1708                 this.$(".oe_account_recon_previous").click(function() {
1709                     self.current_partner = (((self.current_partner - 1) % self.partners.length) + self.partners.length) % self.partners.length;
1710                     self.search_by_partner();
1711                 });
1712                 this.$(".oe_account_recon_next").click(function() {
1713                     self.current_partner = (self.current_partner + 1) % self.partners.length;
1714                     self.search_by_partner();
1715                 });
1716                 this.$(".oe_account_recon_reconcile").click(function() {
1717                     self.reconcile();
1718                 });
1719                 this.$(".oe_account_recom_mark_as_reconciled").click(function() {
1720                     self.mark_as_reconciled();
1721                 });
1722             }
1723             return tmp;
1724         },
1725         do_search: function(domain, context, group_by) {
1726             var self = this;
1727             this.last_domain = domain;
1728             this.last_context = context;
1729             this.last_group_by = group_by;
1730             this.old_search = _.bind(this._super, this);
1731             var mod = new instance.web.Model("account.move.line", context, domain);
1732             return mod.call("list_partners_to_reconcile", []).then(function(result) {
1733                 var current = self.current_partner !== null ? self.partners[self.current_partner][0] : null;
1734                 self.partners = result;
1735                 var index = _.find(_.range(self.partners.length), function(el) {
1736                     if (current === self.partners[el][0])
1737                         return true;
1738                 });
1739                 if (index !== undefined)
1740                     self.current_partner = index;
1741                 else
1742                     self.current_partner = self.partners.length == 0 ? null : 0;
1743                 self.search_by_partner();
1744             });
1745         },
1746         search_by_partner: function() {
1747             var self = this;
1748             var fct = function() {
1749                 return self.old_search(new instance.web.CompoundDomain(self.last_domain, 
1750                     [["partner_id", "in", self.current_partner === null ? [] :
1751                     [self.partners[self.current_partner][0]] ]]), self.last_context, self.last_group_by);
1752             };
1753             if (self.current_partner === null) {
1754                 self.last_reconciliation_date = _t("Never");
1755                 return fct();
1756             } else {
1757                 return new instance.web.Model("res.partner").call("read",
1758                     [self.partners[self.current_partner][0], ["last_reconciliation_date"]]).then(function(res) {
1759                     self.last_reconciliation_date = 
1760                         instance.web.format_value(res.last_reconciliation_date, {"type": "datetime"}, _t("Never"));
1761                     return fct();
1762                 });
1763             }
1764         },
1765         reconcile: function() {
1766             var self = this;
1767             var ids = this.get_selected_ids();
1768             if (ids.length === 0) {
1769                 new instance.web.Dialog(this, {
1770                     title: _t("Warning"),
1771                     size: 'medium',
1772                 }, $("<div />").text(_t("You must choose at least one record."))).open();
1773                 return false;
1774             }
1775
1776             new instance.web.Model("ir.model.data").call("get_object_reference", ["account", "action_view_account_move_line_reconcile"]).then(function(result) {
1777                 var additional_context = _.extend({
1778                     active_id: ids[0],
1779                     active_ids: ids,
1780                     active_model: self.model
1781                 });
1782                 return self.rpc("/web/action/load", {
1783                     action_id: result[1],
1784                     context: additional_context
1785                 }).done(function (result) {
1786                     result.context = instance.web.pyeval.eval('contexts', [result.context, additional_context]);
1787                     result.flags = result.flags || {};
1788                     result.flags.new_window = true;
1789                     return self.do_action(result, {
1790                         on_close: function () {
1791                             self.do_search(self.last_domain, self.last_context, self.last_group_by);
1792                         }
1793                     });
1794                 });
1795             });
1796         },
1797         mark_as_reconciled: function() {
1798             var self = this;
1799             var id = self.partners[self.current_partner][0];
1800             new instance.web.Model("res.partner").call("mark_as_reconciled", [[id]]).then(function() {
1801                 self.do_search(self.last_domain, self.last_context, self.last_group_by);
1802             });
1803         },
1804         do_select: function (ids, records) {
1805             this.trigger('record_selected')
1806             this._super.apply(this, arguments);
1807         },
1808     });
1809     
1810 };