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', events: { "click .statement_name span": "statementNameClickHandler", "keyup .change_statement_name_field": "changeStatementNameFieldHandler", "click .change_statement_name_button": "changeStatementButtonClickHandler", "click .show_more": "showMoreButtonClickHandler", }, init: function(parent, context) { this._super(parent); // Number of reconciliations loaded initially and by clicking 'show more' this.num_reconciliations_fetched_in_batch = 10; if (context.context.statement_id) this.statement_ids = [context.context.statement_id]; if (context.context.statement_ids) this.statement_ids = context.context.statement_ids; this.single_statement = this.statement_ids !== undefined && this.statement_ids.length === 1; this.multiple_statements = this.statement_ids !== undefined && this.statement_ids.length > 1; 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_currency_id_rounding = {}; 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','not in',['view', 'closed', 'consolidation']]], }, }, 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", domain: [['type_tax_use','in',['purchase', 'all']], ['parent_id', '=', false]], }, }, 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", domain: [['type', '!=', 'view'], ['state', 'not in', ['close','cancelled']]], }, }, }; }, start: function() { this._super(); var self = this; // Retreive statement infos and reconciliation data from the model var lines_filter = [['journal_entry_id', '=', false], ['account_id', '=', false]]; var deferred_promises = []; // Working on specified statement(s) if (self.statement_ids && self.statement_ids.length > 0) { lines_filter.push(['statement_id', 'in', self.statement_ids]); // If only one statement, display its name as title and allow to modify it if (self.single_statement) { deferred_promises.push(self.model_bank_statement .query(["name"]) .filter([['id', '=', self.statement_ids[0]]]) .first() .then(function(title){ self.title = title.name; }) ); } // Anyway, find out how many statement lines are reconciled (for the progressbar) deferred_promises.push(self.model_bank_statement .call("number_of_lines_reconciled", [self.statement_ids]) .then(function(num) { self.already_reconciled_lines = num; }) ); } // Get operation templates 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; }); }) ); // Get the function to format currencies deferred_promises.push(new instance.web.Model("res.currency") .call("get_format_currencies_js_function") .then(function(data) { self.formatCurrency = new Function("amount, currency_id", data); }) ); // Get statement lines deferred_promises.push(self.model_bank_statement_line .query(['id']) .filter(lines_filter) .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 currency id -> rounding factor new instance.web.Model("res.currency") .query(['id', 'rounding']) .all().then(function(data) { _.each(data, function(o) { self.map_currency_id_rounding[o.id] = o.rounding }); }); 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, single_statement: self.single_statement, total_lines: self.already_reconciled_lines+self.st_lines.length })); self.updateProgressbar(); var reconciliations_to_show = self.st_lines.slice(0, self.num_reconciliations_fetched_in_batch); 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], {context:self.session.user_context}) .then(function (data) { var child_promises = []; while ((datum = data.shift()) !== undefined) child_promises.push(self.displayReconciliation(datum.st_line.id, 'inactive', false, true, datum.st_line, datum.reconciliation_proposition)); $.when.apply($, child_promises).then(function(){ self.$(".reconciliation_lines_container").animate({opacity: 1}, self.aestetic_animation_speed); self.getChildren()[0].set("mode", "match"); self.updateShowMoreButton(); }); }); }); }, statementNameClickHandler: function() { if (! this.single_statement) return; this.$(".statement_name span").hide(); this.$(".change_statement_name_field").attr("value", this.title); this.$(".change_statement_name_container").show(); this.$(".change_statement_name_field").focus(); }, changeStatementNameFieldHandler: function(e) { var name = this.$(".change_statement_name_field").val(); if (name === "") this.$(".change_statement_name_button").attr("disabled", "disabled"); else this.$(".change_statement_name_button").removeAttr("disabled"); if (name !== "" && e.which === 13) // Enter this.$(".change_statement_name_button").trigger("click"); if (e.which === 27) { // Escape this.$(".statement_name span").show(); this.$(".change_statement_name_container").hide(); } }, changeStatementButtonClickHandler: function() { var self = this; if (! self.single_statement) return; var name = self.$(".change_statement_name_field").val(); if (name === "") return; self.$(".change_statement_name_button").attr("disabled", "disabled"); return self.model_bank_statement .call("write", [[self.statement_ids[0]], {'name': name}]) .done(function () { self.title = name; self.$(".statement_name span").text(name).show(); self.$(".change_statement_name_container").hide(); }).always(function() { self.$(".change_statement_name_button").removeAttr("disabled"); }); }, keyboardShortcutsHandler: function(e) { var self = this; if ((e.which === 13 || e.which === 10) && (e.ctrlKey || e.metaKey)) { self.persistReconciliations(_.filter(self.getChildren(), function(o) { return o.get("balance").toFixed(3) === "0.000"; })); } }, persistReconciliations: function(reconciliations) { if (reconciliations.length === 0) return; var self = this; // Prepare data var data = []; for (var i=0; i=0; i--) { if (breadcrumbs[i].action && breadcrumbs[i].action.id === action_id) { var title = breadcrumbs[i].get_title(); action_manager.select_breadcrumb(i, _.isArray(title) ? i : undefined); found = true; } } if (!found) self.do_action('history_back'); }); }, 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 +_t(" 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.updateShowMoreButton(); 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, single_statement: self.single_statement, multiple_statements: self.multiple_statements, })); // 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'}); if (self.$(".button_back_to_statement").length !== 0) { self.$(".button_back_to_statement").click(function() { self.goBackToStatementsTreeView(); }); } if (self.$(".button_close_statement").length !== 0) { self.$(".button_close_statement").hide(); self.model_bank_statement .query(["balance_end_real", "balance_end"]) .filter([['id', 'in', self.statement_ids]]) .all() .then(function(data){ if (_.all(data, function(o) { return o.balance_end_real === o.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_ids]) .then(function () { self.goBackToStatementsTreeView(); }, function() { self.$(".button_close_statement").removeAttr("disabled"); }); }); } }); } }, showMoreButtonClickHandler: function() { this.displayReconciliations(this.num_reconciliations_fetched_in_batch); }, updateShowMoreButton: function() { var items_remaining = this.st_lines.length - this.last_displayed_reconciliation_index; if (items_remaining > 0) this.$(".show_more").show().find(".num_items_remaining").text(items_remaining); else this.$(".show_more").hide(); }, 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 .change_partner": "changePartnerClickHandler", "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); this.formatCurrency = this.getParent().formatCurrency; if (context.initial_data_provided) { // Process data _.each(context.reconciliation_proposition, function(line) { this.decorateMoveLine(line, context.st_line.currency_id); }, 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 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, context.reconciliation_proposition); } 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_currency_id_rounding = this.getParent().map_currency_id_rounding; this.map_account_id_code = this.getParent().map_account_id_code; this.presets = this.getParent().presets; this.is_valid = true; this.is_consistent = true; // Used to prevent bad server requests this.can_fetch_more_move_lines; // Tell if we can show more move lines this.filter = ""; // In rare cases like when deleting a statement line's partner we don't want the server to // look for a reconciliation proposition (in this particular case it might find a move line // matching the statement line and decide to set the statement line's partner accordingly) this.do_load_reconciliation_proposition = true; this.set("mode", undefined); this.on("change:mode", this, this.modeChanged); this.set("balance", undefined); // Debit is +, credit is - this.on("change:balance", this, this.balanceChanged); 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.fadeOut(0); self.$el.slideUp(0); } return $.when(self.loadData()).then(function(){ return $.when(self.render()).then(function(){ self.is_consistent = true; // 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.stop(true, true).fadeIn({ duration: self.aestetic_animation_speed, queue: false }).css('display', 'none').slideDown(self.aestetic_animation_speed); } }); }); }); }, loadData: function() { var self = this; if (self.context.initial_data_provided) return; // Get ids of selected move lines (to exclude them from reconciliation proposition) var excluded_move_lines_ids = []; if (self.do_load_reconciliation_proposition) { _.each(self.getParent().excluded_move_lines_ids, function(o){ excluded_move_lines_ids = excluded_move_lines_ids.concat(o); }); } // Load statement line return self.model_bank_statement_line .call("get_data_for_reconciliations", [[self.st_line_id], excluded_move_lines_ids, self.do_load_reconciliation_proposition]) .then(function (data) { self.st_line = data[0].st_line; self.decorateStatementLine(self.st_line); self.partner_id = data[0].st_line.partner_id; if (self.getParent().excluded_move_lines_ids[self.partner_id] === undefined) self.getParent().excluded_move_lines_ids[self.partner_id] = []; var mv_lines = []; _.each(data[0].reconciliation_proposition, function(line) { self.decorateMoveLine(line, self.st_line.currency_id); mv_lines.push(line); }, self); self.set("mv_lines_selected", self.get("mv_lines_selected").concat(mv_lines)); }); }, render: function() { var self = this; 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"); 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.$(".match").slideUp(0); self.$el.addClass("no_partner"); self.set("mode", self.context.mode); self.balanceChanged(); self.updateAccountingViewMatchedLines(); 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); return; } // TODO : the .on handler's returned deferred is lost return $.when(self.set("mode", self.context.mode)).then(function(){ // Make sure the display is OK self.balanceChanged(); self.createdLinesChanged(); self.updateAccountingViewMatchedLines(); }); }, restart: function(mode) { var self = this; mode = (mode === undefined ? 'inactive' : mode); self.context.animate_entrance = false; 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, self.get("mv_lines_selected")); $.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").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."), _t("last")))).open(); return; } self.set("mv_lines_selected", self.get("mv_lines_selected").concat(line)); }, deselectMoveLine: function(mv_line) { var self = this; var line_id = mv_line.dataset.lineid; var line = _.find(self.get("mv_lines_selected"), function(o){ return o.id == line_id}); if (! line) return; // If no line found, we've got a syncing problem (let's turn a deaf ear) // add the line to mv_lines_deselected and remove it from mv_lines_selected self.mv_lines_deselected.unshift(line); var mv_lines_selected = _.filter(self.get("mv_lines_selected"), function(o) { return o.id != line_id }); // remove partial reconciliation stuff if necessary if (line.partial_reconcile === true) self.unpartialReconcileLine(line); if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; self.$el.removeClass("no_match"); self.set("mode", "match"); self.set("mv_lines_selected", mv_lines_selected); }, /** Matches pagination */ pagerControlLeftHandler: function() { var self = this; if (self.$(".pager_control_left").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ } if (self.get("pager_index") === 0) { return; } self.set("pager_index", self.get("pager_index")-1 ); }, pagerControlRightHandler: function() { var self = this; if (self.$(".pager_control_right").hasClass("disabled")) { return; /* shouldn't happen, anyway*/ } if (! self.can_fetch_more_move_lines) { return; } self.set("pager_index", self.get("pager_index")+1 ); }, filterHandler: function() { var self = this; self.set("pager_index", 0); self.filter = self.$(".filter").val(); window.clearTimeout(self.apply_filter_timeout); self.apply_filter_timeout = window.setTimeout(self.proxy('updateMatches'), 200); }, /** Creating */ initializeCreateForm: function() { var self = this; _.each(self.create_form, function(field) { field.set("value", false); }); self.label_field.set("value", self.st_line.name); self.amount_field.set("value", -1*self.get("balance")); self.account_id_field.focus(); }, addLineBeingEdited: function() { var self = this; if (! self.islineCreatedBeingEditedValid()) return; self.set("lines_created", self.get("lines_created").concat(self.get("line_created_being_edited"))); // Add empty created line var new_id = self.get("line_created_being_edited")[0].id + 1; self.set("line_created_being_edited", [{'id': new_id}]); self.initializeCreateForm(); }, removeLine: function($line) { var self = this; var line_id = $line.data("lineid"); // if deleting the created line that is being edited, validate it before if (line_id === self.get("line_created_being_edited")[0].id) { self.addLineBeingEdited(); } self.set("lines_created", _.filter(self.get("lines_created"), function(o) { return o.id != line_id })); self.amount_field.set("value", -1*self.get("balance")); }, presetClickHandler: function(e) { var self = this; self.initializeCreateForm(); var preset = self.presets[e.currentTarget.dataset.presetid]; // Hack : set_value of a field calls a handler that returns a deferred because it could make a RPC call // to compute the tax before it updates the line being edited. Unfortunately this deferred is lost. // Hence this ugly hack to avoid concurrency problem that arose when setting amount (in initializeCreateForm), then tax, then another amount if (preset.tax && self.tax_field) self.tax_field.set_value(false); if (preset.amount && self.amount_field) self.amount_field.set_value(false); for (var key in preset) { if (! preset.hasOwnProperty(key) || key === "amount") continue; if (preset[key] && self.hasOwnProperty(key+"_field")) self[key+"_field"].set_value(preset[key]); } if (preset.amount && self.amount_field) { if (preset.amount_type === "fixed") self.amount_field.set_value(preset.amount); else if (preset.amount_type === "percentage_of_total") self.amount_field.set_value(self.st_line.amount * preset.amount / 100); else if (preset.amount_type === "percentage_of_balance") { self.amount_field.set_value(0); self.updateBalance(); self.amount_field.set_value(-1 * self.get("balance") * preset.amount / 100); } } }, /** Display */ initialLineClickHandler: function() { var self = this; if (self.get("mode") === "match") { self.set("mode", "inactive"); } else { self.set("mode", "match"); } }, lineOpenBalanceClickHandler: function() { var self = this; if (self.get("mode") === "create") { self.set("mode", "match"); } else { self.set("mode", "create"); } }, changePartnerClickHandler: function() { var self = this; var partner_name = self.st_line.partner_name; self.$(".change_partner_container").show(); self.$(".partner_name").hide(); self.changePartner(false).then(function(){ self.$(".change_partner_container").find("input").attr("placeholder", partner_name); self.change_partner_field.$drop_down.trigger("click"); }) }, /** Views updating */ updateAccountingViewMatchedLines: function() { var self = this; $.each(self.$(".tbody_matched_lines .bootstrap_popover"), function(){ $(this).popover('destroy') }); self.$(".tbody_matched_lines").empty(); _(self.get("mv_lines_selected")).each(function(line){ var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: true})); self.bindPopoverTo($line.find(".line_info_button")); if (line.propose_partial_reconcile) self.bindPopoverTo($line.find(".do_partial_reconcile_button")); if (line.partial_reconcile) self.bindPopoverTo($line.find(".undo_partial_reconcile_button")); self.$(".tbody_matched_lines").append($line); }); }, updateAccountingViewCreatedLines: function() { var self = this; $.each(self.$(".tbody_created_lines .bootstrap_popover"), function(){ $(this).popover('destroy') }); self.$(".tbody_created_lines").empty(); _(self.getCreatedLines()).each(function(line){ var $line = $(QWeb.render("bank_statement_reconciliation_created_line", {line: line})); $line.click(function(){ self.removeLine($(this)) }); self.$(".tbody_created_lines").append($line); if (line.no_remove_action) { // Then the previous line's remove button deletes this line too $line.hover(function(){ $(this).prev().addClass("active") },function(){ $(this).prev().removeClass("active") }); } }); }, updateMatchView: function() { var self = this; var table = self.$(".match table"); var nothing_displayed = true; // Display move lines $.each(self.$(".match table .bootstrap_popover"), function(){ $(this).popover('destroy') }); table.empty(); var slice_start = self.get("pager_index") * self.max_move_lines_displayed; var slice_end = (self.get("pager_index")+1) * self.max_move_lines_displayed; _( _.filter(self.mv_lines_deselected, function(o){ return o.name.indexOf(self.filter) !== -1 || o.ref.indexOf(self.filter) !== -1 }) .slice(slice_start, slice_end)).each(function(line){ var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); self.bindPopoverTo($line.find(".line_info_button")); table.append($line); nothing_displayed = false; }); _(self.get("mv_lines")).each(function(line){ var $line = $(QWeb.render("bank_statement_reconciliation_move_line", {line: line, selected: false})); self.bindPopoverTo($line.find(".line_info_button")); table.append($line); nothing_displayed = false; }); if (nothing_displayed && this.filter !== "") table.append(QWeb.render("filter_no_match", {filter_str: self.filter})); }, updatePagerControls: function() { var self = this; if (self.get("pager_index") === 0) self.$(".pager_control_left").addClass("disabled"); else self.$(".pager_control_left").removeClass("disabled"); if (! self.can_fetch_more_move_lines) self.$(".pager_control_right").addClass("disabled"); else self.$(".pager_control_right").removeClass("disabled"); }, /** Properties changed */ // Updates the validation button, the "open balance" line and the partial reconciliation sign balanceChanged: function() { var self = this; // 'reset' the widget to invalid state self.is_valid = false; self.$(".tip_reconciliation_not_balanced").show(); self.$(".tbody_open_balance").empty(); self.$(".button_ok").text(_t("OK")).removeClass("oe_highlight").attr("disabled", "disabled"); // Find out if the counterpart is lower than, equal or greater than the transaction being reconciled var balance_type = undefined; if (Math.abs(self.get("balance")).toFixed(3) === "0.000") balance_type = "equal"; else if (self.get("balance") * self.st_line.amount > 0) balance_type = "greater"; else if (self.get("balance") * self.st_line.amount < 0) balance_type = "lower"; // Adjust to different cases if (balance_type === "equal") { displayValidState(true); } else if (balance_type === "greater") { createOpenBalance(_t("Create Write-off")); } else if (balance_type === "lower") { if (self.st_line.has_no_partner) { createOpenBalance(_t("Choose counterpart")); } else { displayValidState(false, _t("Keep open")); createOpenBalance(_t("Open balance")); } } // Show or hide partial reconciliation if (self.get("mv_lines_selected").length > 0) { var last_line = _.last(self.get("mv_lines_selected")); var propose_partial = self.getCreatedLines().length === 0 && balance_type === "greater" && Math.abs(self.get("balance")) < Math.abs(last_line.debit - last_line.credit) && self.get("balance") * (last_line.debit - last_line.credit) < 0 && ! last_line.partial_reconcile && ! last_line.already_paid; last_line.propose_partial_reconcile = propose_partial; self.updateAccountingViewMatchedLines(); } function displayValidState(higlight_ok_button, ok_button_text) { self.is_valid = true; self.$(".tip_reconciliation_not_balanced").hide(); self.$(".button_ok").removeAttr("disabled"); if (higlight_ok_button) self.$(".button_ok").addClass("oe_highlight"); if (ok_button_text !== undefined) self.$(".button_ok").text(ok_button_text) } function createOpenBalance(name) { var balance = self.get("balance"); var amount = self.formatCurrency(Math.abs(balance), self.st_line.currency_id); var $line = $(QWeb.render("bank_statement_reconciliation_line_open_balance", { debit: balance > 0 ? amount : "", credit: balance < 0 ? amount : "", account_code: self.map_account_id_code[self.st_line.open_balance_account_id] })); if (name !== undefined) $line.find(".cell_label").text(name); self.$(".tbody_open_balance").empty().append($line); } }, modeChanged: function(o, val) { var self = this; self.$(".action_pane.active").removeClass("active"); if (val.oldValue === "create") self.addLineBeingEdited(); if (self.get("mode") === "inactive") { self.$(".match").slideUp(self.animation_speed); self.$(".create").slideUp(self.animation_speed); self.el.dataset.mode = "inactive"; } else if (self.get("mode") === "match") { // TODO : remove this old_animation_speed / new_animation_speed hack // when .on handler's returned deferred's no longer lost var old_animation_speed = self.animation_speed; return $.when(self.updateMatches()).then(function() { var new_animation_speed = self.animation_speed; self.animation_speed = old_animation_speed; if (self.$el.hasClass("no_match")) { self.animation_speed = 0; self.set("mode", "create"); return; } self.$(".match").slideDown(self.animation_speed); self.$(".create").slideUp(self.animation_speed); self.el.dataset.mode = "match"; self.animation_speed = new_animation_speed; }); } else if (self.get("mode") === "create") { self.initializeCreateForm(); self.$(".match").slideUp(self.animation_speed); self.$(".create").slideDown(self.animation_speed); self.el.dataset.mode = "create"; } }, pagerChanged: function() { this.updateMatches(); }, mvLinesChanged: function() { var self = this; // If pager_index is out of range, set it to display the last page if (self.get("pager_index") !== 0 && self.get("mv_lines").length === 0 && ! self.can_fetch_more_move_lines) { self.set("pager_index", 0); } // If there is no match to display, disable match view and pass in mode inactive if (self.get("mv_lines").length + self.mv_lines_deselected.length === 0 && !self.can_fetch_more_move_lines && self.filter === "") { self.$el.addClass("no_match"); if (self.get("mode") === "match") { self.set("mode", "inactive"); } } else { self.$el.removeClass("no_match"); } _.each(self.get("mv_lines"), function(line) { if (line.partial_reconciliation_siblings_ids.length > 0) { var correct_format = _.collect(line.partial_reconciliation_siblings_ids, function(o) { return {'id': o} }); self.getParent().excludeMoveLines(self, self.partner_id, correct_format); } }); self.updateMatchView(); self.updatePagerControls(); }, mvLinesSelectedChanged: function(elt, val) { var self = this; var added_lines = _.difference(val.newValue, val.oldValue); var removed_lines = _.difference(val.oldValue, val.newValue); self.getParent().excludeMoveLines(self, self.partner_id, added_lines); self.getParent().unexcludeMoveLines(self, self.partner_id, removed_lines); $.when(self.updateMatches()).then(function(){ self.updateAccountingViewMatchedLines(); self.updateBalance(); }); }, // Generic function for updating the line_created_being_edited formCreateInputChanged: function(elt, val) { var self = this; var line_created_being_edited = self.get("line_created_being_edited"); line_created_being_edited[0][elt.corresponding_property] = val.newValue; line_created_being_edited[0].currency_id = self.st_line.currency_id; // Specific cases if (elt === self.account_id_field) line_created_being_edited[0].account_num = self.map_account_id_code[elt.get("value")]; // Update tax line var deferred_tax = new $.Deferred(); if (elt === self.tax_id_field || elt === self.amount_field) { var amount = self.amount_field.get("value"); var tax_id = self.tax_id_field.get("value"); if (amount && tax_id) { deferred_tax = $.when(self.model_tax .call("compute_for_bank_reconciliation", [tax_id, amount])) .then(function(data){ line_created_being_edited[0].amount_with_tax = line_created_being_edited[0].amount; line_created_being_edited[0].amount = (data.total.toFixed(3) === amount.toFixed(3) ? amount : data.total); var current_line_cursor = 1; $.each(data.taxes, function(index, tax){ if (tax.amount !== 0.0) { var tax_account_id = (amount > 0 ? tax.account_collected_id : tax.account_paid_id); tax_account_id = tax_account_id !== false ? tax_account_id: line_created_being_edited[0].account_id; line_created_being_edited[current_line_cursor] = { id: line_created_being_edited[0].id, account_id: tax_account_id, account_num: self.map_account_id_code[tax_account_id], label: tax.name, amount: tax.amount, no_remove_action: true, currency_id: self.st_line.currency_id, is_tax_line: true }; current_line_cursor = current_line_cursor + 1; } }); } ); } else { line_created_being_edited.length = 1; deferred_tax.resolve(); } } else { deferred_tax.resolve(); } $.when(deferred_tax).then(function(){ // Format amounts var rounding = 1/self.map_currency_id_rounding[self.st_line.currency_id]; $.each(line_created_being_edited, function(index, val) { if (val.amount) { line_created_being_edited[index].amount = Math.round(val.amount*rounding)/rounding; line_created_being_edited[index].amount_str = self.formatCurrency(Math.abs(val.amount), val.currency_id); } if (val.amount_with_tax) line_created_being_edited[index].amount_with_tax = Math.round(val.amount_with_tax*rounding)/rounding; }); self.set("line_created_being_edited", line_created_being_edited); self.createdLinesChanged(); // TODO For some reason, previous line doesn't trigger change handler }); }, createdLinesChanged: function() { var self = this; self.updateAccountingViewCreatedLines(); self.updateBalance(); if (self.islineCreatedBeingEditedValid()) self.$(".add_line").show(); else self.$(".add_line").hide(); }, /** Model */ doPartialReconcileButtonClickHandler: function(e) { var self = this; var line_id = $(e.currentTarget).closest("tr").data("lineid"); var line = _.find(self.get("mv_lines_selected"), function(o) { return o.id == line_id }); self.partialReconcileLine(line); $(e.currentTarget).popover('destroy'); self.updateAccountingViewMatchedLines(); self.updateBalance(); e.stopPropagation(); }, partialReconcileLine: function(line) { var self = this; var balance = self.get("balance"); line.initial_amount = line.debit !== 0 ? line.debit : -1 * line.credit; if (balance < 0) { line.debit += balance; line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id); } else { line.credit -= balance; line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id); } line.propose_partial_reconcile = false; line.partial_reconcile = true; }, undoPartialReconcileButtonClickHandler: function(e) { var self = this; var line_id = $(e.currentTarget).closest("tr").data("lineid"); var line = _.find(self.get("mv_lines_selected"), function(o) { return o.id == line_id }); self.unpartialReconcileLine(line); $(e.currentTarget).popover('destroy'); self.updateAccountingViewMatchedLines(); self.updateBalance(); e.stopPropagation(); }, unpartialReconcileLine: function(line) { var self = this; if (line.initial_amount > 0) { line.debit = line.initial_amount; line.debit_str = self.formatCurrency(line.debit, self.st_line.currency_id); } else { line.credit = -1 * line.initial_amount; line.credit_str = self.formatCurrency(line.credit, self.st_line.currency_id); } line.propose_partial_reconcile = true; line.partial_reconcile = false; }, updateBalance: function() { var self = this; var mv_lines_selected = self.get("mv_lines_selected"); var lines_selected_num = mv_lines_selected.length; // Undo partial reconciliation if necessary if (lines_selected_num !== 1) { _.each(mv_lines_selected, function(line) { if (line.partial_reconcile === true) self.unpartialReconcileLine(line); if (line.propose_partial_reconcile === true) line.propose_partial_reconcile = false; }); self.updateAccountingViewMatchedLines(); } // Compute balance var balance = 0; balance -= self.st_line.amount; _.each(mv_lines_selected, function(o) { balance = balance - o.debit + o.credit; }); _.each(self.getCreatedLines(), function(o) { balance += o.amount; }); // Dealing with floating-point balance = Math.round(balance*1000)/1000; self.set("balance", balance); }, // Loads move lines according to the widget's state updateMatches: function() { var self = this; var deselected_lines_num = self.mv_lines_deselected.length; var offset = self.get("pager_index") * self.max_move_lines_displayed - deselected_lines_num; if (offset < 0) offset = 0; var limit = (self.get("pager_index")+1) * self.max_move_lines_displayed - deselected_lines_num; if (limit > self.max_move_lines_displayed) limit = self.max_move_lines_displayed; var excluded_ids = _.collect(self.get("mv_lines_selected").concat(self.mv_lines_deselected), function(o) { return o.id; }); var globally_excluded_ids = []; if (self.st_line.has_no_partner) _.each(self.getParent().excluded_move_lines_ids, function(o) { globally_excluded_ids = globally_excluded_ids.concat(o) }); else globally_excluded_ids = self.getParent().excluded_move_lines_ids[self.partner_id]; if (globally_excluded_ids !== undefined) for (var i=0; i 0) { return self.model_bank_statement_line .call("get_move_lines_for_reconciliation_by_statement_line_id", [self.st_line.id, excluded_ids, self.filter, offset, limit], {context:self.session.user_context}) .then(function (lines) { _.each(lines, function(line) { self.decorateMoveLine(line, self.st_line.currency_id) }, self); // 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) self.can_fetch_more_move_lines = (lines.length === limit); self.set("mv_lines", lines.slice(0, limit-1)); }); } else { self.set("mv_lines", []); } }, // Changes the partner_id of the statement_line in the DB and reloads the widget changePartner: function(partner_id) { var self = this; self.is_consistent = false; return self.model_bank_statement_line // Update model .call("write", [[self.st_line_id], {'partner_id': partner_id}]) .then(function () { self.do_load_reconciliation_proposition = true; // the server will set the statement line with the new partner self.animation_speed = 0; return $.when(self.restart(self.get("mode"))).then(function(){ self.is_consistent = true; self.set("mode", "match"); }); }); }, // Returns an object that can be passed to process_reconciliation() prepareSelectedMoveLineForPersisting: function(line) { return { name: line.name, debit: line.debit, credit: line.credit, counterpart_move_line_id: line.id, }; }, // idem prepareCreatedMoveLineForPersisting: function(line) { var dict = {}; if (dict['account_id'] === undefined) dict['account_id'] = line.account_id; dict['name'] = line.label; var amount = line.tax_id ? line.amount_with_tax: line.amount; if (amount > 0) dict['credit'] = amount; if (amount < 0) dict['debit'] = -1 * amount; if (line.tax_id) dict['account_tax_id'] = line.tax_id; if (line.is_tax_line) dict['is_tax_line'] = line.is_tax_line; if (line.analytic_account_id) dict['analytic_account_id'] = line.analytic_account_id; return dict; }, // idem prepareOpenBalanceForPersisting: function() { var balance = this.get("balance"); var dict = {}; dict['account_id'] = this.st_line.open_balance_account_id; dict['name'] = this.st_line.name + ' : ' + _t("Open balance"); if (balance > 0) dict['debit'] = balance; if (balance < 0) dict['credit'] = -1*balance; return dict; }, makeMoveLineDicts: function() { var self = this; var mv_line_dicts = []; _.each(self.get("mv_lines_selected"), function(o) { mv_line_dicts.push(self.prepareSelectedMoveLineForPersisting(o)) }); _.each(self.getCreatedLines(), function(o) { mv_line_dicts.push(self.prepareCreatedMoveLineForPersisting(o)) }); if (Math.abs(self.get("balance")).toFixed(3) !== "0.000") mv_line_dicts.push(self.prepareOpenBalanceForPersisting()); return mv_line_dicts; }, // Persist data, notify parent view and terminate widget persistAndDestroy: function(speed) { var self = this; speed = (isNaN(speed) ? self.animation_speed : speed); if (! self.is_consistent) return; // Sliding animation var height = self.$el.outerHeight(); var container = $("
"); container.css("height", height) .css("marginTop", self.$el.css("marginTop")) .css("marginBottom", self.$el.css("marginBottom")); self.$el.wrap(container); var deferred_animation = self.$el.parent().slideUp(speed*height/150); // RPC self.$(".button_ok").attr("disabled", "disabled"); return self.model_bank_statement_line .call("process_reconciliation", [self.st_line_id, self.makeMoveLineDicts()]) .done(function () { self.getParent().unexcludeMoveLines(self, self.partner_id, self.get("mv_lines_selected")); $.each(self.$(".bootstrap_popover"), function(){ $(this).popover('destroy') }); return $.when(deferred_animation).then(function(){ self.$el.parent().remove(); var parent = self.getParent(); return $.when(self.destroy()).then(function() { parent.childValidated(self); }); }); }).fail(function(){ self.$el.parent().slideDown(speed*height/150, function(){ self.$el.unwrap(); }); }).always(function() { self.$(".button_ok").removeAttr("disabled"); }); }, }); instance.web.views.add('tree_account_reconciliation', 'instance.web.account.ReconciliationListView'); instance.web.account.ReconciliationListView = instance.web.ListView.extend({ init: function() { this._super.apply(this, arguments); var self = this; this.current_partner = null; this.on('record_selected', this, function() { if (self.get_selected_ids().length === 0) { self.$(".oe_account_recon_reconcile").attr("disabled", ""); } else { self.$(".oe_account_recon_reconcile").removeAttr("disabled"); } }); }, load_list: function() { var self = this; var tmp = this._super.apply(this, arguments); if (this.partners) { this.$el.prepend(QWeb.render("AccountReconciliation", {widget: this})); this.$(".oe_account_recon_previous").click(function() { self.current_partner = (((self.current_partner - 1) % self.partners.length) + self.partners.length) % self.partners.length; self.search_by_partner(); }); this.$(".oe_account_recon_next").click(function() { self.current_partner = (self.current_partner + 1) % self.partners.length; self.search_by_partner(); }); this.$(".oe_account_recon_reconcile").click(function() { self.reconcile(); }); this.$(".oe_account_recom_mark_as_reconciled").click(function() { self.mark_as_reconciled(); }); } return tmp; }, do_search: function(domain, context, group_by) { var self = this; this.last_domain = domain; this.last_context = context; this.last_group_by = group_by; this.old_search = _.bind(this._super, this); var mod = new instance.web.Model("account.move.line", context, domain); return mod.call("list_partners_to_reconcile", [context, domain]).then(function(result) { var current = self.current_partner !== null ? self.partners[self.current_partner][0] : null; self.partners = result; var index = _.find(_.range(self.partners.length), function(el) { if (current === self.partners[el][0]) return true; }); if (index !== undefined) self.current_partner = index; else self.current_partner = self.partners.length == 0 ? null : 0; self.search_by_partner(); }); }, search_by_partner: function() { var self = this; var fct = function() { return self.old_search(new instance.web.CompoundDomain(self.last_domain, [["partner_id", "in", self.current_partner === null ? [] : [self.partners[self.current_partner][0]] ]]), self.last_context, self.last_group_by); }; if (self.current_partner === null) { self.last_reconciliation_date = _t("Never"); return fct(); } else { return new instance.web.Model("res.partner").call("read", [self.partners[self.current_partner][0], ["last_reconciliation_date"]]).then(function(res) { self.last_reconciliation_date = instance.web.format_value(res.last_reconciliation_date, {"type": "datetime"}, _t("Never")); return fct(); }); } }, reconcile: function() { var self = this; var ids = this.get_selected_ids(); if (ids.length === 0) { new instance.web.Dialog(this, { title: _t("Warning"), size: 'medium', }, $("
").text(_t("You must choose at least one record."))).open(); return false; } new instance.web.Model("ir.model.data").call("get_object_reference", ["account", "action_view_account_move_line_reconcile"]).then(function(result) { var additional_context = _.extend({ active_id: ids[0], active_ids: ids, active_model: self.model }); return self.rpc("/web/action/load", { action_id: result[1], context: additional_context }).done(function (result) { result.context = instance.web.pyeval.eval('contexts', [result.context, additional_context]); result.flags = result.flags || {}; result.flags.new_window = true; return self.do_action(result, { on_close: function () { self.do_search(self.last_domain, self.last_context, self.last_group_by); } }); }); }); }, mark_as_reconciled: function() { var self = this; var id = self.partners[self.current_partner][0]; new instance.web.Model("res.partner").call("mark_as_reconciled", [[id]]).then(function() { self.do_search(self.last_domain, self.last_context, self.last_group_by); }); }, do_select: function (ids, records) { this.trigger('record_selected') this._super.apply(this, arguments); }, }); };