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