openerp.account = function (instance) { openerp.account.quickadd(instance); var _t = instance.web._t, _lt = instance.web._lt; var QWeb = instance.web.qweb; instance.web.account = instance.web.account || {}; instance.web.client_actions.add('bank_statement_reconciliation_view', 'instance.web.account.bankStatementReconciliation'); instance.web.account.bankStatementReconciliation = instance.web.Widget.extend({ className: 'oe_bank_statement_reconciliation', init: function(parent, context) { this._super(parent); this.max_reconciliations_displayed = 10; this.statement_id = context.context.statement_id; this.title = context.context.title || _t("Reconciliation"); this.st_lines = []; this.last_displayed_reconciliation_index = undefined; // Flow control this.reconciled_lines = 0; // idem this.already_reconciled_lines = 0; // Number of lines of the statement which were already reconciled this.model_bank_statement = new instance.web.Model("account.bank.statement"); this.model_bank_statement_line = new instance.web.Model("account.bank.statement.line"); this.reconciliation_menu_id = false; // Used to update the needaction badge this.formatCurrency; // Method that formats the currency ; loaded from the server // Only for statistical purposes this.lines_reconciled_with_ctrl_enter = 0; this.time_widget_loaded = Date.now(); // Stuff used by the children bankStatementReconciliationLine this.max_move_lines_displayed = 5; this.animation_speed = 100; // "Blocking" animations this.aestetic_animation_speed = 300; // eye candy this.map_tax_id_amount = {}; this.presets = {}; // We'll need to get the code of an account selected in a many2one (whose value is the id) this.map_account_id_code = {}; // The same move line cannot be selected for multiple resolutions this.excluded_move_lines_ids = {}; // Description of the fields to initialize in the "create new line" form // NB : for presets to work correctly, a field id must be the same string as a preset field this.create_form_fields = { account_id: { id: "account_id", index: 0, corresponding_property: "account_id", // a account.move field name label: _t("Account"), required: true, tabindex: 10, constructor: instance.web.form.FieldMany2One, field_properties: { relation: "account.account", string: _t("Account"), type: "many2one", domain: [['type','!=','view']], }, }, label: { id: "label", index: 1, corresponding_property: "label", label: _t("Label"), required: true, tabindex: 11, constructor: instance.web.form.FieldChar, field_properties: { string: _t("Label"), type: "char", }, }, tax_id: { id: "tax_id", index: 2, corresponding_property: "tax_id", label: _t("Tax"), required: false, tabindex: 12, constructor: instance.web.form.FieldMany2One, field_properties: { relation: "account.tax", string: _t("Tax"), type: "many2one", }, }, amount: { id: "amount", index: 3, corresponding_property: "amount", label: _t("Amount"), required: true, tabindex: 13, constructor: instance.web.form.FieldFloat, field_properties: { string: _t("Amount"), type: "float", }, }, analytic_account_id: { id: "analytic_account_id", index: 4, corresponding_property: "analytic_account_id", label: _t("Analytic Acc."), required: false, tabindex: 14, group:"analytic.group_analytic_accounting", constructor: instance.web.form.FieldMany2One, field_properties: { relation: "account.analytic.account", string: _t("Analytic Acc."), type: "many2one", }, }, }; }, start: function() { this._super(); var self = this; // Inject variable styles var style = document.createElement("style"); style.appendChild(document.createTextNode("")); document.head.appendChild(style); var css_selector = ".oe_bank_statement_reconciliation_line .toggle_match, .oe_bank_statement_reconciliation_line .toggle_create, .oe_bank_statement_reconciliation_line .initial_line > td"; if(style.sheet.insertRule) { style.sheet.insertRule(css_selector + " { -webkit-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); style.sheet.insertRule(css_selector + " { -moz-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); style.sheet.insertRule(css_selector + " { -ms-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); style.sheet.insertRule(css_selector + " { -o-transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); style.sheet.insertRule(css_selector + " { transition-duration: "+self.aestetic_animation_speed+"ms; }", 0); } else { style.sheet.addRule(css_selector, "-webkit-transition-duration: "+self.aestetic_animation_speed+"ms;"); style.sheet.addRule(css_selector, "-moz-transition-duration: "+self.aestetic_animation_speed+"ms;"); style.sheet.addRule(css_selector, "-ms-transition-duration: "+self.aestetic_animation_speed+"ms;"); style.sheet.addRule(css_selector, "-o-transition-duration: "+self.aestetic_animation_speed+"ms;"); style.sheet.addRule(css_selector, "-webkit-transition-duration: "+self.aestetic_animation_speed+"ms;"); } // Retreive statement infos and reconciliation data from the model var lines_filter = [['journal_entry_id', '=', false]]; var deferred_promises = []; if (self.statement_id) { lines_filter.push(['statement_id', '=', self.statement_id]); deferred_promises.push(self.model_bank_statement .query(["name"]) .filter([['id', '=', self.statement_id]]) .first() .then(function(title){ self.title = title.name; }) ); deferred_promises.push(self.model_bank_statement .call("number_of_lines_reconciled", [self.statement_id]) .then(function(num) { self.already_reconciled_lines = num; }) ); } deferred_promises.push(new instance.web.Model("account.statement.operation.template") .query(['id','name','account_id','label','amount_type','amount','tax_id','analytic_account_id']) .all().then(function (data) { _(data).each(function(preset){ self.presets[preset.id] = preset; }); }) ); deferred_promises.push(self.model_bank_statement .call("get_format_currency_js_function", [self.statement_id]) .then(function(data){ self.formatCurrency = new Function("amount", data); }) ); deferred_promises.push(self.model_bank_statement_line .query(['id']) .filter(lines_filter) .order_by('id') .all().then(function (data) { self.st_lines = _(data).map(function(o){ return o.id }); }) ); // When queries are done, render template and reconciliation lines return $.when.apply($, deferred_promises).then(function(){ // If there is no statement line to reconcile, stop here if (self.st_lines.length === 0) { self.$el.prepend(QWeb.render("bank_statement_nothing_to_reconcile")); return; } // Create a dict account id -> account code for display facilities new instance.web.Model("account.account") .query(['id', 'code']) .all().then(function(data) { _.each(data, function(o) { self.map_account_id_code[o.id] = o.code }); }); // Create a dict tax id -> amount new instance.web.Model("account.tax") .query(['id', 'amount']) .all().then(function(data) { _.each(data, function(o) { self.map_tax_id_amount[o.id] = o.amount }); }); new instance.web.Model("ir.model.data") .call("xmlid_to_res_id", ["account.menu_bank_reconcile_bank_statements"]) .then(function(data) { self.reconciliation_menu_id = data; self.doReloadMenuReconciliation(); }); // Bind keyboard events TODO : méthode standard ? $("body").on("keypress", function (e) { self.keyboardShortcutsHandler(e); }); // Render and display self.$el.prepend(QWeb.render("bank_statement_reconciliation", {title: self.title, total_lines: self.already_reconciled_lines+self.st_lines.length})); self.updateProgressbar(); var reconciliations_to_show = self.st_lines.slice(0, self.max_reconciliations_displayed); self.last_displayed_reconciliation_index = reconciliations_to_show.length; self.$(".reconciliation_lines_container").css("opacity", 0); // Display the reconciliations return self.model_bank_statement_line .call("get_data_for_reconciliations", [reconciliations_to_show]) .then(function (data) { var child_promises = []; _.each(reconciliations_to_show, function(st_line_id){ var datum = data.shift(); child_promises.push(self.displayReconciliation(st_line_id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition)); }); $.when.apply($, child_promises).then(function(){ self.getChildren()[0].set("mode", "match"); self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed); }); }); }); }, keyboardShortcutsHandler: function(e) { var self = this; if (e.which === 13 && (e.ctrlKey || e.metaKey)) { $.each(self.getChildren(), function(i, o){ if (o.is_valid && o.persistAndDestroy()) { self.lines_reconciled_with_ctrl_enter++; } }); } }, // Adds move line ids to the list of move lines not to fetch for a given partner // This is required because the same move line cannot be selected for multiple reconciliation excludeMoveLines: function(source_child, partner_id, line_ids) { var self = this; var excluded_ids = this.excluded_move_lines_ids[partner_id]; var excluded_move_lines_changed = false; _.each(line_ids, function(line_id){ if (excluded_ids.indexOf(line_id) === -1) { excluded_ids.push(line_id); excluded_move_lines_changed = true; } }); if (! excluded_move_lines_changed) return; // Function that finds if an array of line objects contains at least a line identified by its id var contains_lines = function(lines_array, line_ids) { for (var i = 0; i < lines_array.length; i++) for (var j = 0; j < line_ids.length; j++) if (lines_array[i].id === line_ids[j]) return true; return false; }; // Update children if needed _.each(self.getChildren(), function(child){ if (child.partner_id === partner_id && child !== source_child) { if (contains_lines(child.get("mv_lines_selected"), line_ids)) { child.set("mv_lines_selected", _.filter(child.get("mv_lines_selected"), function(o){ return line_ids.indexOf(o.id) === -1 })); } else if (contains_lines(child.mv_lines_deselected, line_ids)) { child.mv_lines_deselected = _.filter(child.mv_lines_deselected, function(o){ return line_ids.indexOf(o.id) === -1 }); child.updateMatches(); } else if (contains_lines(child.get("mv_lines"), line_ids)) { child.updateMatches(); } } }); }, unexcludeMoveLines: function(source_child, partner_id, line_ids) { var self = this; var initial_excluded_lines_num = this.excluded_move_lines_ids[partner_id].length; this.excluded_move_lines_ids[partner_id] = _.difference(this.excluded_move_lines_ids[partner_id], line_ids); if (this.excluded_move_lines_ids[partner_id].length === initial_excluded_lines_num) return; // Update children if needed _.each(self.getChildren(), function(child){ if (child.partner_id === partner_id && child !== source_child && (child.get("mode") === "match" || child.$el.hasClass("no_match"))) child.updateMatches(); }); }, displayReconciliation: function(st_line_id, mode, animate_entrance, initial_data_provided, st_line, reconciliation_proposition) { var self = this; animate_entrance = (animate_entrance === undefined ? true : animate_entrance); initial_data_provided = (initial_data_provided === undefined ? false : initial_data_provided); var context = { st_line_id: st_line_id, mode: mode, animate_entrance: animate_entrance, initial_data_provided: initial_data_provided, st_line: initial_data_provided ? st_line : undefined, reconciliation_proposition: initial_data_provided ? reconciliation_proposition : undefined, }; var widget = new instance.web.account.bankStatementReconciliationLine(self, context); return widget.appendTo(self.$(".reconciliation_lines_container")); }, childValidated: function(child) { var self = this; self.reconciled_lines++; self.updateProgressbar(); self.doReloadMenuReconciliation(); // Display new line if there are left if (self.last_displayed_reconciliation_index < self.st_lines.length) { self.displayReconciliation(self.st_lines[self.last_displayed_reconciliation_index++], 'inactive'); } // Put the first line in match mode if (self.reconciled_lines !== self.st_lines.length) { var first_child = self.getChildren()[0]; if (first_child.get("mode") === "inactive") { first_child.set("mode", "match"); } } // Congratulate the user if the work is done if (self.reconciled_lines === self.st_lines.length) { self.displayDoneMessage(); } }, displayDoneMessage: function() { var self = this; var sec_taken = Math.round((Date.now()-self.time_widget_loaded)/1000); var sec_per_item = Math.round(sec_taken/self.reconciled_lines); var achievements = []; var time_taken; if (sec_taken/60 >= 1) time_taken = Math.floor(sec_taken/60) +"' "+ sec_taken%60 +"''"; else time_taken = sec_taken%60 +" seconds"; var title; if (sec_per_item < 5) title = _t("Whew, that was fast !") + " "; else title = _t("Congrats, you're all done !") + " "; if (self.lines_reconciled_with_ctrl_enter === self.reconciled_lines) achievements.push({ title: _t("Efficiency at its finest"), desc: _t("Only use the ctrl-enter shortcut to validate reconciliations."), icon: "fa-keyboard-o"} ); if (sec_per_item < 5) achievements.push({ title: _t("Fast reconciler"), desc: _t("Take on average less than 5 seconds to reconcile a transaction."), icon: "fa-bolt"} ); // Render it self.$(".protip").hide(); self.$(".oe_form_sheet").append(QWeb.render("bank_statement_reconciliation_done_message", { title: title, time_taken: time_taken, sec_per_item: sec_per_item, transactions_done: self.reconciled_lines, done_with_ctrl_enter: self.lines_reconciled_with_ctrl_enter, achievements: achievements, has_statement_id: self.statement_id !== undefined, })); // Animate it var container = $("
"); self.$(".done_message").wrap(container).css("opacity", 0).css("position", "relative").css("left", "-50%"); self.$(".done_message").animate({opacity: 1, left: 0}, self.aestetic_animation_speed*2, "easeOutCubic"); self.$(".done_message").animate({opacity: 1}, self.aestetic_animation_speed*3, "easeOutCubic"); // Make it interactive self.$(".achievement").popover({'placement': 'top', 'container': self.el, 'trigger': 'hover'}); self.$(".button_back_to_statement").click(function() { self.do_action({ type: 'ir.actions.client', tag: 'history_back', }); }); if (self.$(".button_close_statement").length !== 0) { self.$(".button_close_statement").hide(); self.model_bank_statement .query(["balance_end_real", "balance_end"]) .filter([['id', '=', self.statement_id]]) .first() .then(function(data){ if (data.balance_end_real === data.balance_end) { self.$(".button_close_statement").show(); self.$(".button_close_statement").click(function() { self.$(".button_close_statement").attr("disabled", "disabled"); self.model_bank_statement .call("button_confirm_bank", [[self.statement_id]]) .then(function () { self.do_action({ type: 'ir.actions.client', tag: 'history_back', }); }, function() { self.$(".button_close_statement").removeAttr("disabled"); }); }); } }); } }, updateProgressbar: function() { var self = this; var done = self.already_reconciled_lines + self.reconciled_lines; var total = self.already_reconciled_lines + self.st_lines.length; var prog_bar = self.$(".progress .progress-bar"); prog_bar.attr("aria-valuenow", done); prog_bar.css("width", (done/total*100)+"%"); self.$(".progress .progress-text .valuenow").text(done); }, /* reloads the needaction badge */ doReloadMenuReconciliation: function () { var menu = instance.webclient.menu; if (!menu || !this.reconciliation_menu_id) { return $.when(); } return menu.rpc("/web/menu/load_needaction", {'menu_ids': [this.reconciliation_menu_id]}).done(function(r) { menu.on_needaction_loaded(r); }).then(function () { menu.trigger("need_action_reloaded"); }); }, }); instance.web.account.bankStatementReconciliationLine = instance.web.Widget.extend({ className: 'oe_bank_statement_reconciliation_line', events: { "click .partner_name": "partnerNameClickHandler", "click .button_ok": "persistAndDestroy", "click .mv_line": "moveLineClickHandler", "click .initial_line": "initialLineClickHandler", "click .line_open_balance": "lineOpenBalanceClickHandler", "click .pager_control_left:not(.disabled)": "pagerControlLeftHandler", "click .pager_control_right:not(.disabled)": "pagerControlRightHandler", "keyup .filter": "filterHandler", "click .line_info_button": function(e){e.stopPropagation()}, // small usability hack "click .add_line": "addLineBeingEdited", "click .preset": "presetClickHandler", "click .do_partial_reconcile_button": "doPartialReconcileButtonClickHandler", "click .undo_partial_reconcile_button": "undoPartialReconcileButtonClickHandler", }, init: function(parent, context) { this._super(parent); if (context.initial_data_provided) { // Process data _(context.reconciliation_proposition).each(this.decorateMoveLine.bind(this)); this.set("mv_lines_selected", context.reconciliation_proposition); this.st_line = context.st_line; this.partner_id = context.st_line.partner_id; this.decorateStatementLine(this.st_line); // Exclude selected move lines var selected_line_ids = _(context.reconciliation_proposition).map(function(o){ return o.id }); if (this.getParent().excluded_move_lines_ids[this.partner_id] === undefined) this.getParent().excluded_move_lines_ids[this.partner_id] = []; this.getParent().excludeMoveLines(this, this.partner_id, selected_line_ids); } else { this.set("mv_lines_selected", []); this.st_line = undefined; this.partner_id = undefined; } this.context = context; this.st_line_id = context.st_line_id; this.max_move_lines_displayed = this.getParent().max_move_lines_displayed; this.animation_speed = this.getParent().animation_speed; this.aestetic_animation_speed = this.getParent().aestetic_animation_speed; this.model_bank_statement_line = new instance.web.Model("account.bank.statement.line"); this.model_res_users = new instance.web.Model("res.users"); this.model_tax = new instance.web.Model("account.tax"); this.map_account_id_code = this.getParent().map_account_id_code; this.map_tax_id_amount = this.getParent().map_tax_id_amount; this.formatCurrency = this.getParent().formatCurrency; this.presets = this.getParent().presets; this.is_valid = true; this.is_consistent = true; // Used to prevent bad server requests this.total_move_lines_num = undefined; // Used for pagers this.filter = ""; this.set("balance", undefined); // Debit is +, credit is - this.on("change:balance", this, this.balanceChanged); this.set("mode", undefined); this.on("change:mode", this, this.modeChanged); this.set("pager_index", 0); this.on("change:pager_index", this, this.pagerChanged); // NB : mv_lines represent the counterpart that will be created to reconcile existing move lines, so debit and credit are inverted this.set("mv_lines", []); this.on("change:mv_lines", this, this.mvLinesChanged); this.mv_lines_deselected = []; // deselected lines are displayed on top of the match table this.on("change:mv_lines_selected", this, this.mvLinesSelectedChanged); this.set("lines_created", []); this.set("line_created_being_edited", [{'id': 0}]); this.on("change:lines_created", this, this.createdLinesChanged); this.on("change:line_created_being_edited", this, this.createdLinesChanged); }, start: function() { var self = this; return self._super().then(function() { // no animation while loading self.animation_speed = 0; self.aestetic_animation_speed = 0; self.is_consistent = false; if (self.context.animate_entrance) self.$el.css("opacity", "0"); // Fetch data var deferred_fetch_data = new $.Deferred(); if (! self.context.initial_data_provided) { // Load statement line self.model_bank_statement_line .call("get_statement_line_for_reconciliation", [self.st_line_id]) .then(function (data) { self.st_line = data; self.decorateStatementLine(self.st_line); self.partner_id = data.partner_id; if (self.getParent().excluded_move_lines_ids[self.partner_id] === undefined) self.getParent().excluded_move_lines_ids[self.partner_id] = []; // load and display move lines $.when(self.loadReconciliationProposition()).then(function(){ deferred_fetch_data.resolve(); }); }); } else { deferred_fetch_data.resolve(); } // Display the widget return $.when(deferred_fetch_data).then(function(){ // Render template var presets_array = []; for (var id in self.presets) if (self.presets.hasOwnProperty(id)) presets_array.push(self.presets[id]); self.$el.prepend(QWeb.render("bank_statement_reconciliation_line", {line: self.st_line, mode: self.context.mode, presets: presets_array})); // Stuff that require the template to be rendered self.$(".match").slideUp(0); self.$(".create").slideUp(0); if (self.st_line.no_match) self.$el.addClass("no_match"); if (self.context.mode !== "match") self.updateMatches(); self.bindPopoverTo(self.$(".line_info_button")); self.createFormWidgets(); // Special case hack : no identified partner if (self.st_line.has_no_partner) { self.$el.css("opacity", "0"); self.updateBalance(); self.$(".change_partner_container").show(0); self.change_partner_field.$el.find("input").attr("placeholder", _t("Select Partner")); self.$(".match").slideUp(0); self.$el.addClass("no_partner"); self.set("mode", self.context.mode); self.animation_speed = self.getParent().animation_speed; self.aestetic_animation_speed = self.getParent().aestetic_animation_speed; self.$el.animate({opacity: 1}, self.aestetic_animation_speed); self.is_consistent = true; return; } // TODO : the .on handler's returned deferred is lost return $.when(self.set("mode", self.context.mode)).then(function(){ self.is_consistent = true; // Make sure the display is OK self.balanceChanged(); self.createdLinesChanged(); self.updateAccountingViewMatchedLines(); // Make an entrance self.animation_speed = self.getParent().animation_speed; self.aestetic_animation_speed = self.getParent().aestetic_animation_speed; if (self.context.animate_entrance) return self.$el.animate({opacity: 1}, self.aestetic_animation_speed); }); }); }); }, restart: function(mode) { var self = this; mode = (mode === undefined ? 'inactive' : mode); self.$el.css("height", self.$el.outerHeight()); // Destroy everything _.each(self.getChildren(), function(o){ o.destroy() }); self.is_consistent = false; return $.when(self.$el.animate({opacity: 0}, self.animation_speed)).then(function() { self.getParent().unexcludeMoveLines(self, self.partner_id, _.map(self.get("mv_lines_selected"), function(o){ return o.id })); $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') }); self.$el.empty(); self.$el.removeClass("no_partner"); self.context.mode = mode; self.context.initial_data_provided = false; self.is_valid = true; self.is_consistent = true; self.filter = ""; self.set("balance", undefined, {silent: true}); self.set("mode", undefined, {silent: true}); self.set("pager_index", 0, {silent: true}); self.set("mv_lines", [], {silent: true}); self.set("mv_lines_selected", [], {silent: true}); self.mv_lines_deselected = []; self.set("lines_created", [], {silent: true}); self.set("line_created_being_edited", [{'id': 0}], {silent: true}); // Rebirth return $.when(self.start()).then(function() { self.$el.css("height", "auto"); self.is_consistent = true; self.$el.animate({opacity: 1}, self.animation_speed); }); }); }, /* create form widgets, append them to the dom and bind their events handlers */ createFormWidgets: function() { var self = this; var create_form_fields = self.getParent().create_form_fields; var create_form_fields_arr = []; for (var key in create_form_fields) if (create_form_fields.hasOwnProperty(key)) create_form_fields_arr.push(create_form_fields[key]); create_form_fields_arr.sort(function(a, b){ return b.index - a.index }); // field_manager var dataset = new instance.web.DataSet(this, "account.account", self.context); dataset.ids = []; dataset.arch = { attrs: { string: "Stéphanie de Monaco", version: "7.0", class: "oe_form_container" }, children: [], tag: "form" }; var field_manager = new instance.web.FormView ( this, dataset, false, { initial_mode: 'edit', disable_autofocus: false, $buttons: $(), $pager: $() }); field_manager.load_form(dataset); // fields default properties var Default_field = function() { this.context = {}; this.domain = []; this.help = ""; this.readonly = false; this.required = true; this.selectable = true; this.states = {}; this.views = {}; }; var Default_node = function(field_name) { this.tag = "field"; this.children = []; this.required = true; this.attrs = { invisible: "False", modifiers: '{"required":true}', name: field_name, nolabel: "True", }; }; // Append fields to the field_manager field_manager.fields_view.fields = {}; for (var i=0; i